This commit is contained in:
Yuvi9587 2025-05-09 19:03:01 +05:30
parent 929051d46c
commit 866a5a90de
5 changed files with 1624 additions and 826 deletions

View File

@ -0,0 +1,9 @@
Back to Hell
Fade
Jett
Psylocke
Viper
clove
neon
reyna
sage

File diff suppressed because it is too large Load Diff

845
main.py

File diff suppressed because it is too large Load Diff

View File

@ -147,3 +147,4 @@ Contributions are welcome! Open an issue or submit a pull request if you have im
---
Jett, Neon, Reyna, Clove, Viper, Sage

317
tour.py Normal file
View File

@ -0,0 +1,317 @@
import sys
import traceback # Added for enhanced error reporting
from PyQt5.QtWidgets import (
QApplication, QDialog, QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout,
QStackedWidget, QSpacerItem, QSizePolicy, QCheckBox, QDesktopWidget
)
from PyQt5.QtCore import Qt, QSettings, pyqtSignal
class TourStepWidget(QWidget):
"""A single step/page in the tour."""
def __init__(self, title_text, content_text, parent=None):
super().__init__(parent)
layout = QVBoxLayout(self)
layout.setContentsMargins(20, 20, 20, 20) # Padding around content
layout.setSpacing(15) # Spacing between title and content
title_label = QLabel(title_text)
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("font-size: 18px; font-weight: bold; color: #E0E0E0; padding-bottom: 10px;")
content_label = QLabel(content_text)
content_label.setWordWrap(True)
content_label.setAlignment(Qt.AlignLeft) # Align text to the left for readability
content_label.setTextFormat(Qt.RichText)
content_label.setStyleSheet("font-size: 12px; color: #C8C8C8; line-height: 1.6;")
layout.addWidget(title_label)
layout.addWidget(content_label)
layout.addStretch(1)
class TourDialog(QDialog):
"""
A dialog that shows a multi-page tour to the user.
Includes a "Never show again" checkbox.
Uses QSettings to remember this preference.
"""
tour_finished_normally = pyqtSignal()
tour_skipped = pyqtSignal()
CONFIG_ORGANIZATION_NAME = "KemonoDownloader"
CONFIG_APP_NAME_TOUR = "ApplicationTour"
TOUR_SHOWN_KEY = "neverShowTourAgainV2"
def __init__(self, parent=None):
super().__init__(parent)
self.settings = QSettings(self.CONFIG_ORGANIZATION_NAME, self.CONFIG_APP_NAME_TOUR)
self.current_step = 0
self.setWindowTitle("Welcome to Kemono Downloader!")
self.setModal(True)
self.setMinimumSize(520, 450)
self.setStyleSheet("""
QDialog {
background-color: #2E2E2E;
border: 1px solid #5A5A5A;
}
QLabel {
color: #E0E0E0;
}
QCheckBox {
color: #C0C0C0;
font-size: 10pt;
spacing: 5px;
}
QCheckBox::indicator {
width: 13px;
height: 13px;
}
QPushButton {
background-color: #555;
color: #F0F0F0;
border: 1px solid #6A6A6A;
padding: 8px 15px;
border-radius: 4px;
min-height: 25px;
font-size: 11pt;
}
QPushButton:hover {
background-color: #656565;
}
QPushButton:pressed {
background-color: #4A4A4A;
}
""")
self._init_ui()
self._center_on_screen() # Call method to center the dialog
def _center_on_screen(self):
"""Centers the dialog on the screen."""
try:
# Get the geometry of the screen
screen_geometry = QDesktopWidget().screenGeometry()
# Get the geometry of the dialog
dialog_geometry = self.frameGeometry()
# Calculate the center point for the dialog
center_point = screen_geometry.center()
dialog_geometry.moveCenter(center_point)
# Move the top-left point of the dialog to the calculated position
self.move(dialog_geometry.topLeft())
print(f"[Tour] Dialog centered at: {dialog_geometry.topLeft()}")
except Exception as e:
print(f"[Tour] Error centering dialog: {e}")
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)
# --- Define Tour Steps ---
step1_content = (
"Hello! This quick tour will walk you through the main features of the Kemono Downloader. "
"Our goal is to help you easily download content from Kemono and Coomer.<br><br>"
" • Use the <b>Next</b> and <b>Back</b> buttons to navigate.<br>"
" • Click <b>Skip Tour</b> to close this guide at any time.<br>"
" • Check <b>'Never show this tour again'</b> if you don't want to see this on future startups."
)
self.step1 = TourStepWidget("👋 Welcome!", step1_content)
step2_content = (
"Let's start with the basics for downloading:<br><br>"
" • <b>🔗 Kemono Creator/Post URL:</b><br>"
" Paste the full web address (URL) of a creator's page (e.g., <i>https://kemono.su/patreon/user/12345</i>) "
"or a specific post (e.g., <i>.../post/98765</i>). This tells the downloader where to look for content.<br><br>"
" • <b>📁 Download Location:</b><br>"
" Click 'Browse...' to choose a folder on your computer where all downloaded files will be saved. "
"It's important to select this before starting.<br><br>"
" • <b>📄 Page Range (for Creator URLs only):</b><br>"
" If you're downloading from a creator's page, you can specify a range of pages to download (e.g., pages 2 to 5). "
"Leave blank to try and download all pages. This is disabled if you enter a single post URL or use Manga Mode."
)
self.step2 = TourStepWidget("① Getting Started: URLs & Location", step2_content)
step3_content = (
"Refine what you download with these filters:<br><br>"
" • <b>🎯 Filter by Character(s):</b><br>"
" Enter character names, separated by commas (e.g., <i>Tifa, Aerith</i>). "
"If 'Separate Folders by Name/Title' is on, this helps sort files into folders. "
"In Manga Mode, this filters posts by matching the post title. In Normal Mode, it filters individual files by their filename.<br><br>"
" • <b>🚫 Skip Posts/Files with Words:</b><br>"
" Enter words, separated by commas (e.g., <i>WIP, sketch</i>). "
"Files or posts containing these words in their name (or post title if 'Separate Folders' is off and not Manga Mode) will be skipped.<br><br>"
" • <b>Filter Files (Radio Buttons):</b><br>"
" - <i>All:</i> Download all file types.<br>"
" - <i>Images/GIFs:</i> Only download common image formats and GIFs.<br>"
" - <i>Videos:</i> Only download common video formats.<br>"
" - <i>🔗 Only Links:</i> Don't download files; instead, extract and display any external links found in post descriptions (like Mega, Google Drive links). The log area will show these links."
)
self.step3 = TourStepWidget("② Filtering Your Downloads", step3_content)
step4_content = (
"More options to customize your downloads:<br><br>"
" • <b>Skip .zip / Skip .rar:</b><br>"
" Check these to avoid downloading .zip or .rar archive files.<br><br>"
" • <b>Download Thumbnails Only:</b><br>"
" If checked, only downloads the small preview images (thumbnails) instead of full-sized files. Useful for a quick overview.<br><br>"
" • <b>Compress Large Images:</b><br>"
" If you have the 'Pillow' library installed, this will try to convert very large images (over 1.5MB) to a smaller WebP format to save space. If WebP isn't smaller, the original is kept.<br><br>"
" • <b>🗄️ Custom Folder Name (Single Post Only):</b><br>"
" When downloading a single post URL and using subfolders, you can type a specific name here for that post's folder."
)
self.step4 = TourStepWidget("③ Fine-Tuning: Archives & Images", step4_content)
step5_content = (
"Organize your downloads and manage performance:<br><br>"
" • <b>⚙️ Separate Folders by Name/Title:</b><br>"
" If checked, the downloader tries to create subfolders based on character names (if you used the Character Filter) or by deriving a name from the post title using your 'Known Shows/Characters' list.<br><br>"
" • <b>Subfolder per Post:</b><br>"
" Only active if 'Separate Folders' is on. Creates an additional subfolder for <i>each individual post</i> inside the character/title folder, named like 'PostID_PostTitle'.<br><br>"
" • <b>🚀 Use Multithreading (Threads):</b><br>"
" For creator pages, this can speed up downloads by processing multiple posts at once. For single post URLs, it always uses one thread. Be cautious with very high thread counts.<br><br>"
" • <b>📖 Manga/Comic Mode (Creator URLs only):</b><br>"
" Downloads posts from oldest to newest. It also renames files based on the post title and an extracted or generated sequence number (e.g., <i>MangaTitle - 01.jpg, MangaTitle - 02.jpg</i>). Best used with a character filter matching the series title for correct naming.<br><br>"
" • <b>🎭 Known Shows/Characters:</b><br>"
" Add names here (e.g., a game title, a character's full name). When 'Separate Folders' is on and no character filter is used, the app looks for these known names in post titles to create appropriate folders."
)
self.step5 = TourStepWidget("④ Organization & Performance", step5_content)
step6_content = (
"Monitoring and Controls:<br><br>"
" • <b>📜 Progress Log / Extracted Links Log:</b><br>"
" This area shows detailed messages about the download process or lists extracted links if 'Only Links' mode is active.<br><br>"
" • <b>Show External Links in Log (Checkbox):</b><br>"
" If checked (and not in 'Only Links' mode), a second log panel appears to show external links found in post descriptions.<br><br>"
" • <b>Show Basic/Full Log (Button):</b><br>"
" Toggles the main log between showing all messages (Full) or only important ones (Basic).<br><br>"
" • <b>🔄 Reset (Button):</b><br>"
" Clears all input fields and logs to their default state. Only works when no download is active.<br><br>"
" • <b>⬇️ Start Download / ❌ Cancel (Buttons):</b><br>"
" Start begins the process. Cancel stops an ongoing download."
"<br><br>You're ready to start downloading! Click <b>'Finish'</b>."
)
self.step6 = TourStepWidget("⑤ Logs & Final Controls", step6_content)
self.tour_steps = [self.step1, self.step2, self.step3, self.step4, self.step5, self.step6]
for step_widget in self.tour_steps:
self.stacked_widget.addWidget(step_widget)
bottom_controls_layout = QVBoxLayout()
bottom_controls_layout.setContentsMargins(15, 10, 15, 15)
bottom_controls_layout.setSpacing(10)
self.never_show_again_checkbox = QCheckBox("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("Skip Tour")
self.skip_button.clicked.connect(self._skip_tour_action)
self.back_button = QPushButton("Back")
self.back_button.clicked.connect(self._previous_step)
self.back_button.setEnabled(False)
self.next_button = QPushButton("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 _handle_exit_actions(self):
if self.never_show_again_checkbox.isChecked():
self.settings.setValue(self.TOUR_SHOWN_KEY, True)
self.settings.sync()
print(f"[Tour] '{self.TOUR_SHOWN_KEY}' setting updated to True.")
else:
print(f"[Tour] '{self.TOUR_SHOWN_KEY}' setting not set to True (checkbox was unchecked on exit).")
def _next_step_action(self):
if self.current_step < len(self.tour_steps) - 1:
self.current_step += 1
self.stacked_widget.setCurrentIndex(self.current_step)
else:
self._handle_exit_actions()
self.tour_finished_normally.emit()
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 _skip_tour_action(self):
self._handle_exit_actions()
self.tour_skipped.emit()
self.reject()
def _update_button_states(self):
if self.current_step == len(self.tour_steps) - 1:
self.next_button.setText("Finish")
else:
self.next_button.setText("Next")
self.back_button.setEnabled(self.current_step > 0)
@staticmethod
def run_tour_if_needed(parent_app_window):
print("[Tour] Attempting to run tour (run_tour_if_needed called)...")
try:
settings = QSettings(TourDialog.CONFIG_ORGANIZATION_NAME, TourDialog.CONFIG_APP_NAME_TOUR)
never_show_again = settings.value(TourDialog.TOUR_SHOWN_KEY, False, type=bool)
print(f"[Tour] Current '{TourDialog.TOUR_SHOWN_KEY}' setting is: {never_show_again}")
if never_show_again:
print("[Tour] Skipping tour because 'Never show again' was previously selected.")
return QDialog.Rejected
print("[Tour] 'Never show again' is False. Proceeding to create and show tour dialog.")
tour_dialog = TourDialog(parent_app_window) # _center_on_screen is called in __init__
print("[Tour] TourDialog instance created successfully.")
result = tour_dialog.exec_()
print(f"[Tour] Tour dialog exec_() finished. Result code: {result} (Accepted={QDialog.Accepted}, Rejected={QDialog.Rejected})")
return result
except Exception as e:
print(f"[Tour] CRITICAL ERROR in run_tour_if_needed: {e}")
traceback.print_exc()
return QDialog.Rejected
if __name__ == '__main__':
app = QApplication(sys.argv)
# print("[Tour Test] Resetting 'Never show again' flag for testing purposes.")
# test_settings = QSettings(TourDialog.CONFIG_ORGANIZATION_NAME, TourDialog.CONFIG_APP_NAME_TOUR)
# print(f"[Tour Test] Before reset, '{TourDialog.TOUR_SHOWN_KEY}' is: {test_settings.value(TourDialog.TOUR_SHOWN_KEY, False, type=bool)}")
# test_settings.setValue(TourDialog.TOUR_SHOWN_KEY, False)
# test_settings.sync()
# print(f"[Tour Test] After reset, '{TourDialog.TOUR_SHOWN_KEY}' is: {test_settings.value(TourDialog.TOUR_SHOWN_KEY, False, type=bool)}")
print("[Tour Test] Running tour standalone...")
result = TourDialog.run_tour_if_needed(None)
if result == QDialog.Accepted:
print("[Tour Test] Tour dialog was accepted (Finished).")
elif result == QDialog.Rejected:
print("[Tour Test] Tour dialog was rejected (Skipped or previously set to 'Never show again').")
final_settings = QSettings(TourDialog.CONFIG_ORGANIZATION_NAME, TourDialog.CONFIG_APP_NAME_TOUR)
print(f"[Tour Test] Final state of '{TourDialog.TOUR_SHOWN_KEY}' in settings: {final_settings.value(TourDialog.TOUR_SHOWN_KEY, False, type=bool)}")
sys.exit()