This commit is contained in:
Yuvi9587
2025-05-10 11:07:27 +05:30
parent 866a5a90de
commit aec44f1782
4 changed files with 2299 additions and 1920 deletions

206
Known.txt
View File

@@ -1,9 +1,197 @@
Back to Hell
Fade
Jett
Psylocke
Viper
clove
neon
reyna
sage
Ada
Aeris
Alina
Amara
Anya
Aria
Artemis
Ashe
Astrid
Asuka
Athena
Azura
Belladonna
Bianca
C.C.
Calla
Camilla
Cassia
Celeste
Chika
Clara
Delilah
Dia
Diana
Eira
Elara
Eli
Elise
Elma
Ember
Erza
Esme
Evelyn
Evie
Fiora
Freya
Gasai
Greta
Hanayo
Hancock
Haruhi
Hatsume
Hawkeye
Hinata
Holo
Homura
Ichigo
Illya
Inara
Ino
Isla
Isolde
Ivy
Jeanne
Jinx
Jiro
Juniper
Juvia
Kaelin
Kagome
Kagura
Kaida
Kairi
Kali
Kana
Kanao
Kanna
Kiera
Kikyo
Kirari
Korra
Kotori
Kurisu
Kushina
Kyoko
Lan Fan
Leona
Levy
Lilith
Liora
Lira
Lisanna
Lucia
Lucoa
Lucy
Luna
Lust
Lyra
Madoka
Maia
Makima
Makise
Makomo
Mami
Mari
Marin
Mary
Mavis
Mayuri
Medusa
Mei
Merlin
Mikasa
Milly
Mina
Mion
Mira
Mirabel
Misato
Mitsuri
Momo
Morgana
Nadia
Nami
Naomi
Nelliel
Nerissa
Neve
Nezuko
Noelle
Nova
Nozomi
Nunnally
Nyx
Ochaco
Odette
Ophelia
Orihime
Orla
Perona
Phoebe
Raven
Rei
Reyna
Rhea
Rika
Rin
Rin Tohsaka
Rinoa
Ritsuko
Riza
Robin
Rosalie
Rowan
Ruby
Rukia
Rumi
Saber
Sable
Sakura
Sakura Matou
Sango
Sansa
Satoko
Sayaka
Scáthach
Selene
Seline
Serena
Shinobu
Shion
Shirley
Sierra
Skye
Sophie
Soraya
Sylvia
Talia
Tamayo
Tamsin
Tashigi
Tatiana
Temari
Thalia
Tifa
Toga
Tohru
Tsunade
Umi
Valeria
Viola
Violet
Vivi
Wendy
Winry
Wynne
Yara
Yazawa
Yoruichi
Yoshiko
Yuki Nagato
Yumeko
Yuna
Yuno
Zara
Zelda
Zero Two

File diff suppressed because it is too large Load Diff

2694
main.py

File diff suppressed because it is too large Load Diff

248
tour.py
View File

