mirror of
https://github.com/Yuvi9587/Kemono-Downloader.git
synced 2025-12-29 16:14:44 +00:00
Commit
This commit is contained in:
@@ -752,6 +752,17 @@ class PostProcessorWorker:
|
|||||||
|
|
||||||
effective_unwanted_keywords_for_folder_naming = self.unwanted_keywords.copy()
|
effective_unwanted_keywords_for_folder_naming = self.unwanted_keywords.copy()
|
||||||
is_full_creator_download_no_char_filter = not self.target_post_id_from_initial_url and not current_character_filters
|
is_full_creator_download_no_char_filter = not self.target_post_id_from_initial_url and not current_character_filters
|
||||||
|
|
||||||
|
if (self.show_external_links or self.extract_links_only):
|
||||||
|
embed_data = post_data.get('embed')
|
||||||
|
if isinstance(embed_data, dict) and embed_data.get('url'):
|
||||||
|
embed_url = embed_data['url']
|
||||||
|
embed_subject = embed_data.get('subject', embed_url) # Use subject as link text, fallback to URL
|
||||||
|
platform = get_link_platform(embed_url)
|
||||||
|
|
||||||
|
self.logger(f" 🔗 Found embed link: {embed_url}")
|
||||||
|
self._emit_signal('external_link', post_title, embed_subject, embed_url, platform, "")
|
||||||
|
|
||||||
if is_full_creator_download_no_char_filter and self.creator_download_folder_ignore_words:
|
if is_full_creator_download_no_char_filter and self.creator_download_folder_ignore_words:
|
||||||
self.logger(f" Applying creator download specific folder ignore words ({len(self.creator_download_folder_ignore_words)} words).")
|
self.logger(f" Applying creator download specific folder ignore words ({len(self.creator_download_folder_ignore_words)} words).")
|
||||||
effective_unwanted_keywords_for_folder_naming.update(self.creator_download_folder_ignore_words)
|
effective_unwanted_keywords_for_folder_naming.update(self.creator_download_folder_ignore_words)
|
||||||
|
|||||||
@@ -3,15 +3,19 @@ import os
|
|||||||
import re
|
import re
|
||||||
import traceback
|
import traceback
|
||||||
import json
|
import json
|
||||||
|
import base64
|
||||||
|
import time
|
||||||
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
|
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
|
||||||
|
|
||||||
# --- Third-Party Library Imports ---
|
# --- Third-Party Library Imports ---
|
||||||
|
# Make sure to install these: pip install requests pycryptodome gdown
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from mega import Mega
|
from Crypto.Cipher import AES
|
||||||
MEGA_AVAILABLE = True
|
PYCRYPTODOME_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
MEGA_AVAILABLE = False
|
PYCRYPTODOME_AVAILABLE = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import gdown
|
import gdown
|
||||||
@@ -19,17 +23,15 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
GDRIVE_AVAILABLE = False
|
GDRIVE_AVAILABLE = False
|
||||||
|
|
||||||
# --- Helper Functions ---
|
# --- Constants ---
|
||||||
|
MEGA_API_URL = "https://g.api.mega.co.nz"
|
||||||
|
|
||||||
|
# --- Helper Functions (Original and New) ---
|
||||||
|
|
||||||
def _get_filename_from_headers(headers):
|
def _get_filename_from_headers(headers):
|
||||||
"""
|
"""
|
||||||
Extracts a filename from the Content-Disposition header.
|
Extracts a filename from the Content-Disposition header.
|
||||||
|
(This is from your original file and is kept for Dropbox downloads)
|
||||||
Args:
|
|
||||||
headers (dict): A dictionary of HTTP response headers.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str or None: The extracted filename, or None if not found.
|
|
||||||
"""
|
"""
|
||||||
cd = headers.get('content-disposition')
|
cd = headers.get('content-disposition')
|
||||||
if not cd:
|
if not cd:
|
||||||
@@ -37,64 +39,180 @@ def _get_filename_from_headers(headers):
|
|||||||
|
|
||||||
fname_match = re.findall('filename="?([^"]+)"?', cd)
|
fname_match = re.findall('filename="?([^"]+)"?', cd)
|
||||||
if fname_match:
|
if fname_match:
|
||||||
# Sanitize the filename to prevent directory traversal issues
|
|
||||||
# and remove invalid characters for most filesystems.
|
|
||||||
sanitized_name = re.sub(r'[<>:"/\\|?*]', '_', fname_match[0].strip())
|
sanitized_name = re.sub(r'[<>:"/\\|?*]', '_', fname_match[0].strip())
|
||||||
return sanitized_name
|
return sanitized_name
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# --- Main Service Downloader Functions ---
|
# --- NEW: Helper functions for Mega decryption ---
|
||||||
|
|
||||||
|
def urlb64_to_b64(s):
|
||||||
|
"""Converts a URL-safe base64 string to a standard base64 string."""
|
||||||
|
s = s.replace('-', '+').replace('_', '/')
|
||||||
|
s += '=' * (-len(s) % 4)
|
||||||
|
return s
|
||||||
|
|
||||||
|
def b64_to_bytes(s):
|
||||||
|
"""Decodes a URL-safe base64 string to bytes."""
|
||||||
|
return base64.b64decode(urlb64_to_b64(s))
|
||||||
|
|
||||||
|
def bytes_to_hex(b):
|
||||||
|
"""Converts bytes to a hex string."""
|
||||||
|
return b.hex()
|
||||||
|
|
||||||
|
def hex_to_bytes(h):
|
||||||
|
"""Converts a hex string to bytes."""
|
||||||
|
return bytes.fromhex(h)
|
||||||
|
|
||||||
|
def hrk2hk(hex_raw_key):
|
||||||
|
"""Derives the final AES key from the raw key components for Mega."""
|
||||||
|
key_part1 = int(hex_raw_key[0:16], 16)
|
||||||
|
key_part2 = int(hex_raw_key[16:32], 16)
|
||||||
|
key_part3 = int(hex_raw_key[32:48], 16)
|
||||||
|
key_part4 = int(hex_raw_key[48:64], 16)
|
||||||
|
|
||||||
|
final_key_part1 = key_part1 ^ key_part3
|
||||||
|
final_key_part2 = key_part2 ^ key_part4
|
||||||
|
|
||||||
|
return f'{final_key_part1:016x}{final_key_part2:016x}'
|
||||||
|
|
||||||
|
def decrypt_at(at_b64, key_bytes):
|
||||||
|
"""Decrypts the 'at' attribute to get file metadata."""
|
||||||
|
at_bytes = b64_to_bytes(at_b64)
|
||||||
|
iv = b'\0' * 16
|
||||||
|
cipher = AES.new(key_bytes, AES.MODE_CBC, iv)
|
||||||
|
decrypted_at = cipher.decrypt(at_bytes)
|
||||||
|
return decrypted_at.decode('utf-8').strip('\0').replace('MEGA', '')
|
||||||
|
|
||||||
|
# --- NEW: Core Logic for Mega Downloads ---
|
||||||
|
|
||||||
|
def get_mega_file_info(file_id, file_key, session, logger_func):
|
||||||
|
"""Fetches file metadata and the temporary download URL from the Mega API."""
|
||||||
|
try:
|
||||||
|
hex_raw_key = bytes_to_hex(b64_to_bytes(file_key))
|
||||||
|
hex_key = hrk2hk(hex_raw_key)
|
||||||
|
key_bytes = hex_to_bytes(hex_key)
|
||||||
|
|
||||||
|
# Request file attributes
|
||||||
|
payload = [{"a": "g", "p": file_id}]
|
||||||
|
response = session.post(f"{MEGA_API_URL}/cs", json=payload, timeout=20)
|
||||||
|
response.raise_for_status()
|
||||||
|
res_json = response.json()
|
||||||
|
|
||||||
|
if isinstance(res_json, list) and isinstance(res_json[0], int) and res_json[0] < 0:
|
||||||
|
logger_func(f" [Mega] ❌ API Error: {res_json[0]}. The link may be invalid or removed.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
file_size = res_json[0]['s']
|
||||||
|
at_b64 = res_json[0]['at']
|
||||||
|
|
||||||
|
# Decrypt attributes to get the file name
|
||||||
|
at_dec_json_str = decrypt_at(at_b64, key_bytes)
|
||||||
|
at_dec_json = json.loads(at_dec_json_str)
|
||||||
|
file_name = at_dec_json['n']
|
||||||
|
|
||||||
|
# Request the temporary download URL
|
||||||
|
payload = [{"a": "g", "g": 1, "p": file_id}]
|
||||||
|
response = session.post(f"{MEGA_API_URL}/cs", json=payload, timeout=20)
|
||||||
|
response.raise_for_status()
|
||||||
|
res_json = response.json()
|
||||||
|
dl_temp_url = res_json[0]['g']
|
||||||
|
|
||||||
|
return {
|
||||||
|
'file_name': file_name,
|
||||||
|
'file_size': file_size,
|
||||||
|
'dl_url': dl_temp_url,
|
||||||
|
'hex_raw_key': hex_raw_key
|
||||||
|
}
|
||||||
|
except (requests.RequestException, json.JSONDecodeError, KeyError, ValueError) as e:
|
||||||
|
logger_func(f" [Mega] ❌ Failed to get file info: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def download_and_decrypt_mega_file(info, download_path, logger_func):
|
||||||
|
"""Downloads the file and decrypts it chunk by chunk, reporting progress."""
|
||||||
|
file_name = info['file_name']
|
||||||
|
file_size = info['file_size']
|
||||||
|
dl_url = info['dl_url']
|
||||||
|
hex_raw_key = info['hex_raw_key']
|
||||||
|
|
||||||
|
final_path = os.path.join(download_path, file_name)
|
||||||
|
|
||||||
|
if os.path.exists(final_path) and os.path.getsize(final_path) == file_size:
|
||||||
|
logger_func(f" [Mega] ℹ️ File '{file_name}' already exists with the correct size. Skipping.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Prepare for decryption
|
||||||
|
key = hex_to_bytes(hrk2hk(hex_raw_key))
|
||||||
|
iv_hex = hex_raw_key[32:48] + '0000000000000000'
|
||||||
|
iv_bytes = hex_to_bytes(iv_hex)
|
||||||
|
cipher = AES.new(key, AES.MODE_CTR, initial_value=iv_bytes, nonce=b'')
|
||||||
|
|
||||||
|
try:
|
||||||
|
with requests.get(dl_url, stream=True, timeout=(15, 300)) as r:
|
||||||
|
r.raise_for_status()
|
||||||
|
downloaded_bytes = 0
|
||||||
|
last_log_time = time.time()
|
||||||
|
|
||||||
|
with open(final_path, 'wb') as f:
|
||||||
|
for chunk in r.iter_content(chunk_size=8192):
|
||||||
|
if not chunk:
|
||||||
|
continue
|
||||||
|
decrypted_chunk = cipher.decrypt(chunk)
|
||||||
|
f.write(decrypted_chunk)
|
||||||
|
downloaded_bytes += len(chunk)
|
||||||
|
|
||||||
|
# Log progress every second
|
||||||
|
current_time = time.time()
|
||||||
|
if current_time - last_log_time > 1:
|
||||||
|
progress_percent = (downloaded_bytes / file_size) * 100 if file_size > 0 else 0
|
||||||
|
logger_func(f" [Mega] Downloading '{file_name}': {downloaded_bytes/1024/1024:.2f}MB / {file_size/1024/1024:.2f}MB ({progress_percent:.1f}%)")
|
||||||
|
last_log_time = current_time
|
||||||
|
|
||||||
|
logger_func(f" [Mega] ✅ Successfully downloaded '{file_name}' to '{download_path}'")
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logger_func(f" [Mega] ❌ Download failed for '{file_name}': {e}")
|
||||||
|
except IOError as e:
|
||||||
|
logger_func(f" [Mega] ❌ Could not write to file '{final_path}': {e}")
|
||||||
|
except Exception as e:
|
||||||
|
logger_func(f" [Mega] ❌ An unexpected error occurred during download/decryption: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# --- REPLACEMENT Main Service Downloader Function for Mega ---
|
||||||
|
|
||||||
def download_mega_file(mega_url, download_path, logger_func=print):
|
def download_mega_file(mega_url, download_path, logger_func=print):
|
||||||
"""
|
"""
|
||||||
Downloads a file from a Mega.nz URL.
|
Downloads a file from a Mega.nz URL using direct requests and decryption.
|
||||||
Handles both public links and links that include a decryption key.
|
This replaces the old mega.py implementation.
|
||||||
"""
|
"""
|
||||||
if not MEGA_AVAILABLE:
|
if not PYCRYPTODOME_AVAILABLE:
|
||||||
logger_func("❌ Mega download failed: 'mega.py' library is not installed.")
|
logger_func("❌ Mega download failed: 'pycryptodome' library is not installed. Please run: pip install pycryptodome")
|
||||||
return
|
return
|
||||||
|
|
||||||
logger_func(f" [Mega] Initializing Mega client...")
|
logger_func(f" [Mega] Initializing download for: {mega_url}")
|
||||||
try:
|
|
||||||
mega = Mega()
|
# Regex to capture file ID and key from both old and new URL formats
|
||||||
# Anonymous login is sufficient for public links
|
match = re.search(r'mega(?:\.co)?\.nz/(?:file/|#!)?([a-zA-Z0-9]+)(?:#|!)([a-zA-Z0-9_.-]+)', mega_url)
|
||||||
m = mega.login()
|
if not match:
|
||||||
|
logger_func(f" [Mega] ❌ Error: Invalid Mega URL format.")
|
||||||
|
return
|
||||||
|
|
||||||
|
file_id = match.group(1)
|
||||||
|
file_key = match.group(2)
|
||||||
|
|
||||||
# --- MODIFIED PART: Added error handling for invalid links ---
|
session = requests.Session()
|
||||||
try:
|
session.headers.update({'User-Agent': 'Kemono-Downloader-PyQt/1.0'})
|
||||||
file_details = m.find(mega_url)
|
|
||||||
if file_details is None:
|
file_info = get_mega_file_info(file_id, file_key, session, logger_func)
|
||||||
logger_func(f" [Mega] ❌ Download failed. The link appears to be invalid or has been taken down: {mega_url}")
|
if not file_info:
|
||||||
return
|
logger_func(f" [Mega] ❌ Failed to get file info. The link may be invalid or expired. Aborting.")
|
||||||
except (ValueError, json.JSONDecodeError) as e:
|
return
|
||||||
# This block catches the "Expecting value" error
|
|
||||||
logger_func(f" [Mega] ❌ Download failed. The link is likely invalid or expired. Error: {e}")
|
|
||||||
return
|
|
||||||
except Exception as e:
|
|
||||||
# Catch other potential errors from the mega.py library
|
|
||||||
logger_func(f" [Mega] ❌ An unexpected error occurred trying to access the link: {e}")
|
|
||||||
return
|
|
||||||
# --- END OF MODIFIED PART ---
|
|
||||||
|
|
||||||
filename = file_details[1]['a']['n']
|
logger_func(f" [Mega] File found: '{file_info['file_name']}' (Size: {file_info['file_size'] / 1024 / 1024:.2f} MB)")
|
||||||
logger_func(f" [Mega] File found: '{filename}'. Starting download...")
|
|
||||||
|
download_and_decrypt_mega_file(file_info, download_path, logger_func)
|
||||||
|
|
||||||
# Sanitize filename before saving
|
|
||||||
safe_filename = "".join([c for c in filename if c.isalpha() or c.isdigit() or c in (' ', '.', '_', '-')]).rstrip()
|
|
||||||
final_path = os.path.join(download_path, safe_filename)
|
|
||||||
|
|
||||||
# Check if file already exists
|
# --- ORIGINAL Functions for Google Drive and Dropbox (Unchanged) ---
|
||||||
if os.path.exists(final_path):
|
|
||||||
logger_func(f" [Mega] ℹ️ File '{safe_filename}' already exists. Skipping download.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Start the download
|
|
||||||
m.download_url(mega_url, dest_path=download_path, dest_filename=safe_filename)
|
|
||||||
logger_func(f" [Mega] ✅ Successfully downloaded '{safe_filename}' to '{download_path}'")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger_func(f" [Mega] ❌ An unexpected error occurred during the Mega download process: {e}")
|
|
||||||
|
|
||||||
def download_gdrive_file(url, download_path, logger_func=print):
|
def download_gdrive_file(url, download_path, logger_func=print):
|
||||||
"""Downloads a file from a Google Drive link."""
|
"""Downloads a file from a Google Drive link."""
|
||||||
@@ -103,12 +221,9 @@ def download_gdrive_file(url, download_path, logger_func=print):
|
|||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
logger_func(f" [G-Drive] Starting download for: {url}")
|
logger_func(f" [G-Drive] Starting download for: {url}")
|
||||||
# --- MODIFIED PART: Added a message and set quiet=True ---
|
|
||||||
logger_func(" [G-Drive] Download in progress... This may take some time. Please wait.")
|
logger_func(" [G-Drive] Download in progress... This may take some time. Please wait.")
|
||||||
|
|
||||||
# By setting quiet=True, the progress bar will no longer be printed to the terminal.
|
|
||||||
output_path = gdown.download(url, output=download_path, quiet=True, fuzzy=True)
|
output_path = gdown.download(url, output=download_path, quiet=True, fuzzy=True)
|
||||||
# --- END OF MODIFIED PART ---
|
|
||||||
|
|
||||||
if output_path and os.path.exists(output_path):
|
if output_path and os.path.exists(output_path):
|
||||||
logger_func(f" [G-Drive] ✅ Successfully downloaded to '{output_path}'")
|
logger_func(f" [G-Drive] ✅ Successfully downloaded to '{output_path}'")
|
||||||
@@ -120,15 +235,9 @@ def download_gdrive_file(url, download_path, logger_func=print):
|
|||||||
def download_dropbox_file(dropbox_link, download_path=".", logger_func=print):
|
def download_dropbox_file(dropbox_link, download_path=".", logger_func=print):
|
||||||
"""
|
"""
|
||||||
Downloads a file from a public Dropbox link by modifying the URL for direct download.
|
Downloads a file from a public Dropbox link by modifying the URL for direct download.
|
||||||
|
|
||||||
Args:
|
|
||||||
dropbox_link (str): The public Dropbox link to the file.
|
|
||||||
download_path (str): The directory to save the downloaded file.
|
|
||||||
logger_func (callable): Function to use for logging.
|
|
||||||
"""
|
"""
|
||||||
logger_func(f" [Dropbox] Attempting to download: {dropbox_link}")
|
logger_func(f" [Dropbox] Attempting to download: {dropbox_link}")
|
||||||
|
|
||||||
# Modify the Dropbox URL to force a direct download instead of showing the preview page.
|
|
||||||
parsed_url = urlparse(dropbox_link)
|
parsed_url = urlparse(dropbox_link)
|
||||||
query_params = parse_qs(parsed_url.query)
|
query_params = parse_qs(parsed_url.query)
|
||||||
query_params['dl'] = ['1']
|
query_params['dl'] = ['1']
|
||||||
@@ -145,13 +254,11 @@ def download_dropbox_file(dropbox_link, download_path=".", logger_func=print):
|
|||||||
with requests.get(direct_download_url, stream=True, allow_redirects=True, timeout=(10, 300)) as r:
|
with requests.get(direct_download_url, stream=True, allow_redirects=True, timeout=(10, 300)) as r:
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
# Determine filename from headers or URL
|
|
||||||
filename = _get_filename_from_headers(r.headers) or os.path.basename(parsed_url.path) or "dropbox_file"
|
filename = _get_filename_from_headers(r.headers) or os.path.basename(parsed_url.path) or "dropbox_file"
|
||||||
full_save_path = os.path.join(download_path, filename)
|
full_save_path = os.path.join(download_path, filename)
|
||||||
|
|
||||||
logger_func(f" [Dropbox] Starting download of '{filename}'...")
|
logger_func(f" [Dropbox] Starting download of '{filename}'...")
|
||||||
|
|
||||||
# Write file to disk in chunks
|
|
||||||
with open(full_save_path, 'wb') as f:
|
with open(full_save_path, 'wb') as f:
|
||||||
for chunk in r.iter_content(chunk_size=8192):
|
for chunk in r.iter_content(chunk_size=8192):
|
||||||
f.write(chunk)
|
f.write(chunk)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from PyQt5.QtCore import QUrl, QSize, Qt
|
|||||||
from PyQt5.QtGui import QIcon, QDesktopServices
|
from PyQt5.QtGui import QIcon, QDesktopServices
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QApplication, QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout,
|
QApplication, QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout,
|
||||||
QStackedWidget, QScrollArea, QFrame, QWidget
|
QStackedWidget, QListWidget, QFrame, QWidget, QScrollArea
|
||||||
)
|
)
|
||||||
from ...i18n.translator import get_translation
|
from ...i18n.translator import get_translation
|
||||||
from ..main_window import get_app_icon_object
|
from ..main_window import get_app_icon_object
|
||||||
@@ -46,13 +46,12 @@ class TourStepWidget(QWidget):
|
|||||||
layout.addWidget(scroll_area, 1)
|
layout.addWidget(scroll_area, 1)
|
||||||
|
|
||||||
|
|
||||||
class HelpGuideDialog (QDialog ):
|
class HelpGuideDialog(QDialog):
|
||||||
"""A multi-page dialog for displaying the feature guide."""
|
"""A multi-page dialog for displaying the feature guide with a navigation list."""
|
||||||
def __init__ (self ,steps_data ,parent_app ,parent =None ):
|
def __init__(self, steps_data, parent_app, parent=None):
|
||||||
super ().__init__ (parent )
|
super().__init__(parent)
|
||||||
self .current_step =0
|
self.steps_data = steps_data
|
||||||
self .steps_data =steps_data
|
self.parent_app = parent_app
|
||||||
self .parent_app =parent_app
|
|
||||||
|
|
||||||
scale = self.parent_app.scale_factor if hasattr(self.parent_app, 'scale_factor') else 1.0
|
scale = self.parent_app.scale_factor if hasattr(self.parent_app, 'scale_factor') else 1.0
|
||||||
|
|
||||||
@@ -61,7 +60,7 @@ class HelpGuideDialog (QDialog ):
|
|||||||
self.setWindowIcon(app_icon)
|
self.setWindowIcon(app_icon)
|
||||||
|
|
||||||
self.setModal(True)
|
self.setModal(True)
|
||||||
self.resize(int(650 * scale), int(600 * scale))
|
self.resize(int(800 * scale), int(650 * scale))
|
||||||
|
|
||||||
dialog_font_size = int(11 * scale)
|
dialog_font_size = int(11 * scale)
|
||||||
|
|
||||||
@@ -69,6 +68,7 @@ class HelpGuideDialog (QDialog ):
|
|||||||
if hasattr(self.parent_app, 'current_theme') and self.parent_app.current_theme == "dark":
|
if hasattr(self.parent_app, 'current_theme') and self.parent_app.current_theme == "dark":
|
||||||
current_theme_style = get_dark_theme(scale)
|
current_theme_style = get_dark_theme(scale)
|
||||||
else:
|
else:
|
||||||
|
# Basic light theme fallback
|
||||||
current_theme_style = f"""
|
current_theme_style = f"""
|
||||||
QDialog {{ background-color: #F0F0F0; border: 1px solid #B0B0B0; }}
|
QDialog {{ background-color: #F0F0F0; border: 1px solid #B0B0B0; }}
|
||||||
QLabel {{ color: #1E1E1E; }}
|
QLabel {{ color: #1E1E1E; }}
|
||||||
@@ -86,118 +86,107 @@ class HelpGuideDialog (QDialog ):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
self.setStyleSheet(current_theme_style)
|
self.setStyleSheet(current_theme_style)
|
||||||
self ._init_ui ()
|
self._init_ui()
|
||||||
if self .parent_app :
|
if self.parent_app:
|
||||||
self .move (self .parent_app .geometry ().center ()-self .rect ().center ())
|
self.move(self.parent_app.geometry().center() - self.rect().center())
|
||||||
|
|
||||||
def _tr (self ,key ,default_text =""):
|
def _tr(self, key, default_text=""):
|
||||||
"""Helper to get translation based on current app language."""
|
"""Helper to get translation based on current app language."""
|
||||||
if callable (get_translation )and self .parent_app :
|
if callable(get_translation) and self.parent_app:
|
||||||
return get_translation (self .parent_app .current_selected_language ,key ,default_text )
|
return get_translation(self.parent_app.current_selected_language, key, default_text)
|
||||||
return default_text
|
return default_text
|
||||||
|
|
||||||
|
def _init_ui(self):
|
||||||
|
main_layout = QVBoxLayout(self)
|
||||||
|
main_layout.setContentsMargins(15, 15, 15, 15)
|
||||||
|
main_layout.setSpacing(10)
|
||||||
|
|
||||||
def _init_ui (self ):
|
# Title
|
||||||
main_layout =QVBoxLayout (self )
|
title_label = QLabel(self._tr("help_guide_dialog_title", "Kemono Downloader - Feature Guide"))
|
||||||
main_layout .setContentsMargins (0 ,0 ,0 ,0 )
|
scale = getattr(self.parent_app, 'scale_factor', 1.0)
|
||||||
main_layout .setSpacing (0 )
|
title_font_size = int(16 * scale)
|
||||||
|
title_label.setStyleSheet(f"font-size: {title_font_size}pt; font-weight: bold; color: #E0E0E0;")
|
||||||
|
title_label.setAlignment(Qt.AlignCenter)
|
||||||
|
main_layout.addWidget(title_label)
|
||||||
|
|
||||||
self .stacked_widget =QStackedWidget ()
|
# Content Layout (Navigation + Stacked Pages)
|
||||||
main_layout .addWidget (self .stacked_widget ,1 )
|
content_layout = QHBoxLayout()
|
||||||
|
main_layout.addLayout(content_layout, 1)
|
||||||
|
|
||||||
self .tour_steps_widgets =[]
|
self.nav_list = QListWidget()
|
||||||
scale = self.parent_app.scale_factor if hasattr(self.parent_app, 'scale_factor') else 1.0
|
self.nav_list.setFixedWidth(int(220 * scale))
|
||||||
for title, content in self.steps_data:
|
self.nav_list.setStyleSheet(f"""
|
||||||
step_widget = TourStepWidget(title, content, scale=scale)
|
QListWidget {{
|
||||||
self.tour_steps_widgets.append(step_widget)
|
background-color: #2E2E2E;
|
||||||
|
border: 1px solid #4A4A4A;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: {int(11 * scale)}pt;
|
||||||
|
}}
|
||||||
|
QListWidget::item {{
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid #4A4A4A;
|
||||||
|
}}
|
||||||
|
QListWidget::item:selected {{
|
||||||
|
background-color: #87CEEB;
|
||||||
|
color: #2E2E2E;
|
||||||
|
font-weight: bold;
|
||||||
|
}}
|
||||||
|
""")
|
||||||
|
content_layout.addWidget(self.nav_list)
|
||||||
|
|
||||||
|
self.stacked_widget = QStackedWidget()
|
||||||
|
content_layout.addWidget(self.stacked_widget)
|
||||||
|
|
||||||
|
for title_key, content_key in self.steps_data:
|
||||||
|
title = self._tr(title_key, title_key)
|
||||||
|
content = self._tr(content_key, f"Content for {content_key} not found.")
|
||||||
|
|
||||||
|
self.nav_list.addItem(title)
|
||||||
|
|
||||||
|
step_widget = TourStepWidget(title, content, scale=scale)
|
||||||
self.stacked_widget.addWidget(step_widget)
|
self.stacked_widget.addWidget(step_widget)
|
||||||
|
|
||||||
self .setWindowTitle (self ._tr ("help_guide_dialog_title","Kemono Downloader - Feature Guide"))
|
self.nav_list.currentRowChanged.connect(self.stacked_widget.setCurrentIndex)
|
||||||
|
if self.nav_list.count() > 0:
|
||||||
|
self.nav_list.setCurrentRow(0)
|
||||||
|
|
||||||
buttons_layout =QHBoxLayout ()
|
# Footer Layout (Social links and Close button)
|
||||||
buttons_layout .setContentsMargins (15 ,10 ,15 ,15 )
|
footer_layout = QHBoxLayout()
|
||||||
buttons_layout .setSpacing (10 )
|
footer_layout.setContentsMargins(0, 10, 0, 0)
|
||||||
|
|
||||||
|
# Social Media Icons
|
||||||
|
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
|
||||||
|
assets_base_dir = sys._MEIPASS
|
||||||
|
else:
|
||||||
|
assets_base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
|
||||||
|
|
||||||
self .back_button =QPushButton (self ._tr ("tour_dialog_back_button","Back"))
|
github_icon_path = os.path.join(assets_base_dir, "assets", "github.png")
|
||||||
self .back_button .clicked .connect (self ._previous_step )
|
instagram_icon_path = os.path.join(assets_base_dir, "assets", "instagram.png")
|
||||||
self .back_button .setEnabled (False )
|
discord_icon_path = os.path.join(assets_base_dir, "assets", "discord.png")
|
||||||
|
|
||||||
if getattr (sys ,'frozen',False )and hasattr (sys ,'_MEIPASS'):
|
self.github_button = QPushButton(QIcon(github_icon_path), "")
|
||||||
assets_base_dir =sys ._MEIPASS
|
self.instagram_button = QPushButton(QIcon(instagram_icon_path), "")
|
||||||
else :
|
self.discord_button = QPushButton(QIcon(discord_icon_path), "")
|
||||||
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 ),"")
|
|
||||||
|
|
||||||
scale = self.parent_app.scale_factor if hasattr(self.parent_app, 'scale_factor') else 1.0
|
|
||||||
icon_dim = int(24 * scale)
|
icon_dim = int(24 * scale)
|
||||||
icon_size = QSize(icon_dim, icon_dim)
|
icon_size = QSize(icon_dim, icon_dim)
|
||||||
self .github_button .setIconSize (icon_size )
|
|
||||||
self .instagram_button .setIconSize (icon_size )
|
for button, tooltip_key, url in [
|
||||||
self .Discord_button .setIconSize (icon_size )
|
(self.github_button, "help_guide_github_tooltip", "https://github.com/Yuvi9587"),
|
||||||
|
(self.instagram_button, "help_guide_instagram_tooltip", "https://www.instagram.com/uvi.arts/"),
|
||||||
|
(self.discord_button, "help_guide_discord_tooltip", "https://discord.gg/BqP64XTdJN")
|
||||||
|
]:
|
||||||
|
button.setIconSize(icon_size)
|
||||||
|
button.setToolTip(self._tr(tooltip_key))
|
||||||
|
button.setFixedSize(icon_size.width() + 8, icon_size.height() + 8)
|
||||||
|
button.setStyleSheet("background-color: transparent; border: none;")
|
||||||
|
button.clicked.connect(lambda _, u=url: QDesktopServices.openUrl(QUrl(u)))
|
||||||
|
footer_layout.addWidget(button)
|
||||||
|
|
||||||
self .next_button =QPushButton (self ._tr ("tour_dialog_next_button","Next"))
|
footer_layout.addStretch(1)
|
||||||
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)"))
|
|
||||||
|
|
||||||
|
self.finish_button = QPushButton(self._tr("tour_dialog_finish_button", "Finish"))
|
||||||
|
self.finish_button.clicked.connect(self.accept)
|
||||||
|
footer_layout.addWidget(self.finish_button)
|
||||||
|
|
||||||
social_layout =QHBoxLayout ()
|
main_layout.addLayout(footer_layout)
|
||||||
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"))
|
|
||||||
Reference in New Issue
Block a user