Lemontropia-Suite/ui/setup_wizard.py

636 lines
20 KiB
Python

"""
Lemontropia Suite - Setup Wizard
First-run wizard for configuring the application.
"""
import sys
from pathlib import Path
from typing import Optional
from PyQt6.QtWidgets import (
QWizard, QWizardPage, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QFileDialog,
QComboBox, QCheckBox, QProgressBar, QGroupBox,
QFormLayout, QTextEdit, QMessageBox, QWidget
)
from PyQt6.QtCore import Qt, QSettings, QThread, pyqtSignal
from PyQt6.QtGui import QFont, QPixmap
class GearDownloadWorker(QThread):
"""Background worker for downloading gear database."""
progress = pyqtSignal(int)
finished_signal = pyqtSignal(bool, str)
def __init__(self, parent=None):
super().__init__(parent)
self._cancelled = False
def run(self):
"""Download gear database in background."""
try:
# Simulate download progress
import time
for i in range(0, 101, 10):
if self._cancelled:
self.finished_signal.emit(False, "Download cancelled")
return
self.progress.emit(i)
time.sleep(0.2)
# TODO: Implement actual gear database download
# For now, just create a placeholder
self.finished_signal.emit(True, "Gear database downloaded successfully")
except Exception as e:
self.finished_signal.emit(False, str(e))
def cancel(self):
self._cancelled = True
class WelcomePage(QWizardPage):
"""Welcome page of the setup wizard."""
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("Welcome to Lemontropia Suite")
self.setSubTitle("Let's get you set up for tracking your Entropia Universe sessions.")
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
# Welcome text
welcome_label = QLabel(
"<h2>Welcome!</h2>"
"<p>Lemontropia Suite helps you track your hunting, mining, and crafting sessions "
"in Entropia Universe. This wizard will guide you through the initial setup.</p>"
"<p>You'll need:</p>"
"<ul>"
"<li>Your avatar name (for tracking globals)</li>"
"<li>Path to your Entropia Universe chat log file</li>"
"<li>Preferred default activity type</li>"
"</ul>"
)
welcome_label.setWordWrap(True)
layout.addWidget(welcome_label)
layout.addStretch()
# Note about re-running wizard
note_label = QLabel(
"<i>You can run this wizard again anytime from the Settings menu.</i>"
)
note_label.setStyleSheet("color: #888;")
layout.addWidget(note_label)
class AvatarNamePage(QWizardPage):
"""Page for setting the avatar name."""
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("Avatar Name")
self.setSubTitle("Enter your avatar name exactly as it appears in Entropia Universe.")
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
# Explanation
info_label = QLabel(
"Your avatar name is used to identify your globals and HoFs in the chat log. "
"Make sure to enter it exactly as it appears in-game, including capitalization."
)
info_label.setWordWrap(True)
layout.addWidget(info_label)
layout.addSpacing(20)
# Name input
form_layout = QFormLayout()
self.name_input = QLineEdit()
self.name_input.setPlaceholderText("e.g., John Doe Mega")
self.name_input.textChanged.connect(self.completeChanged)
form_layout.addRow("Avatar Name:", self.name_input)
layout.addLayout(form_layout)
layout.addSpacing(10)
# Example
example_label = QLabel(
"<i>Example: If your avatar is named 'Roberth Noname Rajala', "
"enter it exactly like that.</i>"
)
example_label.setStyleSheet("color: #888;")
layout.addWidget(example_label)
layout.addStretch()
def isComplete(self) -> bool:
return len(self.name_input.text().strip()) > 0
def get_avatar_name(self) -> str:
return self.name_input.text().strip()
class LogPathPage(QWizardPage):
"""Page for configuring the log file path."""
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("Chat Log Path")
self.setSubTitle("Configure the path to your Entropia Universe chat log file.")
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
# Explanation
info_label = QLabel(
"Lemontropia Suite reads your chat log to track events like loot, globals, "
"damage dealt/taken, and healing. Please select your chat.log file."
)
info_label.setWordWrap(True)
layout.addWidget(info_label)
layout.addSpacing(20)
# Default path info
default_path = self._get_default_log_path()
default_label = QLabel(f"<b>Default location:</b><br>{default_path}")
default_label.setStyleSheet("color: #4caf50;")
default_label.setWordWrap(True)
layout.addWidget(default_label)
layout.addSpacing(10)
# Path selection
path_layout = QHBoxLayout()
self.path_input = QLineEdit()
self.path_input.setText(default_path)
self.path_input.textChanged.connect(self.completeChanged)
path_layout.addWidget(self.path_input)
browse_btn = QPushButton("Browse...")
browse_btn.clicked.connect(self.on_browse)
path_layout.addWidget(browse_btn)
layout.addLayout(path_layout)
layout.addSpacing(10)
# Auto-detect checkbox
self.auto_detect_check = QCheckBox("Automatically detect log path on startup")
self.auto_detect_check.setChecked(True)
layout.addWidget(self.auto_detect_check)
layout.addStretch()
# Help text
help_label = QLabel(
"<i>Tip: If you can't find your chat.log, make sure you've run Entropia Universe "
"at least once and enabled chat logging in-game.</i>"
)
help_label.setStyleSheet("color: #888;")
layout.addWidget(help_label)
def _get_default_log_path(self) -> str:
"""Get the default chat log path based on OS."""
# Default paths for different scenarios
import os
# Check environment variable first
env_path = os.getenv('EU_CHAT_LOG_PATH', '')
if env_path:
return env_path
# Windows default path
appdata = os.getenv('LOCALAPPDATA', '')
if appdata:
return str(Path(appdata) / "Entropia Universe" / "chat.log")
# Fallback
return str(Path.home() / "Entropia Universe" / "chat.log")
def on_browse(self):
"""Open file browser to select chat log."""
file_path, _ = QFileDialog.getOpenFileName(
self,
"Select Chat Log File",
str(Path.home()),
"Log Files (*.log);;All Files (*.*)"
)
if file_path:
self.path_input.setText(file_path)
def isComplete(self) -> bool:
return len(self.path_input.text().strip()) > 0
def get_log_path(self) -> str:
return self.path_input.text().strip()
def get_auto_detect(self) -> bool:
return self.auto_detect_check.isChecked()
class ActivityTypePage(QWizardPage):
"""Page for selecting default activity type."""
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("Default Activity Type")
self.setSubTitle("Choose your preferred default activity type.")
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
# Explanation
info_label = QLabel(
"Select the activity type you do most often. This will be the default "
"when starting new sessions. You can always change this later."
)
info_label.setWordWrap(True)
layout.addWidget(info_label)
layout.addSpacing(20)
# Activity type selection
form_layout = QFormLayout()
self.activity_combo = QComboBox()
self.activity_combo.addItem("🎯 Hunting", "hunting")
self.activity_combo.addItem("⛏️ Mining", "mining")
self.activity_combo.addItem("⚒️ Crafting", "crafting")
form_layout.addRow("Default Activity:", self.activity_combo)
layout.addLayout(form_layout)
layout.addSpacing(20)
# Activity descriptions
descriptions = QGroupBox("Activity Types")
desc_layout = QVBoxLayout(descriptions)
hunting_desc = QLabel(
"<b>🎯 Hunting</b><br>"
"Track weapon decay, armor decay, healing costs, loot, globals, and HoFs."
)
hunting_desc.setWordWrap(True)
desc_layout.addWidget(hunting_desc)
mining_desc = QLabel(
"<b>⛏️ Mining</b><br>"
"Track finder decay, extractor decay, claim values, and mining runs."
)
mining_desc.setWordWrap(True)
desc_layout.addWidget(mining_desc)
crafting_desc = QLabel(
"<b>⚒️ Crafting</b><br>"
"Track blueprint runs, material costs, success rates, and near-successes."
)
crafting_desc.setWordWrap(True)
desc_layout.addWidget(crafting_desc)
layout.addWidget(descriptions)
layout.addStretch()
def get_activity_type(self) -> str:
return self.activity_combo.currentData()
class GearDatabasePage(QWizardPage):
"""Page for downloading initial gear database."""
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("Gear Database (Optional)")
self.setSubTitle("Download initial gear database for cost tracking.")
self.setup_ui()
self.worker = None
def setup_ui(self):
layout = QVBoxLayout(self)
# Explanation
info_label = QLabel(
"You can download an initial gear database with common weapons, armor, and tools. "
"This makes it easier to set up your loadouts for cost tracking.<br><br>"
"This step is optional - you can always add gear manually later."
)
info_label.setWordWrap(True)
layout.addWidget(info_label)
layout.addSpacing(20)
# Download option
self.download_check = QCheckBox("Download initial gear database")
self.download_check.setChecked(True)
layout.addWidget(self.download_check)
layout.addSpacing(10)
# Progress bar (hidden initially)
self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False)
layout.addWidget(self.progress_bar)
# Status label
self.status_label = QLabel("")
self.status_label.setStyleSheet("color: #4caf50;")
layout.addWidget(self.status_label)
layout.addStretch()
# Note
note_label = QLabel(
"<i>Note: The gear database requires internet connection. "
"If you skip this now, you can download it later from the Settings menu.</i>"
)
note_label.setStyleSheet("color: #888;")
note_label.setWordWrap(True)
layout.addWidget(note_label)
def initializePage(self):
"""Called when page is shown."""
self.progress_bar.setVisible(False)
self.status_label.setText("")
def download_gear_database(self):
"""Start downloading gear database."""
if not self.download_check.isChecked():
return True
self.progress_bar.setVisible(True)
self.progress_bar.setValue(0)
self.status_label.setText("Downloading gear database...")
# Start worker thread
self.worker = GearDownloadWorker(self)
self.worker.progress.connect(self.progress_bar.setValue)
self.worker.finished_signal.connect(self.on_download_finished)
self.worker.start()
# Wait for completion
self.worker.wait()
return self._download_success
def on_download_finished(self, success: bool, message: str):
"""Handle download completion."""
self._download_success = success
if success:
self.status_label.setText(f"{message}")
self.status_label.setStyleSheet("color: #4caf50;")
else:
self.status_label.setText(f"{message}")
self.status_label.setStyleSheet("color: #f44336;")
def get_download_gear(self) -> bool:
return self.download_check.isChecked()
class SummaryPage(QWizardPage):
"""Final page showing summary of configuration."""
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("Setup Complete")
self.setSubTitle("Review your configuration before finishing.")
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
# Success message
success_label = QLabel("<h2>✓ Setup Complete!</h2>")
success_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(success_label)
layout.addSpacing(20)
# Summary text area
self.summary_text = QTextEdit()
self.summary_text.setReadOnly(True)
self.summary_text.setMaximumHeight(200)
layout.addWidget(self.summary_text)
layout.addSpacing(20)
# Next steps
next_label = QLabel(
"<b>Next Steps:</b><br>"
"1. Create or select a loadout for your gear<br>"
"2. Start a new session to begin tracking<br>"
"3. Check the HUD overlay for real-time stats<br><br>"
"Click <b>Finish</b> to start using Lemontropia Suite!"
)
next_label.setWordWrap(True)
layout.addWidget(next_label)
layout.addStretch()
def initializePage(self):
"""Update summary when page is shown."""
wizard = self.wizard()
# Get values from previous pages
avatar_page = wizard.page(1) # AvatarNamePage
log_page = wizard.page(2) # LogPathPage
activity_page = wizard.page(3) # ActivityTypePage
gear_page = wizard.page(4) # GearDatabasePage
summary = f"""
<b>Configuration Summary:</b><br>
<br>
<b>Avatar Name:</b> {avatar_page.get_avatar_name()}<br>
<b>Chat Log Path:</b> {log_page.get_log_path()}<br>
<b>Auto-detect Log:</b> {'Yes' if log_page.get_auto_detect() else 'No'}<br>
<b>Default Activity:</b> {activity_page.get_activity_type().capitalize()}<br>
<b>Download Gear DB:</b> {'Yes' if gear_page.get_download_gear() else 'No'}<br>
"""
self.summary_text.setHtml(summary)
class SetupWizard(QWizard):
"""
First-run setup wizard for Lemontropia Suite.
Guides users through:
- Setting avatar name
- Configuring log file path
- Choosing default activity type
- Downloading initial gear database (optional)
"""
def __init__(self, parent=None, first_run: bool = True):
super().__init__(parent)
self.setWindowTitle("Lemontropia Suite - Setup Wizard")
self.setMinimumSize(600, 500)
# Set wizard style
self.setWizardStyle(QWizard.WizardStyle.ModernStyle)
# Add pages
self.addPage(WelcomePage())
self.addPage(AvatarNamePage())
self.addPage(LogPathPage())
self.addPage(ActivityTypePage())
self.addPage(GearDatabasePage())
self.addPage(SummaryPage())
# Set button text
self.setButtonText(QWizard.WizardButton.FinishButton, "Start Using Lemontropia Suite")
self._first_run = first_run
def accept(self):
"""Handle wizard completion."""
# Collect all settings
settings = {
'avatar_name': self.field("avatar_name") or self.page(1).get_avatar_name(),
'log_path': self.page(2).get_log_path(),
'auto_detect_log': self.page(2).get_auto_detect(),
'default_activity': self.page(3).get_activity_type(),
'gear_db_downloaded': self.page(4).get_download_gear(),
}
# Save to QSettings
qsettings = QSettings("Lemontropia", "Suite")
qsettings.setValue("player/name", settings['avatar_name'])
qsettings.setValue("log/path", settings['log_path'])
qsettings.setValue("log/auto_detect", settings['auto_detect_log'])
qsettings.setValue("activity/default", settings['default_activity'])
qsettings.setValue("setup/first_run_complete", True)
qsettings.setValue("setup/gear_db_downloaded", settings['gear_db_downloaded'])
qsettings.sync()
# Store settings for access after wizard closes
self._settings = settings
super().accept()
def get_settings(self) -> dict:
"""Get the configured settings after wizard completion."""
return getattr(self, '_settings', {})
@staticmethod
def is_first_run() -> bool:
"""Check if this is the first run of the application."""
settings = QSettings("Lemontropia", "Suite")
return not settings.value("setup/first_run_complete", False, type=bool)
@staticmethod
def reset_first_run():
"""Reset the first-run flag (for testing)."""
settings = QSettings("Lemontropia", "Suite")
settings.setValue("setup/first_run_complete", False)
settings.sync()
def main():
"""Test entry point for the setup wizard."""
from PyQt6.QtWidgets import QApplication
app = QApplication(sys.argv)
# Apply dark theme
dark_stylesheet = """
QWizard {
background-color: #1e1e1e;
color: #e0e0e0;
}
QWizardPage {
background-color: #1e1e1e;
color: #e0e0e0;
}
QLabel {
color: #e0e0e0;
}
QLineEdit {
background-color: #252525;
border: 1px solid #444;
border-radius: 4px;
padding: 6px;
color: #e0e0e0;
}
QPushButton {
background-color: #2d2d2d;
border: 1px solid #444;
border-radius: 4px;
padding: 8px 16px;
color: #e0e0e0;
}
QPushButton:hover {
background-color: #3d3d3d;
}
QComboBox {
background-color: #252525;
border: 1px solid #444;
border-radius: 4px;
padding: 6px;
color: #e0e0e0;
}
QGroupBox {
font-weight: bold;
border: 1px solid #444;
border-radius: 6px;
margin-top: 10px;
padding-top: 10px;
padding: 10px;
color: #e0e0e0;
}
QTextEdit {
background-color: #252525;
border: 1px solid #444;
border-radius: 4px;
padding: 8px;
color: #e0e0e0;
}
QCheckBox {
color: #e0e0e0;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
}
QProgressBar {
border: 1px solid #444;
border-radius: 4px;
text-align: center;
color: #e0e0e0;
}
QProgressBar::chunk {
background-color: #4caf50;
border-radius: 3px;
}
"""
app.setStyleSheet(dark_stylesheet)
wizard = SetupWizard()
if wizard.exec() == QWizard.DialogCode.Accepted:
print("Settings saved:", wizard.get_settings())
else:
print("Wizard cancelled")
sys.exit(0)
if __name__ == '__main__':
main()