@@ -11,22 +11,24 @@ class TourStepWidget(QWidget):
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
layout.setContentsMargins(20, 20, 20, 20)
layout.setSpacing(10) # Adjusted spacing between title and content for bullet points
title_label = QLabel(title_text)
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("font-size: 18px; font-weight: bold; color: #E0E0E0; padding-bottom: 10px;")
# Increased padding-bottom for more space below title
title_label.setStyleSheet("font-size: 18px; font-weight: bold; color: #E0E0E0; padding-bottom: 15px;")
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;")
content_label.setAlignment(Qt.AlignLeft)
content_label.setTextFormat(Qt.RichText)
# Adjusted line-height for bullet point readability
content_label.setStyleSheet("font-size: 11pt; color: #C8C8C8; line-height: 1.8;")
layout.addWidget(title_label)
layout.addWidget(content_label)
layout.addStretch(1)
layout.addStretch(1)
class TourDialog(QDialog):
"""
@@ -34,12 +36,12 @@ class TourDialog(QDialog):
Includes a "Never show again" checkbox.
Uses QSettings to remember this preference.
"""
tour_finished_normally = pyqtSignal()
tour_skipped = pyqtSignal()
tour_finished_normally = pyqtSignal()
tour_skipped = pyqtSignal()
CONFIG_ORGANIZATION_NAME = "KemonoDownloader"
CONFIG_APP_NAME_TOUR = "ApplicationTour"
TOUR_SHOWN_KEY = "neverShowTourAgainV2"
CONFIG_ORGANIZATION_NAME = "KemonoDownloader"
CONFIG_APP_NAME_TOUR = "ApplicationTour"
TOUR_SHOWN_KEY = "neverShowTourAgainV3" # Updated key for new tour content
def __init__(self, parent=None):
super().__init__(parent)
@@ -48,19 +50,20 @@ class TourDialog(QDialog):
self.setWindowTitle("Welcome to Kemono Downloader!")
self.setModal(True)
self.setMinimumSize(520, 450)
# Set fixed square size, smaller than main window
self.setFixedSize(600, 620) # Slightly adjusted for potentially more text
self.setStyleSheet("""
QDialog {
background-color: #2E2E2E;
border: 1px solid #5A5A5A;
}
QLabel {
color: #E0E0E0;
color: #E0E0E0;
}
QCheckBox {
color: #C0C0C0;
font-size: 10pt;
spacing: 5px;
spacing: 5px;
}
QCheckBox::indicator {
width: 13px;
@@ -83,117 +86,129 @@ class TourDialog(QDialog):
}
""")
self._init_ui()
self._center_on_screen() # Call method to center the dialog
self._center_on_screen()
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.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
self.stacked_widget = QStackedWidget()
main_layout.addWidget(self.stacked_widget, 1)
main_layout.addWidget(self.stacked_widget, 1)
# --- Define Tour Steps ---
# --- Define Tour Steps with Updated Content ---
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."
"Hello! This quick tour will walk you through the main features of the Kemono Downloader."
"<ul>"
"<li>Our goal is to help you easily download content from Kemono and Coomer.</li>"
"<li>Use the <b>Next</b> and <b>Back</b> buttons to navigate.</li>"
"<li>Click <b>Skip Tour</b> to close this guide at any time.</li>"
"<li>Check <b>'Never show this tour again'</b> if you don't want to see this on future startups.</li>"
"</ul>"
)
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>"
"Let's start with the basics for downloading:"
"<ul>"
"<li><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>"
"or a specific post (e.g., <i>.../post/98765</i>).</li><br>"
"<li><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."
"This is required unless you are using 'Only Links' mode.</li><br>"
"<li><b>📄 Page Range (Creator URLs only):</b><br>"
" If downloading from a creator's page, you can specify a range of pages (e.g., pages 2 to 5). "
"Leave blank for all pages. This is disabled for single post URLs or when <b>Manga/Comic Mode</b> is active.</li>"
"</ul>"
)
self.step2 = TourStepWidget("① Getting Started: URLs & Location", step2_content)
self.step2 = TourStepWidget("① Getting Started", 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."
"Refine what you download with these filters:"
"<ul>"
"<li><b>🎯 Filter by Character(s):</b><br>"
" Enter character names, comma-separated (e.g., <i>Tifa, Aerith</i>). "
" <ul><li>In <b>Normal Mode</b>, this filters individual files by matching their filenames.</li>"
" <li>In <b>Manga/Comic Mode</b>, this filters entire posts by matching the post title. Useful for targeting specific series.</li>"
" <li>Also helps in folder naming if 'Separate Folders' is enabled.</li></ul></li><br>"
"<li><b>🚫 Skip with Words:</b><br>"
" Enter words, comma-separated (e.g., <i>WIP, sketch, preview</i>). "
" The <b>Scope</b> button (next to this input) cycles how this filter applies:"
" <ul><li><i>Scope: Files:</i> Skips files if their names contain any of these words.</li>"
" <li><i>Scope: Posts:</i> Skips entire posts if their titles contain any of these words.</li>"
" <li><i>Scope: Both:</i> Applies both file and post title skipping.</li></ul></li><br>"
"<li><b>Filter Files (Radio Buttons):</b> Choose what to download:"
" <ul>"
" <li><i>All:</i> Downloads all file types found.</li>"
" <li><i>Images/GIFs:</i> Only common image formats and GIFs.</li>"
" <li><i>Videos:</i> Only common video formats.</li>"
" <li><b><i>📦 Only Archives:</i></b> Exclusively downloads <b>.zip</b> and <b>.rar</b> files. When selected, 'Skip .zip' and 'Skip .rar' checkboxes are automatically disabled and unchecked.</li>"
" <li><i>🔗 Only Links:</i> Extracts and displays external links from post descriptions instead of downloading files.</li>"
" </ul></li>"
"</ul>"
)
self.step3 = TourStepWidget("② Filtering Your Downloads", step3_content)
self.step3 = TourStepWidget("② Filtering 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."
"More options to customize your downloads:"
"<ul>"
"<li><b>Skip .zip / Skip .rar:</b> Check these to avoid downloading these archive file types. "
" <i>(Note: These are disabled and ignored if '📦 Only Archives' mode is selected).</i></li><br>"
"<li><b>Download Thumbnails Only:</b> Downloads small preview images instead of full-sized files (if available).</li><br>"
"<li><b>Compress Large Images:</b> If the 'Pillow' library is installed, images larger than 1.5MB will be converted to WebP format if the WebP version is significantly smaller.</li><br>"
"<li><b>🗄️ Custom Folder Name (Single Post Only):</b><br>"
" If you are downloading a single specific post URL AND 'Separate Folders by Name/Title' is enabled, "
"you can enter a custom name here for that post's download folder.</li>"
"</ul>"
)
self.step4 = TourStepWidget("③ Fine-Tuning: Archives & Images", step4_content)
self.step4 = TourStepWidget("③ Fine-Tuning Downloads", 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."
"Organize your downloads and manage performance:"
"<ul>"
"<li><b>⚙️ Separate Folders by Name/Title:</b> Creates subfolders based on the 'Filter by Character(s)' input or post titles (can use the 'Known Shows/Characters' list as a fallback for folder names).</li><br>"
"<li><b>Subfolder per Post:</b> If 'Separate Folders' is on, this creates an additional subfolder for <i>each individual post</i> inside the main character/title folder.</li><br>"
"<li><b>🚀 Use Multithreading (Threads):</b> Enables faster downloads for creator pages by processing multiple posts or files concurrently. The number of threads can be adjusted. Single post URLs are processed using a single thread for post data but can use multiple threads for file downloads within that post.</li><br>"
"<li><b>📖 Manga/Comic Mode (Creator URLs only):</b> Tailored for sequential content."
" <ul>"
" <li>Downloads posts from <b>oldest to newest</b>.</li>"
" <li>The 'Page Range' input is disabled as all posts are fetched.</li>"
" <li>A <b>filename style toggle button</b> (e.g., 'Name: Post Title' or 'Name: Original File') appears in the top-right of the log area when this mode is active for a creator feed. Click it to change naming:"
" <ul>"
" <li><b><i>Name: Post Title (Default):</i></b> The first file in a post is named after the post's title (e.g., <i>MyMangaChapter1.jpg</i>). Subsequent files in the <i>same post</i> (if any) will retain their original filenames.</li>"
" <li><b><i>Name: Original File:</i></b> All files will attempt to keep their original filenames as provided by the site (e.g., <i>001.jpg, page_02.png</i>). You'll see a recommendation to use 'Post Title' style if you choose this.</li>"
" </ul>"
" </li>"
" <li>For best results with 'Name: Post Title' style, use the 'Filter by Character(s)' field with the manga/series title.</li>"
" </ul></li><br>"
"<li><b>🎭 Known Shows/Characters:</b> Add names here (e.g., <i>Game Title, Series Name, Character Full Name</i>). These are used for automatic folder creation when 'Separate Folders' is on and no specific 'Filter by Character(s)' is provided for a post.</li>"
"</ul>"
)
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>."
"Monitoring and Controls:"
"<ul>"
"<li><b>📜 Progress Log / Extracted Links Log:</b> Shows detailed download messages. If '🔗 Only Links' mode is active, this area displays the extracted links.</li><br>"
"<li><b>Show External Links in Log:</b> If checked, a secondary log panel appears below the main log to display any external links found in post descriptions. <i>(This is disabled if '🔗 Only Links' or '📦 Only Archives' mode is active).</i></li><br>"
"<li><b>Log Verbosity (Show Basic/Full Log):</b> Toggles the main log between showing all messages (Full) or only key summaries, errors, and warnings (Basic).</li><br>"
"<li><b>🔄 Reset:</b> Clears all input fields, logs, and resets temporary settings to their defaults. Can only be used when no download is active.</li><br>"
"<li><b>⬇️ Start Download / ❌ Cancel:</b> These buttons initiate or stop the current download/extraction process.</li>"
"</ul>"
"<br>You're all set! Click <b>'Finish'</b> to close the tour and start using the downloader."
)
self.step6 = TourStepWidget("⑤ Logs & Final Controls", step6_content)
@@ -202,12 +217,12 @@ class TourDialog(QDialog):
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 = QVBoxLayout()
bottom_controls_layout.setContentsMargins(15, 10, 15, 15) # Adjusted margins
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)
bottom_controls_layout.addWidget(self.never_show_again_checkbox, 0, Qt.AlignLeft)
buttons_layout = QHBoxLayout()
buttons_layout.setSpacing(10)
@@ -227,29 +242,28 @@ class TourDialog(QDialog):
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)
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).")
self.settings.sync()
# 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:
else:
self._handle_exit_actions()
self.tour_finished_normally.emit()
self.accept()
self.accept()
self._update_button_states()
def _previous_step(self):
@@ -261,7 +275,7 @@ class TourDialog(QDialog):
def _skip_tour_action(self):
self._handle_exit_actions()
self.tour_skipped.emit()
self.reject()
self.reject()
def _update_button_states(self):
if self.current_step == len(self.tour_steps) - 1:
@@ -272,45 +286,39 @@ class TourDialog(QDialog):
@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}")
never_show_again = settings.value(TourDialog.TOUR_SHOWN_KEY, False, type=bool)
if never_show_again:
print("[Tour] Skipping tour because 'Never show again' was previously selected.")
return QDialog.Rejected
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})")
tour_dialog = TourDialog(parent_app_window)
result = tour_dialog.exec_()
return result
except Exception as e:
print(f"[Tour] CRITICAL ERROR in run_tour_if_needed: {e}")
traceback.print_exc()
return QDialog.Rejected
traceback.print_exc()
return QDialog.Rejected
if __name__ == '__main__':
app = QApplication(sys.argv)
# --- For testing: force the tour to show by resetting the flag ---
# 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)}")
# test_settings.setValue(TourDialog.TOUR_SHOWN_KEY, False) # Set to False to force tour
# test_settings.sync()
# --- End testing block ---
print("[Tour Test] Running tour standalone...")
result = TourDialog.run_tour_if_needed(None)
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)}")