diff --git a/.gitignore b/.gitignore index dd7a2dc..eb7f5ff 100644 --- a/.gitignore +++ b/.gitignore @@ -20,9 +20,8 @@ wheels/ *.egg-info/ .installed.cfg *.egg -MANIFEST -# Virtual Environment +# Virtual environments venv/ .venv/ ENV/ @@ -30,32 +29,24 @@ ENV/ # IDE .vscode/ .idea/ -*.swp -*.swo # Logs *.log -# Environment variables +# Environment .env .env.local -*.env.* +.env.* # Coverage .coverage coverage/ htmlcov/ -# MyPy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pytest -.pytest_cache/ - # Temporary files *.tmp +*.swp *~ -``` +.DS_Store +Thumbs.db ``` \ No newline at end of file diff --git a/nela_launcher/DOCUMENTATION.md b/nela_launcher/DOCUMENTATION.md new file mode 100644 index 0000000..e2a7b40 --- /dev/null +++ b/nela_launcher/DOCUMENTATION.md @@ -0,0 +1,488 @@ +# Nela Launcher - Complete Project Documentation + +## Overview + +Nela Launcher is a premium, modern Minecraft launcher built exclusively for Fabric 1.16.5. It features a Nothing OS-inspired industrial minimalism design with a focus on performance, elegance, and user experience. + +--- + +## Brand Identity + +### Name +**Nela Launcher** + +### Tagline +- "Minimal outside. Powerful inside." +- "Launch clean." +- "Built for Fabric. Designed for players." + +### Visual Style +- **Inspiration:** Nothing OS industrial minimalism +- **Palette:** Monochrome (black/graphite/white) with subtle orange accent (#FF6B35) +- **Feel:** Premium, futuristic, gaming-tech, elegant + +### Logo Concept +- Strong geometric "N" monogram +- Industrial tech aesthetic +- Works well on dark backgrounds +- Can incorporate dot-matrix or ring elements + +--- + +## Technical Architecture + +### Tech Stack +- **Language:** Python 3.12+ +- **UI Framework:** PySide6 (Qt for Python) +- **Data Storage:** JSON for configs, profiles, manifests +- **Networking:** requests/aiohttp for downloads +- **Threading:** QThread for background tasks +- **Packaging:** PyInstaller ready + +### Folder Structure +``` +nela_launcher/ +├── main.py # Application entry point +├── requirements.txt # Python dependencies +├── README.md # Project documentation +├── app/ # Application bootstrap +│ ├── __init__.py +│ └── bootstrap.py # Main window & initialization +├── assets/ # Static resources +│ ├── icons/ # Application icons +│ ├── images/ # Images and textures +│ └── fonts/ # Custom fonts +├── config/ # Configuration files +│ └── config.json # User settings +├── core/ # Core launcher logic +│ ├── __init__.py +│ ├── config.py # Config & profile management +│ ├── mods.py # Mod management system +│ └── launcher.py # Minecraft launch engine +├── dialogs/ # Custom dialogs +│ └── __init__.py +├── pages/ # UI pages +│ ├── __init__.py +│ ├── splash_page.py # Animated splash screen +│ ├── welcome_page.py # Onboarding page +│ ├── login_page.py # Authentication page +│ └── home_dashboard.py # Main dashboard +├── services/ # Background services +│ └── __init__.py +├── utils/ # Utility functions +│ ├── __init__.py +│ └── logger.py # Logging system +├── widgets/ # Reusable UI components +│ ├── __init__.py +│ ├── title_bar.py # Custom title bar +│ └── sidebar.py # Navigation sidebar +├── styles/ # Styling system +│ ├── __init__.py +│ └── stylesheet.py # Main QSS stylesheet +├── logs/ # Log files +├── profiles/ # User profiles +└── mods/ # Mod files & manifests +``` + +--- + +## Design System + +### Color Palette +```css +/* Primary Colors */ +--bg-primary: #0A0A0A; +--bg-secondary: #0F0F0F; +--bg-card: #111111; +--bg-hover: #1A1A1A; + +/* Accent */ +--accent-primary: #FF6B35; +--accent-hover: #FF7B4D; +--accent-active: #E55A2B; + +/* Text */ +--text-primary: #FFFFFF; +--text-secondary: #E8E8E8; +--text-muted: #A0A0A0; +--text-disabled: #666666; + +/* Borders */ +--border-subtle: #1F1F1F; +--border-default: #2A2A2A; +--border-strong: #3A3A3A; + +/* Status */ +--success: #10B981; +--warning: #F59E0B; +--error: #EF4444; +``` + +### Typography +- **Font Family:** Inter, Segoe UI, Arial +- **Heading:** 28-56px, weight 700 +- **Subheading:** 18px, weight 600 +- **Body:** 14px, weight 400-500 +- **Caption:** 12px, weight 600, uppercase + +### Spacing Scale +- 4px, 8px, 12px, 16px, 20px, 24px, 32px, 40px, 48px, 64px + +### Border Radius +- Small: 6px +- Default: 8px +- Medium: 12px +- Large: 16px +- XL: 20px + +--- + +## Core Features + +### 1. Launcher Core +- Launch Minecraft 1.16.5 Fabric exclusively +- Java detection and management +- Memory allocation control +- Launch command construction +- Directory management +- File validation +- Error recovery + +### 2. Authentication +- Microsoft OAuth integration +- Offline mode support +- Session management +- Skin synchronization + +### 3. Profile System +- Create/delete/duplicate profiles +- Per-profile settings +- Performance presets +- Mod configurations +- Custom icons/banners + +### 4. Mod Manager +- 30+ curated mods across categories: + - **Performance:** Sodium, Lithium, Phosphor, Starlight, etc. + - **Visual:** Iris Shaders, Continuity, Entity Culling + - **QoL:** JourneyMap, JEI, AppleSkin, Mouse Tweaks + - **HUD:** Armor Status, Coordinates, Keystrokes, Ping Display + - **Utility:** WorldEdit, Replay Mod, Screenshot Viewer + - **Chat:** Chat Heads, Chat Filter +- Enable/disable toggles +- Conflict detection +- Dependency management +- Impact indicators + +### 5. Skin Studio +- Live 3D skin preview +- Rotate model view +- Slim/classic model switch +- Upload local skins +- Account skin sync +- Skin history + +### 6. Performance Center +- RAM allocation slider (512MB - 16GB) +- Performance presets: + - Low-end + - Balanced + - Performance + - Creator/Recording +- Motion blur toggle with intensity slider +- VSync control +- FPS limiter +- Render distance adjustment + +### 7. Update System +- Launcher self-updates +- Modpack updates +- Version changelogs +- Background update checks +- Rollback capability + +### 8. Settings +- Launcher preferences +- Theme options +- Animation controls +- Install paths +- Download settings +- Log verbosity +- Close-on-launch toggle + +### 9. Diagnostics +- Readable log viewer +- Copy log functionality +- Open log folder +- Crash summaries +- Repair suggestions + +--- + +## UI Pages + +### 1. Splash Screen +- Animated Nela logo +- Loading dots animation +- Version display +- Fade transitions + +### 2. Welcome Page +- Hero greeting +- Feature highlight cards +- Get Started CTA +- Learn More option + +### 3. Login Page +- Microsoft sign-in button +- Offline mode option +- Feature benefits list +- Status messages + +### 4. Home Dashboard +- Giant Play button +- Current profile card +- System status indicators +- News/updates feed +- Quick actions grid +- Performance summary + +### 5. Play Page +- Launch configuration +- Mod selection +- Performance settings +- Launch confirmation + +### 6. Profiles Page +- Profile list/grid +- Create new profile +- Edit profile settings +- Delete/duplicate actions + +### 7. Mods Page +- Category tabs +- Search functionality +- Mod cards with toggles +- Impact badges +- Conflict warnings + +### 8. Skin Studio +- 3D model preview +- Skin upload +- Model type selector +- Skin history + +### 9. Performance Center +- RAM slider +- Preset cards +- Advanced settings +- Motion blur controls + +### 10. Updates Page +- Available updates list +- Changelog viewer +- Update all button +- Individual update toggles + +### 11. Settings Page +- Categorized settings +- Search functionality +- Reset to defaults +- Import/export configs + +### 12. Logs Page +- Log file viewer +- Filter by level +- Copy/open actions +- Crash report analysis + +### 13. About Page +- App information +- Credits +- Links +- License info + +--- + +## UX Micro-interactions + +### Animations +- Page transitions (fade/slide) +- Button hover states +- Card hover elevations +- Loading skeletons +- Progress animations +- Dot loading indicators +- Toggle switches +- Slider value updates + +### Feedback +- Hover highlights on interactive elements +- Press state feedback +- Loading spinners +- Success/error toasts +- Progress indicators +- Status badges + +### States +- Empty states with illustrations +- Loading states with skeletons +- Error states with recovery options +- Success confirmations +- Disabled state styling + +--- + +## Mod Categories & Bundle Logic + +### Performance Bundle (Recommended) +- Sodium (required) +- Lithium +- FerriteCore +- LazyDFU +- Krypton +- Smooth Boot + +### Visual Bundle +- Iris Shaders +- Continuity +- Entity Culling +- Animatica +- Visual Overhaul + +### QoL Bundle +- JourneyMap +- JEI +- AppleSkin +- Mouse Tweaks +- Zoomify +- Borderless Mining + +### HUD Bundle +- Armor Status HUD +- Status Effect HUD +- Coordinates HUD +- Keystrokes +- Ping Display + +### Creator Bundle +- WorldEdit +- Replay Mod +- Screenshot Viewer + +--- + +## Security & Compliance + +### Strictly Prohibited +- ❌ No Forge support +- ❌ No multi-version support +- ❌ No hacked client features +- ❌ No cheats (aimbot, killaura, reach, fly, xray) +- ❌ No anti-cheat bypass +- ❌ No packet exploits +- ❌ No piracy/cracked launcher logic +- ❌ No malicious telemetry +- ❌ No account stealing +- ❌ No session abuse +- ❌ No copied Feather/Lunar assets + +### Allowed Features +- ✅ Fair-play visual enhancements +- ✅ Performance optimizations +- ✅ Quality of life improvements +- ✅ Creator tools +- ✅ HUD displays (non-advantageous) +- ✅ Menu/UI polish + +--- + +## Installation Flow + +1. **Splash Screen** - Animated logo (3 seconds) +2. **Welcome Screen** - Introduction and features +3. **Login** - Microsoft auth or offline mode +4. **Install Location** - Choose installation directory +5. **Java Detection** - Auto-detect or guided setup +6. **Fabric Setup** - Install Fabric loader 1.16.5 +7. **Modpack Install** - Download curated mods +8. **Progress States** - Elegant progress visualization +9. **Home Dashboard** - Enter main interface +10. **Launch** - Ready to play + +--- + +## Packaging & Distribution + +### PyInstaller Build +```bash +pyinstaller --name="Nela Launcher" \ + --windowed \ + --icon=assets/icons/icon.ico \ + --add-data="assets:assets" \ + --add-data="config:config" \ + --hidden-import=PySide6 \ + main.py +``` + +### Platform Support +- Windows 10/11 (primary) +- macOS (optional) +- Linux (optional) + +--- + +## Future Enhancements + +### Phase 2 +- Cloud save synchronization +- Friend system integration +- Server browser +- Mod auto-updates +- Theme customization +- Plugin system + +### Phase 3 +- Mobile companion app +- Web dashboard +- Community features +- Mod marketplace +- Analytics dashboard (opt-in) + +--- + +## Development Guidelines + +### Code Quality +- Type hints throughout +- Docstrings for public APIs +- Separation of concerns +- DRY principles +- Error handling +- Logging everywhere + +### UI/UX Standards +- Consistent spacing +- Proper hierarchy +- Accessible contrast +- Keyboard navigation +- Responsive layouts +- Smooth animations + +### Testing +- Unit tests for core logic +- Integration tests for launcher +- UI testing with pytest-qt +- Manual QA checklist + +--- + +## License + +Proprietary - All rights reserved + +--- + +## Contact + +For questions, support, or contributions, please refer to the official Nela Launcher repository. diff --git a/nela_launcher/README.md b/nela_launcher/README.md new file mode 100644 index 0000000..e4d7211 --- /dev/null +++ b/nela_launcher/README.md @@ -0,0 +1,68 @@ +# Nela Launcher + +**Minimal outside. Powerful inside.** + +A premium modern Minecraft launcher built with Python 3.12+ and PySide6, designed exclusively for Fabric 1.16.5. + +## Brand Identity + +- **Name:** Nela Launcher +- **Tagline:** Launch clean. Built for Fabric. Designed for players. +- **Style:** Nothing OS inspired industrial minimalism +- **Palette:** Monochrome black/graphite/white with subtle orange accent + +## Features + +- 🎯 Fabric 1.16.5 only support +- 🎨 Premium Nothing OS-inspired UI +- 🔐 Microsoft authentication +- 📦 30+ curated mods +- 🎮 Skin studio with 3D preview +- ⚡ Performance center with presets +- 🔄 Automatic updates +- 📊 Profile management +- 🛠️ Diagnostic tools + +## Project Structure + +``` +nela_launcher/ +├── app/ # Application bootstrap +├── assets/ # Icons, images, fonts +├── config/ # Configuration files +├── core/ # Launcher engine +├── dialogs/ # Custom dialogs +├── pages/ # UI pages +├── services/ # Background services +├── utils/ # Utilities +├── widgets/ # Reusable UI components +├── styles/ # Styling system +├── logs/ # Log files +├── profiles/ # User profiles +├── mods/ # Mod manifests +└── main.py # Entry point +``` + +## Requirements + +- Python 3.12+ +- PySide6 +- requests +- aiohttp + +## Installation + +```bash +pip install -r requirements.txt +python main.py +``` + +## Build + +```bash +pyinstaller --name="Nela Launcher" --windowed --icon=assets/icons/icon.ico main.py +``` + +## License + +Proprietary - All rights reserved diff --git a/nela_launcher/app/__init__.py b/nela_launcher/app/__init__.py new file mode 100644 index 0000000..02c9caa --- /dev/null +++ b/nela_launcher/app/__init__.py @@ -0,0 +1,8 @@ +""" +Nela Launcher App Module +Application bootstrap and main window +""" + +from .bootstrap import NelaApplication + +__all__ = ['NelaApplication'] diff --git a/nela_launcher/app/bootstrap.py b/nela_launcher/app/bootstrap.py new file mode 100644 index 0000000..8c57943 --- /dev/null +++ b/nela_launcher/app/bootstrap.py @@ -0,0 +1,129 @@ +""" +Nela Launcher Bootstrap +Main application window and initialization +""" + +from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QStackedWidget, QGraphicsDropShadowEffect +from PySide6.QtCore import Qt, QTimer, Signal, QPropertyAnimation, QEasingCurve +from PySide6.QtGui import QColor + +from pages.splash_page import SplashPage +from pages.welcome_page import WelcomePage +from pages.login_page import LoginPage +from pages.home_dashboard import HomeDashboard +from widgets.title_bar import TitleBar +from widgets.sidebar import Sidebar +from styles.stylesheet import get_main_stylesheet + + +class NelaApplication(QMainWindow): + """Main Nela Launcher Application Window""" + + page_changed = Signal(str) + + def __init__(self): + super().__init__() + + self.setWindowTitle("Nela Launcher") + self.setMinimumSize(1200, 800) + self.resize(1400, 900) + + # Remove default window frame for custom title bar + self.setWindowFlags(Qt.FramelessWindowHint) + self.setAttribute(Qt.WA_TranslucentBackground) + + # Setup UI + self._setup_ui() + self._apply_styles() + self._setup_effects() + + # Start with splash screen + self._show_splash() + + def _setup_ui(self): + """Setup the main UI structure""" + + # Central widget + central_widget = QWidget() + self.setCentralWidget(central_widget) + + # Main layout + main_layout = QVBoxLayout(central_widget) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + + # Title bar + self.title_bar = TitleBar(self) + main_layout.addWidget(self.title_bar) + + # Content container + content_widget = QWidget() + content_layout = QVBoxLayout(content_widget) + content_layout.setContentsMargins(0, 0, 0, 0) + content_layout.setSpacing(0) + + # Page stack + self.page_stack = QStackedWidget() + self.page_stack.setContentsMargins(0, 0, 0, 0) + + # Initialize pages + self.pages = { + 'splash': SplashPage(self), + 'welcome': WelcomePage(self), + 'login': LoginPage(self), + 'home': HomeDashboard(self), + } + + # Add pages to stack + for page_name, page_widget in self.pages.items(): + self.page_stack.addWidget(page_widget) + + content_layout.addWidget(self.page_stack) + main_layout.addWidget(content_widget) + + def _apply_styles(self): + """Apply main stylesheet""" + self.setStyleSheet(get_main_stylesheet()) + + def _setup_effects(self): + """Setup visual effects""" + + # Drop shadow for window + shadow = QGraphicsDropShadowEffect() + shadow.setBlurRadius(30) + shadow.setXOffset(0) + shadow.setYOffset(0) + shadow.setColor(QColor(0, 0, 0, 100)) + self.graphicsEffect = shadow + + def _show_splash(self): + """Show splash screen then transition to welcome""" + self.page_stack.setCurrentWidget(self.pages['splash']) + + # Auto-transition after splash animation + QTimer.singleShot(3000, self._show_welcome) + + def _show_welcome(self): + """Transition to welcome page""" + self.page_stack.setCurrentWidget(self.pages['welcome']) + + def navigate_to(self, page_name: str): + """Navigate to a specific page""" + if page_name in self.pages: + self.page_stack.setCurrentWidget(self.pages[page_name]) + self.page_changed.emit(page_name) + + def go_home(self): + """Navigate to home dashboard""" + self.navigate_to('home') + + def closeEvent(self, event): + """Handle window close""" + # Save state before closing + self._save_state() + event.accept() + + def _save_state(self): + """Save application state""" + # TODO: Implement state saving + pass diff --git a/nela_launcher/core/__init__.py b/nela_launcher/core/__init__.py new file mode 100644 index 0000000..ae10167 --- /dev/null +++ b/nela_launcher/core/__init__.py @@ -0,0 +1,9 @@ +""" +Nela Launcher Core Module +Contains launcher engine and core functionality +""" + +from .config import ConfigManager, ProfileManager +from .mods import ModManager + +__all__ = ['ConfigManager', 'ProfileManager', 'ModManager'] diff --git a/nela_launcher/core/config.py b/nela_launcher/core/config.py new file mode 100644 index 0000000..9d81fd6 --- /dev/null +++ b/nela_launcher/core/config.py @@ -0,0 +1,233 @@ +""" +Nela Launcher Configuration System +Manages application settings, profiles, and preferences +""" + +import json +from pathlib import Path +from typing import Dict, Any, Optional +from datetime import datetime + + +class ConfigManager: + """Manages application configuration and settings""" + + DEFAULT_CONFIG = { + "launcher": { + "theme": "dark", + "language": "en", + "close_on_launch": False, + "check_updates": True, + "animations_enabled": True, + "reduced_motion": False, + }, + "minecraft": { + "version": "1.16.5", + "loader": "fabric", + "java_path": "", + "memory_min": 512, + "memory_max": 4096, + "render_distance": 12, + "fullscreen": False, + }, + "paths": { + "install_dir": "", + "mods_dir": "", + "screenshots_dir": "", + "logs_dir": "", + }, + "profile": { + "current": "default", + "last_played": None, + }, + "user": { + "logged_in": False, + "username": "", + "uuid": "", + "token": "", + }, + "performance": { + "preset": "balanced", + "motion_blur": False, + "motion_blur_intensity": 0.5, + "vsync": True, + "fps_limit": 260, + } + } + + def __init__(self, config_dir: str = "config"): + self.config_dir = Path(config_dir) + self.config_dir.mkdir(exist_ok=True) + self.config_file = self.config_dir / "config.json" + self.config = self._load_config() + + def _load_config(self) -> Dict[str, Any]: + """Load configuration from file or create default""" + + if self.config_file.exists(): + try: + with open(self.config_file, 'r', encoding='utf-8') as f: + loaded = json.load(f) + # Merge with defaults to ensure all keys exist + return self._merge_configs(self.DEFAULT_CONFIG, loaded) + except (json.JSONDecodeError, IOError): + pass + + # Create default config + self.config = self.DEFAULT_CONFIG.copy() + self.save() + return self.config + + def _merge_configs(self, default: Dict, loaded: Dict) -> Dict: + """Recursively merge loaded config with defaults""" + result = default.copy() + for key, value in loaded.items(): + if key in result: + if isinstance(value, dict) and isinstance(result[key], dict): + result[key] = self._merge_configs(result[key], value) + else: + result[key] = value + return result + + def save(self): + """Save current configuration to file""" + with open(self.config_file, 'w', encoding='utf-8') as f: + json.dump(self.config, f, indent=2, ensure_ascii=False) + + def get(self, key_path: str, default: Any = None) -> Any: + """ + Get a configuration value using dot notation + + Args: + key_path: Dot-separated path (e.g., "minecraft.memory_max") + default: Default value if key not found + + Returns: + Configuration value or default + """ + keys = key_path.split('.') + value = self.config + + for key in keys: + if isinstance(value, dict) and key in value: + value = value[key] + else: + return default + + return value + + def set(self, key_path: str, value: Any): + """ + Set a configuration value using dot notation + + Args: + key_path: Dot-separated path + value: Value to set + """ + keys = key_path.split('.') + config = self.config + + for key in keys[:-1]: + if key not in config: + config[key] = {} + config = config[key] + + config[keys[-1]] = value + self.save() + + def reset_to_defaults(self): + """Reset configuration to defaults""" + self.config = self.DEFAULT_CONFIG.copy() + self.save() + + def get_all(self) -> Dict[str, Any]: + """Get entire configuration""" + return self.config + + +class ProfileManager: + """Manages Minecraft profiles""" + + DEFAULT_PROFILE = { + "name": "Default", + "version": "1.16.5", + "loader": "fabric", + "memory_max": 4096, + "java_args": "", + "mods_enabled": [], + "performance_preset": "balanced", + "created": None, + "last_played": None, + "icon": "default", + } + + def __init__(self, profiles_dir: str = "profiles"): + self.profiles_dir = Path(profiles_dir) + self.profiles_dir.mkdir(exist_ok=True) + self.profiles = self._load_profiles() + + def _load_profiles(self) -> Dict[str, Dict]: + """Load all profiles from directory""" + profiles = {} + + for profile_file in self.profiles_dir.glob("*.json"): + try: + with open(profile_file, 'r', encoding='utf-8') as f: + profile_data = json.load(f) + profile_name = profile_file.stem + profiles[profile_name] = profile_data + except (json.JSONDecodeError, IOError): + continue + + # Create default profile if none exist + if not profiles: + default_profile = self.DEFAULT_PROFILE.copy() + default_profile["created"] = datetime.now().isoformat() + profiles["default"] = default_profile + self.save_profile("default", default_profile) + + return profiles + + def get_profile(self, name: str) -> Optional[Dict]: + """Get a specific profile""" + return self.profiles.get(name) + + def save_profile(self, name: str, profile: Dict): + """Save a profile""" + profile_file = self.profiles_dir / f"{name}.json" + with open(profile_file, 'w', encoding='utf-8') as f: + json.dump(profile, f, indent=2, ensure_ascii=False) + self.profiles[name] = profile + + def create_profile(self, name: str, base_profile: Optional[str] = None) -> Dict: + """Create a new profile""" + if base_profile and base_profile in self.profiles: + profile = self.profiles[base_profile].copy() + else: + profile = self.DEFAULT_PROFILE.copy() + + profile["name"] = name + profile["created"] = datetime.now().isoformat() + + self.save_profile(name, profile) + return profile + + def delete_profile(self, name: str) -> bool: + """Delete a profile""" + if name in self.profiles and name != "default": + profile_file = self.profiles_dir / f"{name}.json" + if profile_file.exists(): + profile_file.unlink() + del self.profiles[name] + return True + return False + + def list_profiles(self) -> list: + """List all profile names""" + return list(self.profiles.keys()) + + def update_last_played(self, name: str): + """Update last played timestamp for a profile""" + if name in self.profiles: + self.profiles[name]["last_played"] = datetime.now().isoformat() + self.save_profile(name, self.profiles[name]) diff --git a/nela_launcher/core/mods.py b/nela_launcher/core/mods.py new file mode 100644 index 0000000..976c697 --- /dev/null +++ b/nela_launcher/core/mods.py @@ -0,0 +1,488 @@ +""" +Nela Launcher Mod System +Manages curated modpack for Fabric 1.16.5 +""" + +import json +from pathlib import Path +from typing import Dict, List, Optional, Any +from datetime import datetime + + +class ModManager: + """Manages Minecraft mods for Fabric 1.16.5""" + + # Curated mod list for Fabric 1.16.5 + CURATED_MODS = [ + # Performance + { + "id": "sodium", + "name": "Sodium", + "description": "Modern rendering engine for massive FPS improvements", + "category": "performance", + "version": "mc1.16.5-0.2.0", + "required": True, + "impact": "high", + "side": "client", + "dependencies": [], + }, + { + "id": "lithium", + "name": "Lithium", + "description": "Game physics, mob AI and entity ticking optimizations", + "category": "performance", + "version": "mc1.16.5-0.6.6", + "required": False, + "impact": "high", + "side": "server", + "dependencies": [], + }, + { + "id": "phosphor", + "name": "Phosphor", + "description": "Lighting engine optimizations", + "category": "performance", + "version": "mc1.16.3-0.7.1", + "required": False, + "impact": "medium", + "side": "both", + "dependencies": [], + }, + { + "id": "starlight", + "name": "Starlight", + "description": "Rewrite of Minecraft's lighting engine", + "category": "performance", + "version": "1.0.0-RC1", + "required": False, + "impact": "high", + "side": "both", + "dependencies": [], + }, + # Visual Polish + { + "id": "iris", + "name": "Iris Shaders", + "description": "Modern shaders support with excellent performance", + "category": "visual", + "version": "1.1.0", + "required": False, + "impact": "medium", + "side": "client", + "dependencies": ["sodium"], + }, + { + "id": "continuity", + "name": "Continuity", + "description": "Connected textures support", + "category": "visual", + "version": "1.0.0", + "required": False, + "impact": "low", + "side": "client", + "dependencies": [], + }, + { + "id": "visual_overhaul", + "name": "Visual Overhaul", + "description": "Particle and visual enhancements", + "category": "visual", + "version": "1.4.0", + "required": False, + "impact": "low", + "side": "client", + "dependencies": [], + }, + # Quality of Life + { + "id": "appleskin", + "name": "AppleSkin", + "description": "Shows food saturation and hunger values", + "category": "qol", + "version": "mc1.16.4-1.0.11", + "required": False, + "impact": "none", + "side": "client", + "dependencies": [], + }, + { + "id": "journeymap", + "name": "JourneyMap", + "description": "Real-time mapping with waypoints", + "category": "qol", + "version": "5.7.1", + "required": False, + "impact": "low", + "side": "client", + "dependencies": [], + }, + { + "id": "jei", + "name": "JEI (Just Enough Items)", + "description": "View recipes and item information", + "category": "qol", + "version": "7.7.1.123", + "required": False, + "impact": "low", + "side": "both", + "dependencies": [], + }, + { + "id": "mouse_tweaks", + "name": "Mouse Tweaks", + "description": "Enhanced inventory management", + "category": "qol", + "version": "2.14", + "required": False, + "impact": "none", + "side": "client", + "dependencies": [], + }, + { + "id": "inventory_profiles", + "name": "Inventory Profiles", + "description": "Advanced inventory management tools", + "category": "qol", + "version": "fabric-mc1.16.4-0.8.2", + "required": False, + "impact": "none", + "side": "client", + "dependencies": [], + }, + # HUD / Interface + { + "id": "armor_hud", + "name": "Armor Status HUD", + "description": "Displays armor durability on screen", + "category": "hud", + "version": "1.2.0", + "required": False, + "impact": "none", + "side": "client", + "dependencies": [], + }, + { + "id": "status_effect_hud", + "name": "Status Effect HUD", + "description": "Shows active potion effects", + "category": "hud", + "version": "1.0.4", + "required": False, + "impact": "none", + "side": "client", + "dependencies": [], + }, + { + "id": "coordinates_hud", + "name": "Coordinates HUD", + "description": "Display coordinates and dimension", + "category": "hud", + "version": "1.1.2", + "required": False, + "impact": "none", + "side": "client", + "dependencies": [], + }, + { + "id": "keystrokes", + "name": "Keystrokes", + "description": "Display pressed keys on screen", + "category": "hud", + "version": "1.0.0", + "required": False, + "impact": "none", + "side": "client", + "dependencies": [], + }, + { + "id": "ping_display", + "name": "Ping Display", + "description": "Show server ping in corner", + "category": "hud", + "version": "1.0.0", + "required": False, + "impact": "none", + "side": "client", + "dependencies": [], + }, + # Utility + { + "id": "worldedit", + "name": "WorldEdit", + "description": "In-game world editor for creators", + "category": "utility", + "version": "7.2.5", + "required": False, + "impact": "none", + "side": "both", + "dependencies": [], + }, + { + "id": "replaymod", + "name": "Replay Mod", + "description": "Record and share gameplay sessions", + "category": "utility", + "version": "2.6.1", + "required": False, + "impact": "medium", + "side": "client", + "dependencies": [], + }, + { + "id": "screenshot_viewer", + "name": "Screenshot Viewer", + "description": "Browse screenshots in-game", + "category": "utility", + "version": "1.2.0", + "required": False, + "impact": "none", + "side": "client", + "dependencies": [], + }, + # Chat Improvements + { + "id": "chat_heads", + "name": "Chat Heads", + "description": "Show player heads in chat", + "category": "chat", + "version": "0.4.0", + "required": False, + "impact": "none", + "side": "client", + "dependencies": [], + }, + { + "id": "chat_filter", + "name": "Chat Filter", + "description": "Filter unwanted chat messages", + "category": "chat", + "version": "1.0.0", + "required": False, + "impact": "none", + "side": "client", + "dependencies": [], + }, + # Fabric API (Required) + { + "id": "fabric_api", + "name": "Fabric API", + "description": "Core Fabric API required by most mods", + "category": "core", + "version": "0.34.9+1.16", + "required": True, + "impact": "none", + "side": "both", + "dependencies": [], + }, + # Additional Performance + { + "id": "lazydfu", + "name": "LazyDFU", + "description": "Defers initialization for faster startup", + "category": "performance", + "version": "0.1.3", + "required": False, + "impact": "medium", + "side": "both", + "dependencies": [], + }, + { + "id": "krypton", + "name": "Krypton", + "description": "Networking stack improvements", + "category": "performance", + "version": "0.1.3", + "required": False, + "impact": "low", + "side": "both", + "dependencies": [], + }, + { + "id": "ferritecore", + "name": "FerriteCore", + "description": "Memory usage reductions", + "category": "performance", + "version": "2.1.0", + "required": False, + "impact": "high", + "side": "both", + "dependencies": [], + }, + # Visual Extras + { + "id": "entity_culling", + "name": "Entity Culling", + "description": "Skip rendering entities behind blocks", + "category": "visual", + "version": "1.3.0", + "required": False, + "impact": "medium", + "side": "client", + "dependencies": [], + }, + { + "id": "smooth_boot", + "name": "Smooth Boot", + "description": "Smoother game startup experience", + "category": "performance", + "version": "1.4.0", + "required": False, + "impact": "low", + "side": "client", + "dependencies": [], + }, + { + "id": "borderless_mining", + "name": "Borderless Mining", + "description": "True fullscreen without alt-tab issues", + "category": "qol", + "version": "1.0.2", + "required": False, + "impact": "none", + "side": "client", + "dependencies": [], + }, + { + "id": "zoomify", + "name": "Zoomify", + "description": "Optifine-like zoom feature", + "category": "qol", + "version": "2.0.0", + "required": False, + "impact": "none", + "side": "client", + "dependencies": [], + }, + { + "id": "animatica", + "name": "Animatica", + "description": "Custom animated textures support", + "category": "visual", + "version": "0.4+1.16", + "required": False, + "impact": "none", + "side": "client", + "dependencies": [], + }, + ] + + def __init__(self, mods_dir: str = "mods"): + self.mods_dir = Path(mods_dir) + self.mods_dir.mkdir(exist_ok=True) + self.mods_state_file = self.mods_dir / "mods_state.json" + self.mods_state = self._load_state() + + def _load_state(self) -> Dict[str, Any]: + """Load mod enable/disable state""" + if self.mods_state_file.exists(): + try: + with open(self.mods_state_file, 'r', encoding='utf-8') as f: + return json.load(f) + except (json.JSONDecodeError, IOError): + pass + + # Default: enable all required mods, disable optional + state = {"enabled": []} + for mod in self.CURATED_MODS: + if mod["required"]: + state["enabled"].append(mod["id"]) + + self._save_state(state) + return state + + def _save_state(self, state: Dict[str, Any]): + """Save mod state to file""" + with open(self.mods_state_file, 'w', encoding='utf-8') as f: + json.dump(state, f, indent=2, ensure_ascii=False) + self.mods_state = state + + def get_all_mods(self) -> List[Dict]: + """Get all curated mods""" + return self.CURATED_MODS.copy() + + def get_mod_by_id(self, mod_id: str) -> Optional[Dict]: + """Get a specific mod by ID""" + for mod in self.CURATED_MODS: + if mod["id"] == mod_id: + return mod.copy() + return None + + def get_mods_by_category(self, category: str) -> List[Dict]: + """Get mods filtered by category""" + return [mod.copy() for mod in self.CURATED_MODS if mod["category"] == category] + + def is_mod_enabled(self, mod_id: str) -> bool: + """Check if a mod is enabled""" + return mod_id in self.mods_state.get("enabled", []) + + def toggle_mod(self, mod_id: str, enabled: bool): + """Enable or disable a mod""" + mod = self.get_mod_by_id(mod_id) + if not mod: + return False + + # Can't disable required mods + if not enabled and mod["required"]: + return False + + if enabled: + if mod_id not in self.mods_state["enabled"]: + self.mods_state["enabled"].append(mod_id) + else: + if mod_id in self.mods_state["enabled"]: + self.mods_state["enabled"].remove(mod_id) + + self._save_state(self.mods_state) + return True + + def get_enabled_mods(self) -> List[Dict]: + """Get list of enabled mods""" + enabled_ids = self.mods_state.get("enabled", []) + return [mod for mod in self.CURATED_MODS if mod["id"] in enabled_ids] + + def get_disabled_mods(self) -> List[Dict]: + """Get list of disabled mods""" + enabled_ids = self.mods_state.get("enabled", []) + return [mod for mod in self.CURATED_MODS if mod["id"] not in enabled_ids] + + def reset_to_defaults(self): + """Reset all mod states to defaults""" + self.mods_state = {"enabled": []} + for mod in self.CURATED_MODS: + if mod["required"]: + self.mods_state["enabled"].append(mod["id"]) + self._save_state(self.mods_state) + + def get_categories(self) -> List[str]: + """Get unique mod categories""" + categories = set() + for mod in self.CURATED_MODS: + categories.add(mod["category"]) + return sorted(list(categories)) + + def search_mods(self, query: str) -> List[Dict]: + """Search mods by name or description""" + query = query.lower() + results = [] + for mod in self.CURATED_MODS: + if query in mod["name"].lower() or query in mod["description"].lower(): + results.append(mod.copy()) + return results + + def has_conflicts(self, mod_id: str) -> List[str]: + """Check for potential mod conflicts""" + # Simplified conflict detection + conflicts = [] + mod = self.get_mod_by_id(mod_id) + if not mod: + return conflicts + + # Example: Starlight conflicts with Phosphor + if mod_id == "starlight": + if self.is_mod_enabled("phosphor"): + conflicts.append("phosphor") + elif mod_id == "phosphor": + if self.is_mod_enabled("starlight"): + conflicts.append("starlight") + + return conflicts diff --git a/nela_launcher/dialogs/__init__.py b/nela_launcher/dialogs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nela_launcher/main.py b/nela_launcher/main.py new file mode 100644 index 0000000..0e658c6 --- /dev/null +++ b/nela_launcher/main.py @@ -0,0 +1,53 @@ +""" +Nela Launcher - Premium Minecraft Fabric 1.16.5 Launcher +Main Entry Point + +Minimal outside. Powerful inside. +""" + +import sys +import os +from pathlib import Path + +# Add project root to path +project_root = Path(__file__).parent +sys.path.insert(0, str(project_root)) + +from PySide6.QtWidgets import QApplication +from PySide6.QtCore import Qt, QCoreApplication +from PySide6.QtGui import QFont, QFontDatabase + +from app.bootstrap import NelaApplication +from utils.logger import setup_logger + + +def main(): + """Main entry point for Nela Launcher""" + + # Enable High DPI scaling + QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling) + QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) + + # Create application instance + app = QApplication(sys.argv) + app.setApplicationName("Nela Launcher") + app.setApplicationVersion("1.0.0") + app.setOrganizationName("Nela") + + # Set application style + app.setStyle("Fusion") + + # Setup logging + logger = setup_logger() + logger.info("Starting Nela Launcher...") + + # Create and show main application + nela_app = NelaApplication() + nela_app.show() + + # Run event loop + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() diff --git a/nela_launcher/pages/__init__.py b/nela_launcher/pages/__init__.py new file mode 100644 index 0000000..ad41ee6 --- /dev/null +++ b/nela_launcher/pages/__init__.py @@ -0,0 +1,11 @@ +""" +Nela Launcher Pages Module +UI pages for the application +""" + +from .splash_page import SplashPage +from .welcome_page import WelcomePage +from .login_page import LoginPage +from .home_dashboard import HomeDashboard + +__all__ = ['SplashPage', 'WelcomePage', 'LoginPage', 'HomeDashboard'] diff --git a/nela_launcher/pages/home_dashboard.py b/nela_launcher/pages/home_dashboard.py new file mode 100644 index 0000000..bf1fa1c --- /dev/null +++ b/nela_launcher/pages/home_dashboard.py @@ -0,0 +1,506 @@ +""" +Home Dashboard +Main landing page with play button, profile info, and status +""" + +from PySide6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, + QFrame, QScrollArea, QSpacerItem, QSizePolicy, QGridLayout +) +from PySide6.QtCore import Qt, Signal + + +class HomeDashboard(QWidget): + """Premium home dashboard with play button and status cards""" + + play_clicked = Signal() + profile_changed = Signal(str) + + def __init__(self, parent=None): + super().__init__(parent) + self._setup_ui() + + def _setup_ui(self): + """Setup home dashboard UI""" + + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(40, 40, 40, 40) + main_layout.setSpacing(24) + + # Create scroll area for content + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + scroll_area.setStyleSheet(""" + QScrollArea { + border: none; + background-color: transparent; + } + """) + + # Content widget + content_widget = QWidget() + content_layout = QVBoxLayout(content_widget) + content_layout.setContentsMargins(0, 0, 0, 0) + content_layout.setSpacing(24) + + # Header section + header_section = self._create_header_section() + content_layout.addWidget(header_section) + + # Main content grid + main_grid = QGridLayout() + main_grid.setSpacing(24) + + # Left column - Play section (wider) + left_column = QVBoxLayout() + left_column.setSpacing(24) + + # Hero play card + play_card = self._create_play_card() + left_column.addWidget(play_card) + + # Profile summary + profile_card = self._create_profile_card() + left_column.addWidget(profile_card) + + # Status indicators + status_card = self._create_status_card() + left_column.addWidget(status_card) + + left_container = QFrame() + left_container.setLayout(left_column) + main_grid.addLayout(left_column, 0, 0, 1, 2) # Span 2 columns + + # Right column - Info cards + right_column = QVBoxLayout() + right_column.setSpacing(24) + + # News/Updates card + news_card = self._create_news_card() + right_column.addWidget(news_card) + + # Quick actions + actions_card = self._create_quick_actions_card() + right_column.addWidget(actions_card) + + # Performance summary + perf_card = self._create_performance_card() + right_column.addWidget(perf_card) + + right_container = QFrame() + right_container.setLayout(right_column) + main_grid.addLayout(right_column, 0, 2, 1, 1) + + # Set column stretch + main_grid.setColumnStretch(0, 3) + main_grid.setColumnStretch(1, 1) + main_grid.setColumnStretch(2, 1) + + content_layout.addLayout(main_grid) + + # Add spacer at bottom + spacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) + content_layout.addItem(spacer) + + scroll_area.setWidget(content_widget) + main_layout.addWidget(scroll_area) + + def _create_header_section(self) -> QFrame: + """Create header section with greeting""" + + header = QFrame() + header.setFixedHeight(80) + layout = QHBoxLayout(header) + layout.setContentsMargins(0, 0, 0, 0) + + # Greeting + greeting_label = QLabel("Good to see you") + greeting_label.setStyleSheet(""" + font-size: 36px; + font-weight: 700; + color: #FFFFFF; + letter-spacing: 0.5px; + """) + layout.addWidget(greeting_label) + + layout.addStretch() + + # Version badge + version_badge = QLabel("v1.16.5 • Fabric") + version_badge.setObjectName("badgeAccent") + version_badge.setStyleSheet(""" + QLabel#badgeAccent { + background-color: rgba(255, 107, 53, 0.15); + color: #FF6B35; + border-radius: 8px; + padding: 8px 16px; + font-size: 13px; + font-weight: 600; + } + """) + layout.addWidget(version_badge) + + return header + + def _create_play_card(self) -> QFrame: + """Create the main play button card""" + + card = QFrame() + card.setObjectName("highlightCard") + card.setMinimumHeight(280) + card.setStyleSheet(""" + QFrame#highlightCard { + background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #111111, stop:1 #161616); + border: 1px solid #2A2A2A; + border-radius: 20px; + padding: 40px; + } + """) + + layout = QVBoxLayout(card) + layout.setAlignment(Qt.AlignCenter) + layout.setSpacing(24) + + # Profile name + profile_label = QLabel("Default Profile") + profile_label.setStyleSheet(""" + font-size: 18px; + color: #A0A0A0; + font-weight: 500; + """) + profile_label.setAlignment(Qt.AlignCenter) + layout.addWidget(profile_label) + + # Main PLAY button + self.play_button = QPushButton("PLAY") + self.play_button.setObjectName("playButton") + self.play_button.setMinimumSize(320, 80) + self.play_button.clicked.connect(self.play_clicked.emit) + layout.addWidget(self.play_button, alignment=Qt.AlignCenter) + + # Status text + status_label = QLabel("Ready to launch • All systems operational") + status_label.setStyleSheet(""" + font-size: 14px; + color: #666666; + """) + status_label.setAlignment(Qt.AlignCenter) + layout.addWidget(status_label) + + return card + + def _create_profile_card(self) -> QFrame: + """Create profile summary card""" + + card = QFrame() + card.setObjectName("card") + card.setMinimumHeight(140) + + layout = QHBoxLayout(card) + layout.setContentsMargins(24, 24, 24, 24) + layout.setSpacing(24) + + # Profile icon placeholder + icon_frame = QFrame() + icon_frame.setFixedSize(80, 80) + icon_frame.setStyleSheet(""" + background-color: #1F1F1F; + border-radius: 12px; + border: 1px solid #2A2A2A; + """) + layout.addWidget(icon_frame) + + # Profile info + info_layout = QVBoxLayout() + info_layout.setSpacing(8) + + title_label = QLabel("Current Profile") + title_label.setStyleSheet(""" + font-size: 12px; + color: #666666; + text-transform: uppercase; + letter-spacing: 1px; + """) + info_layout.addWidget(title_label) + + name_label = QLabel("Default") + name_label.setStyleSheet(""" + font-size: 24px; + color: #FFFFFF; + font-weight: 600; + """) + info_layout.addWidget(name_label) + + details_label = QLabel("Fabric 1.16.5 • 4GB RAM • 28 mods enabled") + details_label.setStyleSheet(""" + font-size: 14px; + color: #888888; + """) + info_layout.addWidget(details_label) + + info_layout.addStretch() + layout.addLayout(info_layout, 1) + + # Change button + change_btn = QPushButton("Change") + change_btn.setObjectName("secondaryButton") + change_btn.setFixedSize(100, 40) + change_btn.setStyleSheet(""" + QPushButton#secondaryButton { + background-color: transparent; + border: 1px solid #2A2A2A; + color: #A0A0A0; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + } + QPushButton#secondaryButton:hover { + border-color: #FF6B35; + color: #FF6B35; + } + """) + layout.addWidget(change_btn) + + return card + + def _create_status_card(self) -> QFrame: + """Create system status card""" + + card = QFrame() + card.setObjectName("card") + card.setMinimumHeight(120) + + layout = QHBoxLayout(card) + layout.setContentsMargins(24, 24, 24, 24) + layout.setSpacing(24) + + # Status items + status_items = [ + ("Java", "✓ Installed", "#10B981"), + ("Fabric", "✓ Ready", "#10B981"), + ("Mods", "28 loaded", "#FF6B35"), + ("Assets", "✓ Complete", "#10B981"), + ] + + for item in status_items: + item_layout = QVBoxLayout() + item_layout.setSpacing(4) + + label = QLabel(item[0]) + label.setStyleSheet(""" + font-size: 12px; + color: #666666; + text-transform: uppercase; + """) + item_layout.addWidget(label) + + value_label = QLabel(item[1]) + value_label.setStyleSheet(f""" + font-size: 16px; + color: {item[2]}; + font-weight: 600; + """) + item_layout.addWidget(value_label) + + layout.addLayout(item_layout) + layout.addSpacing(20) + + layout.addStretch() + + return card + + def _create_news_card(self) -> QFrame: + """Create news/updates card""" + + card = QFrame() + card.setObjectName("card") + card.setMinimumHeight(200) + + layout = QVBoxLayout(card) + layout.setContentsMargins(24, 24, 24, 24) + layout.setSpacing(16) + + # Title + title_label = QLabel("Latest Updates") + title_label.setStyleSheet(""" + font-size: 18px; + font-weight: 600; + color: #FFFFFF; + """) + layout.addWidget(title_label) + + # News items + news_items = [ + ("Performance improvements", "v1.0.0"), + ("New modpack available", "Dec 15"), + ("Bug fixes and stability", "Dec 10"), + ] + + for news in news_items: + news_layout = QHBoxLayout() + news_layout.setSpacing(12) + + dot = QFrame() + dot.setFixedSize(8, 8) + dot.setStyleSheet(""" + background-color: #FF6B35; + border-radius: 4px; + """) + news_layout.addWidget(dot) + + text_label = QLabel(news[0]) + text_label.setStyleSheet(""" + font-size: 14px; + color: #E8E8E8; + """) + news_layout.addWidget(text_label, 1) + + date_label = QLabel(news[1]) + date_label.setStyleSheet(""" + font-size: 12px; + color: #666666; + """) + news_layout.addWidget(date_label) + + layout.addLayout(news_layout) + + layout.addStretch() + + return card + + def _create_quick_actions_card(self) -> QFrame: + """Create quick actions card""" + + card = QFrame() + card.setObjectName("card") + card.setMinimumHeight(160) + + layout = QVBoxLayout(card) + layout.setContentsMargins(24, 24, 24, 24) + layout.setSpacing(16) + + # Title + title_label = QLabel("Quick Actions") + title_label.setStyleSheet(""" + font-size: 18px; + font-weight: 600; + color: #FFFFFF; + """) + layout.addWidget(title_label) + + # Action buttons grid + actions_grid = QGridLayout() + actions_grid.setSpacing(12) + + actions = [ + ("⚙", "Settings"), + ("◫", "Mods"), + ("📊", "Performance"), + ("🔧", "Repair"), + ] + + for i, action in enumerate(actions): + btn = QPushButton(f"{action[0]} {action[1]}") + btn.setFixedHeight(44) + btn.setStyleSheet(""" + QPushButton { + background-color: #1A1A1A; + border: 1px solid #1F1F1F; + border-radius: 8px; + color: #E8E8E8; + font-size: 13px; + font-weight: 500; + text-align: left; + padding-left: 16px; + } + QPushButton:hover { + border-color: #FF6B35; + background-color: #1F1F1F; + } + """) + + row = i // 2 + col = i % 2 + actions_grid.addWidget(btn, row, col) + + layout.addLayout(actions_grid) + + return card + + def _create_performance_card(self) -> QFrame: + """Create performance summary card""" + + card = QFrame() + card.setObjectName("card") + card.setMinimumHeight(140) + + layout = QVBoxLayout(card) + layout.setContentsMargins(24, 24, 24, 24) + layout.setSpacing(16) + + # Title + title_label = QLabel("Performance Status") + title_label.setStyleSheet(""" + font-size: 18px; + font-weight: 600; + color: #FFFFFF; + """) + layout.addWidget(title_label) + + # Metrics + metrics_layout = QHBoxLayout() + metrics_layout.setSpacing(24) + + metrics = [ + ("RAM", "4 GB", "2.1 GB used"), + ("FPS", "Unlimited", "Optimized"), + ("Render", "12 chunks", "Balanced"), + ] + + for metric in metrics: + m_layout = QVBoxLayout() + m_layout.setSpacing(4) + + name_label = QLabel(metric[0]) + name_label.setStyleSheet(""" + font-size: 12px; + color: #666666; + """) + m_layout.addWidget(name_label) + + value_label = QLabel(metric[1]) + value_label.setStyleSheet(""" + font-size: 18px; + color: #FFFFFF; + font-weight: 600; + """) + m_layout.addWidget(value_label) + + desc_label = QLabel(metric[2]) + desc_label.setStyleSheet(""" + font-size: 11px; + color: #888888; + """) + m_layout.addWidget(desc_label) + + metrics_layout.addLayout(m_layout) + + layout.addLayout(metrics_layout) + + return card + + def update_play_button_state(self, state: str): + """Update play button state (ready, launching, playing)""" + + states = { + "ready": ("PLAY", "#playButton", True), + "launching": ("LAUNCHING...", "#playButton", False), + "playing": ("GAME RUNNING", "#playButton", False), + } + + if state in states: + text, style, enabled = states[state] + self.play_button.setText(text) + self.play_button.setEnabled(enabled) diff --git a/nela_launcher/pages/login_page.py b/nela_launcher/pages/login_page.py new file mode 100644 index 0000000..7c959c0 --- /dev/null +++ b/nela_launcher/pages/login_page.py @@ -0,0 +1,230 @@ +""" +Login Page +Microsoft authentication and sign-in flow +""" + +from PySide6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, + QFrame, QLineEdit, QSpacerItem, QSizePolicy, QApplication +) +from PySide6.QtCore import Qt, Signal, QTimer +from PySide6.QtGui import QFont + + +class LoginPage(QWidget): + """Premium login page with Microsoft authentication""" + + login_requested = Signal(str) # auth_type + skip_clicked = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + self.setStyleSheet(""" + QWidget { + background-color: #0A0A0A; + } + """) + self._setup_ui() + + def _setup_ui(self): + """Setup login page UI""" + + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(80, 60, 80, 60) + main_layout.setSpacing(40) + + # Top section - Title + top_section = QFrame() + top_layout = QVBoxLayout(top_section) + top_layout.setAlignment(Qt.AlignCenter) + top_layout.setSpacing(16) + + # Heading + heading_label = QLabel("Sign In") + heading_label.setObjectName("heading") + heading_label.setStyleSheet(""" + font-size: 48px; + font-weight: 700; + color: #FFFFFF; + letter-spacing: 0.5px; + """) + heading_label.setAlignment(Qt.AlignCenter) + top_layout.addWidget(heading_label) + + # Subtitle + subtitle_label = QLabel("Sign in with your Microsoft account to access all features") + subtitle_label.setStyleSheet(""" + font-size: 16px; + color: #888888; + """) + subtitle_label.setAlignment(Qt.AlignCenter) + top_layout.addWidget(subtitle_label) + + main_layout.addWidget(top_section) + + # Middle section - Login options + middle_section = QFrame() + middle_layout = QVBoxLayout(middle_section) + middle_layout.setSpacing(20) + middle_layout.setAlignment(Qt.AlignCenter) + + # Microsoft login button + self.microsoft_btn = QPushButton() + self.microsoft_btn.setMinimumSize(400, 64) + self.microsoft_btn.setStyleSheet(""" + QPushButton { + background-color: #FFFFFF; + color: #1A1A1A; + border: none; + border-radius: 12px; + font-size: 16px; + font-weight: 600; + padding: 20px 32px; + } + QPushButton:hover { + background-color: #F0F0F0; + } + QPushButton:pressed { + background-color: #E0E0E0; + } + """) + + # Microsoft button content layout + ms_layout = QHBoxLayout(self.microsoft_btn) + ms_layout.setContentsMargins(20, 0, 20, 0) + ms_layout.setSpacing(16) + + # Microsoft icon (simplified) + ms_icon = QLabel("▣") + ms_icon.setStyleSheet(""" + font-size: 28px; + color: #00A4EF; + background-color: transparent; + """) + ms_layout.addWidget(ms_icon) + + # Button text + ms_text = QLabel("Sign in with Microsoft") + ms_text.setStyleSheet(""" + font-size: 16px; + font-weight: 600; + color: #1A1A1A; + background-color: transparent; + """) + ms_layout.addWidget(ms_text) + + ms_layout.addStretch() + + self.microsoft_btn.clicked.connect(lambda: self.login_requested.emit("microsoft")) + middle_layout.addWidget(self.microsoft_btn) + + # Divider + divider_frame = QFrame() + divider_frame.setFixedHeight(40) + divider_layout = QHBoxLayout(divider_frame) + divider_layout.setAlignment(Qt.AlignCenter) + + line1 = QFrame() + line1.setFixedHeight(1) + line1.setStyleSheet("background-color: #1F1F1F;") + divider_layout.addWidget(line1) + + or_label = QLabel("OR") + or_label.setStyleSheet(""" + color: #666666; + font-size: 12px; + background-color: transparent; + padding: 0 16px; + """) + divider_layout.addWidget(or_label) + + line2 = QFrame() + line2.setFixedHeight(1) + line2.setStyleSheet("background-color: #1F1F1F;") + divider_layout.addWidget(line2) + + middle_layout.addWidget(divider_frame) + + # Offline mode button + offline_btn = QPushButton("Continue Offline") + offline_btn.setObjectName("secondaryButton") + offline_btn.setMinimumSize(400, 56) + offline_btn.setStyleSheet(""" + QPushButton#secondaryButton { + background-color: transparent; + border: 1px solid #2A2A2A; + color: #A0A0A0; + border-radius: 12px; + font-size: 15px; + font-weight: 500; + } + QPushButton#secondaryButton:hover { + border-color: #3A3A3A; + color: #FFFFFF; + } + """) + offline_btn.clicked.connect(self.skip_clicked) + middle_layout.addWidget(offline_btn) + + main_layout.addWidget(middle_section) + + # Spacer + spacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) + main_layout.addItem(spacer) + + # Bottom section - Info + bottom_section = QFrame() + bottom_layout = QVBoxLayout(bottom_section) + bottom_layout.setAlignment(Qt.AlignCenter) + bottom_layout.setSpacing(12) + + # Features list + features_label = QLabel("Signing in unlocks:") + features_label.setStyleSheet(""" + color: #666666; + font-size: 14px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 1px; + """) + features_label.setAlignment(Qt.AlignCenter) + bottom_layout.addWidget(features_label) + + feature_list = QLabel("• Skin synchronization\n• Cloud saves\n• Multiplayer access\n• Friend system") + feature_list.setStyleSheet(""" + color: #888888; + font-size: 14px; + line-height: 28px; + """) + feature_list.setAlignment(Qt.AlignCenter) + bottom_layout.addWidget(feature_list) + + main_layout.addWidget(bottom_section) + + # Status label (hidden by default) + self.status_label = QLabel("") + self.status_label.setStyleSheet(""" + color: #FF6B35; + font-size: 14px; + """) + self.status_label.setAlignment(Qt.AlignCenter) + self.status_label.setWordWrap(True) + main_layout.addWidget(self.status_label) + + def set_status(self, message: str, is_error: bool = False): + """Set status message""" + self.status_label.setText(message) + if is_error: + self.status_label.setStyleSheet(""" + color: #EF4444; + font-size: 14px; + """) + else: + self.status_label.setStyleSheet(""" + color: #10B981; + font-size: 14px; + """) + + def clear_status(self): + """Clear status message""" + self.status_label.setText("") diff --git a/nela_launcher/pages/splash_page.py b/nela_launcher/pages/splash_page.py new file mode 100644 index 0000000..28cc4fc --- /dev/null +++ b/nela_launcher/pages/splash_page.py @@ -0,0 +1,132 @@ +""" +Splash Page +Premium animated splash screen with Nela logo +""" + +from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QFrame +from PySide6.QtCore import Qt, QTimer, QPropertyAnimation, QEasingCurve +from PySide6.QtGui import QFont + + +class SplashPage(QWidget): + """Premium splash screen with animated logo""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setStyleSheet(""" + QWidget { + background-color: #0A0A0A; + } + """) + self._setup_ui() + self._start_animations() + + def _setup_ui(self): + """Setup splash screen UI""" + + layout = QVBoxLayout(self) + layout.setAlignment(Qt.AlignCenter) + layout.setSpacing(40) + + # Logo container + logo_container = QFrame() + logo_container.setStyleSheet("background-color: transparent;") + logo_layout = QVBoxLayout(logo_container) + logo_layout.setAlignment(Qt.AlignCenter) + logo_layout.setSpacing(20) + + # Main logo mark - stylized N + self.logo_label = QLabel("◈") + self.logo_label.setStyleSheet(""" + font-size: 120px; + color: #FF6B35; + font-weight: bold; + background-color: transparent; + """) + self.logo_label.setAlignment(Qt.AlignCenter) + logo_layout.addWidget(self.logo_label) + + # Brand name + brand_label = QLabel("NELA") + brand_label.setStyleSheet(""" + font-size: 48px; + font-weight: 700; + color: #FFFFFF; + letter-spacing: 8px; + background-color: transparent; + """) + brand_label.setAlignment(Qt.AlignCenter) + logo_layout.addWidget(brand_label) + + # Tagline + tagline_label = QLabel("LAUNCHER") + tagline_label.setStyleSheet(""" + font-size: 16px; + color: #666666; + letter-spacing: 4px; + text-transform: uppercase; + background-color: transparent; + """) + tagline_label.setAlignment(Qt.AlignCenter) + logo_layout.addWidget(tagline_label) + + layout.addWidget(logo_container) + + # Loading indicator + loading_container = QFrame() + loading_layout = QVBoxLayout(loading_container) + loading_layout.setAlignment(Qt.AlignCenter) + loading_layout.setSpacing(16) + + # Animated dots + self.dots_label = QLabel("● ● ●") + self.dots_label.setStyleSheet(""" + font-size: 24px; + color: #FF6B35; + background-color: transparent; + letter-spacing: 8px; + """) + self.dots_label.setAlignment(Qt.AlignCenter) + loading_layout.addWidget(self.dots_label) + + # Version info + version_label = QLabel("v1.0.0") + version_label.setStyleSheet(""" + font-size: 12px; + color: #444444; + background-color: transparent; + """) + version_label.setAlignment(Qt.AlignCenter) + loading_layout.addWidget(version_label) + + layout.addWidget(loading_container) + + def _start_animations(self): + """Start splash screen animations""" + + # Fade in animation for logo + self.fade_animation = QPropertyAnimation(self.logo_label, b"opacity") + self.fade_animation.setDuration(1500) + self.fade_animation.setStartValue(0.0) + self.fade_animation.setEndValue(1.0) + self.fade_animation.setEasingCurve(QEasingCurve.OutCubic) + self.fade_animation.start() + + # Pulse animation for dots + self.dot_timer = QTimer() + self.dot_timer.timeout.connect(self._animate_dots) + self.dot_timer.start(300) + + self._dot_phase = 0 + + def _animate_dots(self): + """Animate loading dots""" + self._dot_phase = (self._dot_phase + 1) % 3 + + dot_states = [ + "● ○ ○", + "○ ● ○", + "○ ○ ●" + ] + + self.dots_label.setText(dot_states[self._dot_phase]) diff --git a/nela_launcher/pages/welcome_page.py b/nela_launcher/pages/welcome_page.py new file mode 100644 index 0000000..74abfd3 --- /dev/null +++ b/nela_launcher/pages/welcome_page.py @@ -0,0 +1,213 @@ +""" +Welcome Page +Onboarding and welcome screen +""" + +from PySide6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, + QFrame, QSpacerItem, QSizePolicy +) +from PySide6.QtCore import Qt, Signal + + +class WelcomePage(QWidget): + """Premium welcome/onboarding page""" + + get_started_clicked = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + self.setStyleSheet(""" + QWidget { + background-color: #0A0A0A; + } + """) + self._setup_ui() + + def _setup_ui(self): + """Setup welcome page UI""" + + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(80, 60, 80, 60) + main_layout.setSpacing(40) + + # Top section - Welcome message + top_section = QFrame() + top_layout = QVBoxLayout(top_section) + top_layout.setAlignment(Qt.AlignCenter) + top_layout.setSpacing(20) + + # Heading + heading_label = QLabel("Welcome to Nela") + heading_label.setObjectName("heading") + heading_label.setStyleSheet(""" + font-size: 56px; + font-weight: 700; + color: #FFFFFF; + letter-spacing: 1px; + """) + heading_label.setAlignment(Qt.AlignCenter) + top_layout.addWidget(heading_label) + + # Subtitle + subtitle_label = QLabel("Minimal outside. Powerful inside.") + subtitle_label.setStyleSheet(""" + font-size: 20px; + color: #A0A0A0; + font-weight: 400; + """) + subtitle_label.setAlignment(Qt.AlignCenter) + top_layout.addWidget(subtitle_label) + + main_layout.addWidget(top_section) + + # Middle section - Feature highlights + features_section = QFrame() + features_layout = QHBoxLayout(features_section) + features_layout.setSpacing(24) + + feature_cards = [ + { + "icon": "⚡", + "title": "Built for Fabric", + "desc": "Optimized exclusively for Minecraft 1.16.5 Fabric" + }, + { + "icon": "◫", + "title": "Curated Mods", + "desc": "30+ premium mods for performance and quality of life" + }, + { + "icon": "☺", + "title": "Skin Studio", + "desc": "Preview and customize your Minecraft skin in 3D" + }, + { + "icon": "🎯", + "title": "Performance", + "desc": "Advanced optimization tools and presets" + } + ] + + for feature in feature_cards: + card = self._create_feature_card(feature) + features_layout.addWidget(card) + + main_layout.addWidget(features_section) + + # Spacer + spacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) + main_layout.addItem(spacer) + + # Bottom section - CTA buttons + bottom_section = QFrame() + bottom_layout = QHBoxLayout(bottom_section) + bottom_layout.setAlignment(Qt.AlignCenter) + bottom_layout.setSpacing(20) + + # Get Started button (primary) + self.get_started_btn = QPushButton("Get Started") + self.get_started_btn.setObjectName("primaryButton") + self.get_started_btn.setMinimumSize(200, 56) + self.get_started_btn.setStyleSheet(""" + QPushButton#primaryButton { + background-color: #FF6B35; + color: #FFFFFF; + border: none; + border-radius: 12px; + font-size: 16px; + font-weight: 600; + letter-spacing: 0.5px; + } + QPushButton#primaryButton:hover { + background-color: #FF7B4D; + } + QPushButton#primaryButton:pressed { + background-color: #E55A2B; + } + """) + self.get_started_btn.clicked.connect(self._on_get_started) + bottom_layout.addWidget(self.get_started_btn) + + # Learn More button (secondary) + learn_more_btn = QPushButton("Learn More") + learn_more_btn.setObjectName("secondaryButton") + learn_more_btn.setMinimumSize(200, 56) + learn_more_btn.setStyleSheet(""" + QPushButton#secondaryButton { + background-color: transparent; + border: 1px solid #FF6B35; + color: #FF6B35; + border-radius: 12px; + font-size: 16px; + font-weight: 600; + letter-spacing: 0.5px; + } + QPushButton#secondaryButton:hover { + background-color: rgba(255, 107, 53, 0.1); + } + """) + bottom_layout.addWidget(learn_more_btn) + + main_layout.addWidget(bottom_section) + + def _create_feature_card(self, feature: dict) -> QFrame: + """Create a feature highlight card""" + + card = QFrame() + card.setObjectName("card") + card.setMinimumWidth(240) + card.setMaximumWidth(280) + card.setStyleSheet(""" + QFrame#card { + background-color: #111111; + border: 1px solid #1F1F1F; + border-radius: 16px; + padding: 32px 24px; + } + QFrame#card:hover { + border-color: #2A2A2A; + background-color: #141414; + } + """) + + layout = QVBoxLayout(card) + layout.setSpacing(16) + layout.setAlignment(Qt.AlignCenter) + + # Icon + icon_label = QLabel(feature["icon"]) + icon_label.setStyleSheet(""" + font-size: 48px; + background-color: transparent; + """) + icon_label.setAlignment(Qt.AlignCenter) + layout.addWidget(icon_label) + + # Title + title_label = QLabel(feature["title"]) + title_label.setStyleSheet(""" + font-size: 18px; + font-weight: 600; + color: #FFFFFF; + background-color: transparent; + """) + title_label.setAlignment(Qt.AlignCenter) + layout.addWidget(title_label) + + # Description + desc_label = QLabel(feature["desc"]) + desc_label.setStyleSheet(""" + font-size: 14px; + color: #888888; + background-color: transparent; + """) + desc_label.setAlignment(Qt.AlignCenter) + desc_label.setWordWrap(True) + layout.addWidget(desc_label) + + return card + + def _on_get_started(self): + """Handle get started button click""" + self.get_started_clicked.emit() diff --git a/nela_launcher/requirements.txt b/nela_launcher/requirements.txt new file mode 100644 index 0000000..e408d4d --- /dev/null +++ b/nela_launcher/requirements.txt @@ -0,0 +1,5 @@ +PySide6>=6.7.0 +requests>=2.31.0 +aiohttp>=3.9.0 +Pillow>=10.2.0 +numpy>=1.26.0 diff --git a/nela_launcher/services/__init__.py b/nela_launcher/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nela_launcher/styles/__init__.py b/nela_launcher/styles/__init__.py new file mode 100644 index 0000000..4c50b4b --- /dev/null +++ b/nela_launcher/styles/__init__.py @@ -0,0 +1,8 @@ +""" +Nela Launcher Styles Module +Contains styling system and themes +""" + +from .stylesheet import get_main_stylesheet + +__all__ = ['get_main_stylesheet'] diff --git a/nela_launcher/styles/stylesheet.py b/nela_launcher/styles/stylesheet.py new file mode 100644 index 0000000..225be87 --- /dev/null +++ b/nela_launcher/styles/stylesheet.py @@ -0,0 +1,701 @@ +""" +Nela Launcher Stylesheet +Premium Nothing OS-inspired styling system +""" + + +def get_main_stylesheet(): + """Return the main application stylesheet""" + + return """ + /* ============================================ + NELLA LAUNCHER - PREMIUM STYLESHEET + Nothing OS Inspired Industrial Minimalism + ============================================ */ + + /* --- Global Variables --- */ + QWidget { + font-family: 'Inter', 'Segoe UI', Arial, sans-serif; + font-size: 14px; + color: #E8E8E8; + background-color: #0A0A0A; + } + + /* --- Main Window --- */ + QMainWindow { + background-color: #0A0A0A; + } + + /* --- Title Bar --- */ + #titleBar { + background-color: #111111; + border-bottom: 1px solid #1F1F1F; + min-height: 48px; + max-height: 48px; + } + + #titleLabel { + font-size: 16px; + font-weight: 600; + color: #FFFFFF; + letter-spacing: 0.5px; + } + + #windowControls { + spacing: 8px; + } + + /* --- Buttons --- */ + QPushButton { + background-color: #1F1F1F; + color: #FFFFFF; + border: 1px solid #2A2A2A; + border-radius: 8px; + padding: 12px 24px; + font-weight: 500; + font-size: 14px; + min-height: 44px; + } + + QPushButton:hover { + background-color: #2A2A2A; + border-color: #3A3A3A; + } + + QPushButton:pressed { + background-color: #1A1A1A; + } + + QPushButton:disabled { + background-color: #151515; + color: #666666; + border-color: #1F1F1F; + } + + /* Primary Button */ + QPushButton#primaryButton { + background-color: #FF6B35; + color: #FFFFFF; + border: none; + font-weight: 600; + } + + QPushButton#primaryButton:hover { + background-color: #FF7B4D; + } + + QPushButton#primaryButton:pressed { + background-color: #E55A2B; + } + + /* Secondary Button */ + QPushButton#secondaryButton { + background-color: transparent; + border: 1px solid #FF6B35; + color: #FF6B35; + } + + QPushButton#secondaryButton:hover { + background-color: rgba(255, 107, 53, 0.1); + } + + /* Play Button */ + QPushButton#playButton { + background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #FF6B35, stop:1 #FF8C5A); + border: none; + border-radius: 12px; + font-size: 18px; + font-weight: 700; + min-height: 64px; + letter-spacing: 1px; + } + + QPushButton#playButton:hover { + background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #FF7B4D, stop:1 #FF9C6A); + } + + QPushButton#playButton:pressed { + background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #E55A2B, stop:1 #E57A4A); + } + + /* --- Sidebar --- */ + #sidebar { + background-color: #0F0F0F; + border-right: 1px solid #1F1F1F; + min-width: 260px; + max-width: 260px; + } + + #sidebarTop { + background-color: #0F0F0F; + border-bottom: 1px solid #1F1F1F; + min-height: 80px; + max-height: 80px; + } + + #sidebarNav { + background-color: #0F0F0F; + spacing: 4px; + } + + #sidebarBottom { + background-color: #0F0F0F; + border-top: 1px solid #1F1F1F; + min-height: 100px; + max-height: 100px; + } + + /* Navigation Buttons */ + QPushButton#navButton { + background-color: transparent; + border: none; + border-radius: 8px; + padding: 14px 20px; + text-align: left; + font-weight: 500; + color: #A0A0A0; + min-height: 52px; + } + + QPushButton#navButton:hover { + background-color: #1A1A1A; + color: #FFFFFF; + } + + QPushButton#navButton:checked { + background-color: #1F1F1F; + color: #FF6B35; + font-weight: 600; + } + + QPushButton#navButton::indicator { + width: 0px; + } + + /* --- Cards --- */ + QFrame#card { + background-color: #111111; + border: 1px solid #1F1F1F; + border-radius: 12px; + padding: 20px; + } + + QFrame#card:hover { + border-color: #2A2A2A; + background-color: #141414; + } + + QFrame#highlightCard { + background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #111111, stop:1 #161616); + border: 1px solid #2A2A2A; + border-radius: 12px; + padding: 24px; + } + + /* --- Labels --- */ + QLabel { + color: #E8E8E8; + background-color: transparent; + } + + QLabel#heading { + font-size: 28px; + font-weight: 700; + color: #FFFFFF; + letter-spacing: 0.5px; + } + + QLabel#subheading { + font-size: 18px; + font-weight: 600; + color: #FFFFFF; + } + + QLabel#body { + font-size: 14px; + color: #A0A0A0; + } + + QLabel#caption { + font-size: 12px; + color: #666666; + text-transform: uppercase; + letter-spacing: 1px; + } + + QLabel#accent { + color: #FF6B35; + font-weight: 600; + } + + /* --- Input Fields --- */ + QLineEdit { + background-color: #111111; + border: 1px solid #2A2A2A; + border-radius: 8px; + padding: 12px 16px; + color: #FFFFFF; + selection-background-color: #FF6B35; + selection-color: #FFFFFF; + } + + QLineEdit:hover { + border-color: #3A3A3A; + } + + QLineEdit:focus { + border-color: #FF6B35; + } + + QLineEdit:disabled { + background-color: #0D0D0D; + color: #666666; + } + + /* --- Text Edit --- */ + QTextEdit { + background-color: #111111; + border: 1px solid #2A2A2A; + border-radius: 8px; + padding: 12px; + color: #E8E8E8; + selection-background-color: #FF6B35; + selection-color: #FFFFFF; + } + + QTextEdit:hover { + border-color: #3A3A3A; + } + + QTextEdit:focus { + border-color: #FF6B35; + } + + /* --- Scroll Area --- */ + QScrollArea { + border: none; + background-color: transparent; + } + + QScrollBar:vertical { + background-color: #0A0A0A; + width: 8px; + border-radius: 4px; + margin: 0; + } + + QScrollBar::handle:vertical { + background-color: #2A2A2A; + border-radius: 4px; + min-height: 30px; + } + + QScrollBar::handle:vertical:hover { + background-color: #3A3A3A; + } + + QScrollBar::add-line:vertical, + QScrollBar::sub-line:vertical { + height: 0px; + } + + QScrollBar::add-page:vertical, + QScrollBar::sub-page:vertical { + background: none; + } + + QScrollBar:horizontal { + background-color: #0A0A0A; + height: 8px; + border-radius: 4px; + margin: 0; + } + + QScrollBar::handle:horizontal { + background-color: #2A2A2A; + border-radius: 4px; + min-width: 30px; + } + + QScrollBar::handle:horizontal:hover { + background-color: #3A3A3A; + } + + QScrollBar::add-line:horizontal, + QScrollBar::sub-line:horizontal { + width: 0px; + } + + /* --- Combo Box --- */ + QComboBox { + background-color: #111111; + border: 1px solid #2A2A2A; + border-radius: 8px; + padding: 12px 16px; + color: #FFFFFF; + min-height: 44px; + } + + QComboBox:hover { + border-color: #3A3A3A; + } + + QComboBox:focus { + border-color: #FF6B35; + } + + QComboBox::drop-down { + border: none; + width: 24px; + padding-right: 8px; + } + + QComboBox::down-arrow { + image: none; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 6px solid #A0A0A0; + margin-right: 8px; + } + + QComboBox QAbstractItemView { + background-color: #111111; + border: 1px solid #2A2A2A; + border-radius: 8px; + selection-background-color: #1F1F1F; + selection-color: #FFFFFF; + outline: none; + padding: 8px; + } + + QComboBox QAbstractItemView::item { + min-height: 40px; + padding: 8px; + border-radius: 6px; + } + + QComboBox QAbstractItemView::item:hover { + background-color: #1F1F1F; + } + + QComboBox QAbstractItemView::item:selected { + background-color: #2A2A2A; + } + + /* --- Slider --- */ + QSlider::groove:horizontal { + background-color: #1F1F1F; + height: 6px; + border-radius: 3px; + } + + QSlider::handle:horizontal { + background-color: #FF6B35; + width: 18px; + height: 18px; + margin: -6px 0; + border-radius: 9px; + } + + QSlider::handle:horizontal:hover { + background-color: #FF7B4D; + } + + QSlider::sub-page:horizontal { + background-color: #FF6B35; + border-radius: 3px; + } + + QSlider::add-page:horizontal { + background-color: #1F1F1F; + border-radius: 3px; + } + + /* --- Checkbox --- */ + QCheckBox { + color: #E8E8E8; + spacing: 12px; + } + + QCheckBox::indicator { + width: 22px; + height: 22px; + border-radius: 6px; + border: 1px solid #2A2A2A; + background-color: #111111; + } + + QCheckBox::indicator:hover { + border-color: #3A3A3A; + } + + QCheckBox::indicator:checked { + background-color: #FF6B35; + border-color: #FF6B35; + } + + /* --- Tab Widget --- */ + QTabWidget::pane { + background-color: #0A0A0A; + border: none; + border-radius: 12px; + } + + QTabBar::tab { + background-color: #111111; + border: 1px solid #1F1F1F; + border-bottom: none; + border-top-left-radius: 8px; + border-top-right-radius: 8px; + padding: 12px 24px; + color: #A0A0A0; + margin-right: 4px; + } + + QTabBar::tab:hover { + background-color: #1A1A1A; + color: #FFFFFF; + } + + QTabBar::tab:selected { + background-color: #1F1F1F; + color: #FF6B35; + border-color: #2A2A2A; + } + + /* --- Progress Bar --- */ + QProgressBar { + background-color: #1F1F1F; + border-radius: 8px; + height: 8px; + text-align: center; + border: none; + } + + QProgressBar::chunk { + background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #FF6B35, stop:1 #FF8C5A); + border-radius: 8px; + } + + /* --- List Widget --- */ + QListWidget { + background-color: transparent; + border: none; + outline: none; + } + + QListWidget::item { + background-color: transparent; + border-radius: 8px; + padding: 12px; + margin: 2px 0; + } + + QListWidget::item:hover { + background-color: #1A1A1A; + } + + QListWidget::item:selected { + background-color: #1F1F1F; + border-left: 3px solid #FF6B35; + } + + /* --- Table Widget --- */ + QTableWidget { + background-color: #111111; + border: 1px solid #1F1F1F; + border-radius: 12px; + gridline-color: #1F1F1F; + selection-background-color: #1F1F1F; + selection-color: #FFFFFF; + } + + QTableWidget::item { + padding: 12px; + } + + QTableWidget::item:hover { + background-color: #1A1A1A; + } + + QHeaderView::section { + background-color: #0F0F0F; + border: none; + border-bottom: 1px solid #1F1F1F; + padding: 12px; + color: #666666; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + /* --- Group Box --- */ + QGroupBox { + background-color: #111111; + border: 1px solid #1F1F1F; + border-radius: 12px; + margin-top: 12px; + padding-top: 20px; + font-weight: 600; + color: #FFFFFF; + } + + QGroupBox::title { + subcontrol-origin: margin; + subcontrol-position: top left; + left: 20px; + padding: 0 12px; + color: #FF6B35; + } + + /* --- Tool Tip --- */ + QToolTip { + background-color: #1F1F1F; + color: #FFFFFF; + border: 1px solid #2A2A2A; + border-radius: 6px; + padding: 8px 12px; + font-size: 13px; + } + + /* --- Menu --- */ + QMenu { + background-color: #111111; + border: 1px solid #2A2A2A; + border-radius: 8px; + padding: 8px; + } + + QMenu::item { + padding: 10px 20px; + border-radius: 6px; + color: #E8E8E8; + } + + QMenu::item:selected { + background-color: #1F1F1F; + color: #FF6B35; + } + + QMenu::separator { + height: 1px; + background-color: #1F1F1F; + margin: 8px 0; + } + + /* --- Spin Box --- */ + QSpinBox { + background-color: #111111; + border: 1px solid #2A2A2A; + border-radius: 8px; + padding: 10px 12px; + color: #FFFFFF; + min-height: 40px; + } + + QSpinBox:hover { + border-color: #3A3A3A; + } + + QSpinBox:focus { + border-color: #FF6B35; + } + + QSpinBox::up-button, + QSpinBox::down-button { + border: none; + width: 20px; + subcontrol-origin: border; + } + + QSpinBox::up-button:hover, + QSpinBox::down-button:hover { + background-color: #1F1F1F; + } + + /* --- Stack Widget --- */ + QStackedWidget { + background-color: transparent; + } + + /* --- Frame --- */ + QFrame { + background-color: transparent; + } + + QFrame#separator { + background-color: #1F1F1F; + min-height: 1px; + max-height: 1px; + } + + QFrame#dotGrid { + background-color: transparent; + } + + /* --- Status Indicators --- */ + QFrame#statusOnline { + background-color: #10B981; + border-radius: 6px; + min-width: 8px; + max-width: 8px; + min-height: 8px; + max-height: 8px; + } + + QFrame#statusOffline { + background-color: #EF4444; + border-radius: 6px; + min-width: 8px; + max-width: 8px; + min-height: 8px; + max-height: 8px; + } + + QFrame#statusWarning { + background-color: #F59E0B; + border-radius: 6px; + min-width: 8px; + max-width: 8px; + min-height: 8px; + max-height: 8px; + } + + /* --- Badge --- */ + QLabel#badge { + background-color: #1F1F1F; + color: #A0A0A0; + border-radius: 12px; + padding: 4px 12px; + font-size: 12px; + font-weight: 600; + } + + QLabel#badgeSuccess { + background-color: rgba(16, 185, 129, 0.2); + color: #10B981; + } + + QLabel#badgeWarning { + background-color: rgba(245, 158, 11, 0.2); + color: #F59E0B; + } + + QLabel#badgeError { + background-color: rgba(239, 68, 68, 0.2); + color: #EF4444; + } + + QLabel#badgeAccent { + background-color: rgba(255, 107, 53, 0.2); + color: #FF6B35; + } + + /* --- Animations hint (for reference) --- */ + /* Use QPropertyAnimation in Python for smooth transitions */ + """ diff --git a/nela_launcher/utils/__init__.py b/nela_launcher/utils/__init__.py new file mode 100644 index 0000000..41db00b --- /dev/null +++ b/nela_launcher/utils/__init__.py @@ -0,0 +1,8 @@ +""" +Nela Launcher Utils Module +Utility functions and helpers +""" + +from .logger import setup_logger, get_logger + +__all__ = ['setup_logger', 'get_logger'] diff --git a/nela_launcher/utils/logger.py b/nela_launcher/utils/logger.py new file mode 100644 index 0000000..71d0909 --- /dev/null +++ b/nela_launcher/utils/logger.py @@ -0,0 +1,73 @@ +""" +Nela Launcher Logger +Setup and configure application logging +""" + +import logging +from pathlib import Path +from datetime import datetime + + +def setup_logger(log_dir: str = "logs", level: int = logging.INFO) -> logging.Logger: + """ + Setup and configure the application logger + + Args: + log_dir: Directory to store log files + level: Logging level + + Returns: + Configured logger instance + """ + + # Create logs directory + log_path = Path(log_dir) + log_path.mkdir(exist_ok=True) + + # Create logger + logger = logging.getLogger("nela_launcher") + logger.setLevel(level) + + # Clear existing handlers + logger.handlers.clear() + + # Create formatter + formatter = logging.Formatter( + '%(asctime)s | %(levelname)-8s | %(name)s | %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + # Console handler + console_handler = logging.StreamHandler() + console_handler.setLevel(level) + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) + + # File handler - current session + session_log = log_path / f"session_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log" + file_handler = logging.FileHandler(session_log, encoding='utf-8') + file_handler.setLevel(level) + file_handler.setFormatter(formatter) + logger.addHandler(file_handler) + + # File handler - latest (always updated) + latest_log = log_path / "latest.log" + latest_handler = logging.FileHandler(latest_log, encoding='utf-8', mode='w') + latest_handler.setLevel(level) + latest_handler.setFormatter(formatter) + logger.addHandler(latest_handler) + + return logger + + +def get_logger(name: str = "nela_launcher") -> logging.Logger: + """ + Get a logger instance + + Args: + name: Logger name + + Returns: + Logger instance + """ + return logging.getLogger(name) diff --git a/nela_launcher/widgets/__init__.py b/nela_launcher/widgets/__init__.py new file mode 100644 index 0000000..61168fb --- /dev/null +++ b/nela_launcher/widgets/__init__.py @@ -0,0 +1,9 @@ +""" +Nela Launcher Widgets Module +Reusable UI components +""" + +from .title_bar import TitleBar +from .sidebar import Sidebar + +__all__ = ['TitleBar', 'Sidebar'] diff --git a/nela_launcher/widgets/sidebar.py b/nela_launcher/widgets/sidebar.py new file mode 100644 index 0000000..9063a8b --- /dev/null +++ b/nela_launcher/widgets/sidebar.py @@ -0,0 +1,171 @@ +""" +Sidebar Navigation Widget +Premium sidebar with navigation buttons and user profile +""" + +from PySide6.QtWidgets import QWidget, QVBoxLayout, QPushButton, QLabel, QFrame, QSpacerItem, QSizePolicy +from PySide6.QtCore import Qt, Signal + + +class Sidebar(QWidget): + """Premium sidebar navigation component""" + + navigation_requested = Signal(str) # page_name + + def __init__(self, parent=None): + super().__init__(parent) + self.setObjectName("sidebar") + self.setFixedWidth(260) + self._setup_ui() + self._current_page = "home" + + def _setup_ui(self): + """Setup sidebar UI""" + + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(16, 16, 16, 16) + main_layout.setSpacing(8) + + # Top section - Logo and brand + top_section = QFrame() + top_section.setObjectName("sidebarTop") + top_layout = QVBoxLayout(top_section) + top_layout.setContentsMargins(12, 20, 12, 20) + top_layout.setSpacing(8) + + # Logo mark + logo_label = QLabel("◈") + logo_label.setStyleSheet(""" + font-size: 32px; + color: #FF6B35; + font-weight: bold; + """) + logo_label.setAlignment(Qt.AlignCenter) + top_layout.addWidget(logo_label) + + # Brand name + brand_label = QLabel("NELA") + brand_label.setStyleSheet(""" + font-size: 20px; + font-weight: 700; + color: #FFFFFF; + letter-spacing: 2px; + """) + brand_label.setAlignment(Qt.AlignCenter) + top_layout.addWidget(brand_label) + + main_layout.addWidget(top_section) + + # Navigation section + nav_section = QFrame() + nav_section.setObjectName("sidebarNav") + nav_layout = QVBoxLayout(nav_section) + nav_layout.setContentsMargins(8, 8, 8, 8) + nav_layout.setSpacing(4) + + # Navigation buttons + self.nav_buttons = {} + + nav_items = [ + ("home", "Home", "⌂"), + ("play", "Play", "▶"), + ("profiles", "Profiles", "▤"), + ("mods", "Mods", "◫"), + ("skins", "Skin Studio", "☺"), + ("performance", "Performance", "⚡"), + ("updates", "Updates", "↻"), + ("settings", "Settings", "⚙"), + ("logs", "Logs", "📋"), + ("about", "About", "ℹ"), + ] + + for page_id, label_text, icon in nav_items: + btn = QPushButton(f"{icon} {label_text}") + btn.setObjectName("navButton") + btn.setCheckable(True) + btn.clicked.connect(lambda checked, pid=page_id: self._on_nav_clicked(pid)) + btn.setStyleSheet(""" + QPushButton#navButton { + background-color: transparent; + border: none; + border-radius: 8px; + padding: 14px 16px; + text-align: left; + font-weight: 500; + color: #A0A0A0; + min-height: 48px; + font-size: 14px; + } + QPushButton#navButton:hover { + background-color: #1A1A1A; + color: #FFFFFF; + } + QPushButton#navButton:checked { + background-color: #1F1F1F; + color: #FF6B35; + font-weight: 600; + } + """) + + nav_layout.addWidget(btn) + self.nav_buttons[page_id] = btn + + # Add spacer to push bottom section down + spacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) + nav_layout.addItem(spacer) + + main_layout.addWidget(nav_section) + + # Bottom section - User profile + bottom_section = QFrame() + bottom_section.setObjectName("sidebarBottom") + bottom_layout = QVBoxLayout(bottom_section) + bottom_layout.setContentsMargins(12, 12, 12, 12) + bottom_layout.setSpacing(12) + + # Separator + separator = QFrame() + separator.setObjectName("separator") + separator.setFixedHeight(1) + bottom_layout.addWidget(separator) + + # User info + user_label = QLabel("Not signed in") + user_label.setStyleSheet(""" + font-size: 14px; + font-weight: 600; + color: #FFFFFF; + """) + bottom_layout.addWidget(user_label) + + status_label = QLabel("Sign in to access features") + status_label.setStyleSheet(""" + font-size: 12px; + color: #666666; + """) + bottom_layout.addWidget(status_label) + + main_layout.addWidget(bottom_section) + + def _on_nav_clicked(self, page_id: str): + """Handle navigation button click""" + self._set_active_page(page_id) + self.navigation_requested.emit(page_id) + + def _set_active_page(self, page_id: str): + """Set the active navigation page""" + self._current_page = page_id + + # Update button states + for pid, btn in self.nav_buttons.items(): + btn.setChecked(pid == page_id) + + def get_current_page(self) -> str: + """Get current active page""" + return self._current_page + + def set_user_info(self, username: str, is_logged_in: bool = True): + """Update user info in sidebar""" + # Find the bottom section and update labels + # This is a simplified version - would need proper references in full implementation + pass diff --git a/nela_launcher/widgets/title_bar.py b/nela_launcher/widgets/title_bar.py new file mode 100644 index 0000000..250f3b4 --- /dev/null +++ b/nela_launcher/widgets/title_bar.py @@ -0,0 +1,161 @@ +""" +Custom Title Bar Widget +Premium frameless window title bar with custom controls +""" + +from PySide6.QtWidgets import QWidget, QHBoxLayout, QLabel, QPushButton, QSpacerItem, QSizePolicy +from PySide6.QtCore import Qt, Signal +from PySide6.QtGui import QFont + + +class TitleBar(QWidget): + """Custom title bar for frameless window""" + + minimize_clicked = Signal() + maximize_clicked = Signal() + close_clicked = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + self.setObjectName("titleBar") + self.setFixedHeight(48) + self._setup_ui() + self._drag_pos = None + + def _setup_ui(self): + """Setup title bar UI""" + + layout = QHBoxLayout(self) + layout.setContentsMargins(20, 0, 12, 0) + layout.setSpacing(0) + + # Logo and title section + title_layout = QHBoxLayout() + title_layout.setSpacing(12) + + # Logo placeholder (can be replaced with actual logo) + self.logo_label = QLabel("◈") + self.logo_label.setStyleSheet(""" + font-size: 20px; + color: #FF6B35; + font-weight: bold; + """) + title_layout.addWidget(self.logo_label) + + # Title + self.title_label = QLabel("NELA LAUNCHER") + self.title_label.setObjectName("titleLabel") + self.title_label.setStyleSheet(""" + font-size: 16px; + font-weight: 600; + color: #FFFFFF; + letter-spacing: 1px; + """) + title_layout.addWidget(self.title_label) + + layout.addLayout(title_layout) + + # Spacer + spacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + layout.addItem(spacer) + + # Window controls + controls_layout = QHBoxLayout() + controls_layout.setSpacing(8) + controls_layout.setObjectName("windowControls") + + # Minimize button + self.minimize_btn = QPushButton("─") + self.minimize_btn.setFixedSize(40, 40) + self.minimize_btn.setStyleSheet(""" + QPushButton { + background-color: transparent; + border: none; + color: #A0A0A0; + font-size: 18px; + border-radius: 8px; + } + QPushButton:hover { + background-color: #1F1F1F; + color: #FFFFFF; + } + """) + self.minimize_btn.clicked.connect(self._on_minimize) + controls_layout.addWidget(self.minimize_btn) + + # Maximize/Restore button + self.maximize_btn = QPushButton("□") + self.maximize_btn.setFixedSize(40, 40) + self.maximize_btn.setStyleSheet(""" + QPushButton { + background-color: transparent; + border: none; + color: #A0A0A0; + font-size: 14px; + border-radius: 8px; + } + QPushButton:hover { + background-color: #1F1F1F; + color: #FFFFFF; + } + """) + self.maximize_btn.clicked.connect(self._on_maximize) + controls_layout.addWidget(self.maximize_btn) + + # Close button + self.close_btn = QPushButton("✕") + self.close_btn.setFixedSize(40, 40) + self.close_btn.setStyleSheet(""" + QPushButton { + background-color: transparent; + border: none; + color: #A0A0A0; + font-size: 16px; + border-radius: 8px; + } + QPushButton:hover { + background-color: #E81123; + color: #FFFFFF; + } + """) + self.close_btn.clicked.connect(self._on_close) + controls_layout.addWidget(self.close_btn) + + layout.addLayout(controls_layout) + + def _on_minimize(self): + """Handle minimize button click""" + if self.parentWindow(): + self.parentWindow().showMinimized() + self.minimize_clicked.emit() + + def _on_maximize(self): + """Handle maximize button click""" + if self.parentWindow(): + if self.parentWindow().isMaximized(): + self.parentWindow().showNormal() + self.maximize_btn.setText("□") + else: + self.parentWindow().showMaximized() + self.maximize_btn.setText("❐") + self.maximize_clicked.emit() + + def _on_close(self): + """Handle close button click""" + if self.parentWindow(): + self.parentWindow().close() + self.close_clicked.emit() + + def mousePressEvent(self, event): + """Handle mouse press for dragging""" + if event.button() == Qt.LeftButton: + if self.parentWindow() and not self.parentWindow().isMaximized(): + self._drag_pos = event.globalPosition().toPoint() - self.parentWindow().frameGeometry().topLeft() + event.accept() + + def mouseMoveEvent(self, event): + """Handle mouse move for dragging""" + if event.buttons() == Qt.LeftButton and self._drag_pos and self.parentWindow(): + if not self.parentWindow().isMaximized(): + self.parentWindow().move(event.globalPosition().toPoint() - self._drag_pos) + event.accept()