# --- 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)