import 'package:flutter/material.dart'; import 'package:moonwell_launcher/app/design_system/mw_primitives.dart'; import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart'; const _ivory = Color(0xFFECE4D0); const _muted = Color(0x80ECE4D0); const _border = Color(0x247FB8D4); class MwGameTile extends StatelessWidget { const MwGameTile({ super.key, required this.title, required this.icon, this.subtitle, this.selected = false, this.enabled = true, this.compact = false, this.onPressed, this.mark, }); final String title; final String? subtitle; final IconData icon; final bool selected; final bool enabled; final bool compact; final VoidCallback? onPressed; final String? mark; @override Widget build(BuildContext context) { final accent = MoonWellDesignTokens.of(context).accent; return Opacity( opacity: enabled ? 1 : .58, child: Material( color: selected ? accent.withAlpha(25) : accent.withAlpha(8), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), side: BorderSide(color: selected ? accent : _border), ), child: InkWell( onTap: enabled ? onPressed : null, borderRadius: BorderRadius.circular(6), child: SizedBox( height: 52, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), child: Row( children: [ Container( width: 32, height: 32, alignment: Alignment.center, decoration: BoxDecoration( gradient: LinearGradient( colors: [const Color(0xFF5B3A8A), accent], ), borderRadius: BorderRadius.circular(6), ), child: Text( mark ?? title.characters.first, style: const TextStyle( fontFamily: 'Cinzel', color: _ivory, fontSize: 12, ), ), ), if (!compact) ...[ const SizedBox(width: 10), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title.toUpperCase(), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontFamily: 'Cinzel', color: _ivory, fontSize: 10, letterSpacing: .8, ), ), const SizedBox(height: 3), Text( (subtitle ?? (enabled ? 'OPEN BETA' : 'СКОРО')) .toUpperCase(), style: TextStyle( fontFamily: 'JetBrains Mono', color: selected ? accent : _muted, fontSize: 8, letterSpacing: 1.1, ), ), ], ), ), if (!enabled) const Text( 'SOON', style: TextStyle( fontFamily: 'JetBrains Mono', color: _muted, fontSize: 7, letterSpacing: 1, ), ), ], ], ), ), ), ), ), ); } } class MwGameRail extends StatelessWidget { const MwGameRail({super.key, this.compact = false}); final bool compact; @override Widget build(BuildContext context) { final accent = MoonWellDesignTokens.of(context).accent; return Container( padding: const EdgeInsets.fromLTRB(14, 48, 14, 30), decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xB5070A18), Color(0x8F070A18)], ), border: Border(right: BorderSide(color: _border)), ), child: Column( children: [ Container( width: 40, height: 40, alignment: Alignment.center, decoration: BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( colors: [accent, const Color(0xFF5B3A8A)], ), border: Border.all(color: accent), boxShadow: [ BoxShadow(color: accent.withAlpha(70), blurRadius: 12), ], ), child: const Text('A', style: TextStyle(fontFamily: 'Cinzel')), ), const SizedBox(height: 14), const Divider(color: _border), const Align( alignment: Alignment.centerLeft, child: Padding( padding: EdgeInsets.fromLTRB(8, 7, 0, 8), child: Text( 'ИГРЫ', style: TextStyle( fontFamily: 'JetBrains Mono', color: _muted, fontSize: 9, letterSpacing: 2.2, ), ), ), ), MwGameTile( title: 'MoonWell · WoW', subtitle: 'Open Beta', mark: 'MW', icon: Icons.nights_stay, selected: true, compact: compact, onPressed: () {}, ), const SizedBox(height: 8), const MwGameTile( title: 'MoonWell Karts', subtitle: 'Closed Alpha', mark: 'K', icon: Icons.sports_motorsports, enabled: false, ), const SizedBox(height: 8), const MwGameTile( title: 'Tides of Elune', subtitle: 'В разработке', mark: 'T', icon: Icons.style, enabled: false, ), const SizedBox(height: 8), const MwGameTile( title: 'Project Verge', subtitle: 'Анонс', mark: 'V', icon: Icons.auto_awesome, enabled: false, ), const Spacer(), const Divider(color: _border), Row( children: [ IconButton( onPressed: null, tooltip: 'Друзья', icon: const Icon(Icons.people_outline, size: 18), ), const Spacer(), Icon( Icons.settings_outlined, color: accent.withAlpha(150), size: 18, ), const SizedBox(width: 12), ], ), ], ), ); } } class MwNewsItemData { const MwNewsItemData({ required this.title, required this.body, this.createdAt, this.imageUrl, }); final String title; final String body; final DateTime? createdAt; final String? imageUrl; } class MwNewsItem extends StatelessWidget { const MwNewsItem({super.key, required this.item}); final MwNewsItemData item; @override Widget build(BuildContext context) { final accent = MoonWellDesignTokens.of(context).accent; final date = item.createdAt?.toLocal(); return Padding( padding: const EdgeInsets.fromLTRB(0, 13, 12, 13), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( border: Border.all(color: accent), borderRadius: BorderRadius.circular(3), ), child: Text( 'НОВОСТЬ', style: TextStyle( fontFamily: 'JetBrains Mono', color: accent, fontSize: 8, letterSpacing: 1.5, ), ), ), const Spacer(), Text( date == null ? 'СЕГОДНЯ' : '${date.day.toString().padLeft(2, '0')}.${date.month.toString().padLeft(2, '0')}', style: const TextStyle( fontFamily: 'JetBrains Mono', color: _muted, fontSize: 9, ), ), ], ), const SizedBox(height: 7), Text( item.title, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( fontFamily: 'Cormorant Garamond', color: _ivory, fontSize: 16, height: 1.25, ), ), ], ), ); } } enum MwNewsListState { loading, empty, failure, populated } class MwNewsList extends StatelessWidget { const MwNewsList({ super.key, required this.state, this.items = const [], this.error, this.onRetry, }); final MwNewsListState state; final List items; final String? error; final VoidCallback? onRetry; @override Widget build(BuildContext context) { final accent = MoonWellDesignTokens.of(context).accent; return Container( decoration: BoxDecoration( color: const Color(0xD00A0D1F), border: Border.all(color: const Color(0x477FB8D4)), borderRadius: BorderRadius.circular(10), ), clipBehavior: Clip.antiAlias, child: Column( children: [ SizedBox( height: 45, child: Row( children: [ _NewsTab(label: 'НОВОСТИ', selected: true, accent: accent), _NewsTab(label: 'СОБЫТИЯ', accent: accent), _NewsTab(label: 'ПАТЧИ', accent: accent), ], ), ), const Divider(height: 1, color: _border), Expanded(child: _content()), Container( height: 40, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: const BoxDecoration( border: Border(top: BorderSide(color: _border)), ), child: Row( children: [ const Text( 'MOONWELL.GG', style: TextStyle( fontFamily: 'JetBrains Mono', color: _muted, fontSize: 9, letterSpacing: 1.5, ), ), const Spacer(), Text( 'ВСЕ →', style: TextStyle( fontFamily: 'JetBrains Mono', color: accent, fontSize: 9, letterSpacing: 1.5, ), ), ], ), ), ], ), ); } Widget _content() => switch (state) { MwNewsListState.loading => const Center( child: CircularProgressIndicator(strokeWidth: 2), ), MwNewsListState.empty => const Center( child: Text('Новостей пока нет', style: TextStyle(color: _muted)), ), MwNewsListState.failure => Center( child: Text( error ?? 'Не удалось загрузить новости', style: const TextStyle(color: Color(0xFFD76A78)), ), ), MwNewsListState.populated => ListView.separated( padding: const EdgeInsets.only(left: 16, right: 4), itemCount: items.length, separatorBuilder: (_, _) => const Divider(height: 1, color: _border), itemBuilder: (_, index) => MwNewsItem(item: items[index]), ), }; } class _NewsTab extends StatelessWidget { const _NewsTab({ required this.label, required this.accent, this.selected = false, }); final String label; final Color accent; final bool selected; @override Widget build(BuildContext context) => Expanded( child: Container( alignment: Alignment.center, decoration: BoxDecoration( color: selected ? accent.withAlpha(12) : Colors.transparent, border: Border( bottom: BorderSide(color: selected ? accent : Colors.transparent), ), ), child: Text( label, style: TextStyle( fontFamily: 'Cinzel', color: selected ? _ivory : _muted, fontSize: 9, letterSpacing: 1.5, ), ), ), ); } enum MwSyncPresentation { install, scanning, downloading, pausing, paused, verifying, ready, launched, failure, } class MwSyncActionArea extends StatelessWidget { const MwSyncActionArea({ super.key, required this.state, required this.status, required this.onPrimary, this.onPause, this.progress, this.currentPath, this.error, }); final MwSyncPresentation state; final String status; final VoidCallback onPrimary; final VoidCallback? onPause; final double? progress; final String? currentPath; final String? error; @override Widget build(BuildContext context) { final accent = MoonWellDesignTokens.of(context).accent; final ready = state == MwSyncPresentation.ready; final active = { MwSyncPresentation.scanning, MwSyncPresentation.downloading, MwSyncPresentation.pausing, MwSyncPresentation.verifying, }.contains(state); final value = ready || state == MwSyncPresentation.launched ? 1.0 : (progress ?? 0); final label = switch (state) { MwSyncPresentation.install => 'УСТАНОВИТЬ', MwSyncPresentation.paused => 'ПРОДОЛЖИТЬ', MwSyncPresentation.ready => 'ИГРАТЬ', MwSyncPresentation.launched => 'ИГРА ЗАПУЩЕНА', MwSyncPresentation.failure => 'ПОВТОРИТЬ', _ => 'ПАУЗА', }; return Container( padding: const EdgeInsets.fromLTRB(32, 18, 32, 20), decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0x0006091A), Color(0xF206091A)], ), ), child: Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( color: const Color(0x99070A18), border: Border.all(color: _border), borderRadius: BorderRadius.circular(6), ), child: const Row( children: [ _PulseDot(), SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "ELUNE'S GRACE", style: TextStyle( fontFamily: 'Cinzel', color: _ivory, fontSize: 11, letterSpacing: 1.5, ), ), SizedBox(height: 3), Text( 'ONLINE · EU', style: TextStyle( fontFamily: 'JetBrains Mono', color: _muted, fontSize: 8, letterSpacing: 1.1, ), ), ], ), ], ), ), const SizedBox(width: 24), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Row( children: [ Expanded( child: Text( error ?? status, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontFamily: 'JetBrains Mono', color: error == null ? accent : const Color(0xFFD76A78), fontSize: 10, letterSpacing: 1.1, ), ), ), Text( '${(value * 100).round()}%', style: const TextStyle( fontFamily: 'JetBrains Mono', color: _ivory, fontSize: 13, ), ), ], ), const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(2), child: LinearProgressIndicator( value: value, minHeight: 4, color: accent, backgroundColor: accent.withAlpha(28), ), ), const SizedBox(height: 8), Row( children: [ Expanded( child: Text( currentPath ?? (ready ? 'Все файлы целы' : 'Подготовка клиента'), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontFamily: 'JetBrains Mono', color: _muted, fontSize: 8, ), ), ), Text( ready ? 'Последняя проверка: только что' : '', style: const TextStyle( fontFamily: 'JetBrains Mono', color: _muted, fontSize: 8, ), ), ], ), ], ), ), const SizedBox(width: 24), SizedBox( width: 220, height: 64, child: ElevatedButton.icon( onPressed: active ? onPause : (state == MwSyncPresentation.launched ? null : onPrimary), icon: Icon( ready ? Icons.play_arrow : active ? Icons.pause : Icons.refresh, size: 20, ), label: Text(label), style: ElevatedButton.styleFrom( backgroundColor: ready ? const Color(0xFF60B17F) : state == MwSyncPresentation.failure ? const Color(0xFFD76A78) : const Color(0xFF6D499E), foregroundColor: ready ? const Color(0xFF07130F) : _ivory, textStyle: const TextStyle( fontFamily: 'Cinzel', fontSize: 14, letterSpacing: 2.4, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), ), ), ), ), ], ), ); } } class _PulseDot extends StatelessWidget { const _PulseDot(); @override Widget build(BuildContext context) => Container( width: 8, height: 8, decoration: const BoxDecoration( color: Color(0xFF6FC28A), shape: BoxShape.circle, boxShadow: [BoxShadow(color: Color(0xAA6FC28A), blurRadius: 8)], ), ); } class MwLauncherStatusBar extends StatelessWidget { const MwLauncherStatusBar({ super.key, required this.status, this.fileCount, this.speed, this.eta, this.buildHash, }); final String status; final String? fileCount; final String? speed; final String? eta; final String? buildHash; @override Widget build(BuildContext context) => Container( padding: const EdgeInsets.symmetric(horizontal: 32), decoration: const BoxDecoration( color: Color(0xD904060E), border: Border(top: BorderSide(color: _border)), ), child: DefaultTextStyle( style: const TextStyle( fontFamily: 'JetBrains Mono', color: _muted, fontSize: 8, letterSpacing: .8, ), child: Row( children: [ Expanded( child: Text(status, maxLines: 1, overflow: TextOverflow.ellipsis), ), if (fileCount != null) Text(fileCount!), if (speed != null) ...[const SizedBox(width: 24), Text(speed!)], if (eta != null) ...[const SizedBox(width: 24), Text(eta!)], if (buildHash != null) ...[ const SizedBox(width: 24), Text(buildHash!), ], const SizedBox(width: 24), const Text('Лаунчер v1.0.0'), ], ), ), ); } class MwAccountAffordance extends StatelessWidget { const MwAccountAffordance({ super.key, required this.onLogout, this.onSettings, }); final VoidCallback onLogout; final VoidCallback? onSettings; @override Widget build(BuildContext context) => PopupMenuButton( tooltip: 'Аккаунт', onSelected: (value) => value == 'settings' ? onSettings?.call() : onLogout(), itemBuilder: (_) => const [ PopupMenuItem(value: 'settings', child: Text('Настройки')), PopupMenuItem(value: 'logout', child: Text('Выйти')), ], child: Container( padding: const EdgeInsets.fromLTRB(8, 7, 15, 7), decoration: BoxDecoration( color: const Color(0x99070A18), border: Border.all(color: _border), borderRadius: BorderRadius.circular(99), ), child: const Row( children: [ CircleAvatar( radius: 18, backgroundColor: Color(0xFF5B3A8A), child: Text( 'Æ', style: TextStyle(fontFamily: 'Cinzel', fontSize: 12), ), ), SizedBox(width: 11), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Aranthel', style: TextStyle( fontFamily: 'Cinzel', color: _ivory, fontSize: 12, letterSpacing: 1, ), ), SizedBox(height: 2), Text( 'ХРАНИТЕЛЬ РОЩИ', style: TextStyle( fontFamily: 'JetBrains Mono', color: _muted, fontSize: 7, letterSpacing: 1.1, ), ), ], ), ], ), ), ); } class MwSettingsPanel extends StatelessWidget { const MwSettingsPanel({ super.key, required this.installationPath, required this.themeVariant, required this.onChooseDirectory, required this.onThemeChanged, required this.onVerify, required this.onLogout, this.syncActive = false, }); final String? installationPath; final MoonWellThemeVariant themeVariant; final VoidCallback onChooseDirectory; final ValueChanged onThemeChanged; final VoidCallback onVerify; final VoidCallback onLogout; final bool syncActive; @override Widget build(BuildContext context) => MwPanel( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text('Настройки', style: Theme.of(context).textTheme.headlineMedium), const SizedBox(height: 20), const Text('Папка установки'), const SizedBox(height: 6), MwStatusText( text: installationPath ?? 'Папка не выбрана', monospace: true, ), const SizedBox(height: 8), MwButton( label: 'Выбрать папку', icon: Icons.folder_open_outlined, onPressed: syncActive ? null : onChooseDirectory, variant: MwButtonVariant.secondary, ), const SizedBox(height: 20), SegmentedButton( segments: const [ ButtonSegment( value: MoonWellThemeVariant.forest, label: Text('Лес'), ), ButtonSegment( value: MoonWellThemeVariant.temple, label: Text('Храм'), ), ], selected: {themeVariant}, onSelectionChanged: (value) => onThemeChanged(value.single), ), const SizedBox(height: 20), MwButton( label: 'Проверить файлы', onPressed: syncActive ? null : onVerify, variant: MwButtonVariant.secondary, ), const SizedBox(height: 8), MwButton( label: 'Выйти', onPressed: onLogout, variant: MwButtonVariant.destructive, ), ], ), ); }