diff --git a/lib/app/home_screen/home_screen.dart b/lib/app/home_screen/home_screen.dart index d4f14ab..9a2c6fc 100644 --- a/lib/app/home_screen/home_screen.dart +++ b/lib/app/home_screen/home_screen.dart @@ -1,11 +1,10 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:moonwell_launcher/app/home_screen/bloc/home_screen_bloc.dart'; import 'package:moonwell_launcher/app/home_screen/home_screen_content.dart'; import 'package:moonwell_launcher/app/login_screen/login_screen.dart'; import 'package:moonwell_launcher/app/theme/mw_theme.dart'; +import 'package:moonwell_launcher/app/widgets/window_title_bar.dart'; import 'package:moonwell_launcher/features/launcher/application/client_sync_use_case.dart'; import 'package:moonwell_launcher/features/launcher/data/game_installation_service.dart'; import 'package:moonwell_launcher/features/launcher/data/launcher_api_client.dart'; @@ -13,7 +12,6 @@ import 'package:moonwell_launcher/features/launcher/domain/entities/client_manif import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_session.dart'; import 'package:moonwell_launcher/features/preferences/domain/repositories/preferences_repository.dart'; import 'package:moonwell_launcher/service_container.dart'; -import 'package:window_manager/window_manager.dart'; class HomeScreen extends StatelessWidget { const HomeScreen({super.key, required this.session, required this.manifest}); @@ -38,8 +36,8 @@ class HomeScreen extends StatelessWidget { listener: (context, state) async { await Navigator.of(context).pushReplacement( PageRouteBuilder( - pageBuilder: (_, __, ___) => const LoginScreen(), - transitionsBuilder: (_, animation, __, child) { + pageBuilder: (_, _, _) => const LoginScreen(), + transitionsBuilder: (_, animation, _, child) { return FadeTransition(opacity: animation, child: child); }, transitionDuration: const Duration(milliseconds: 250), @@ -49,11 +47,9 @@ class HomeScreen extends StatelessWidget { child: Scaffold( body: Stack( children: [ - // Background image Positioned.fill( child: Image.asset('assets/background.png', fit: BoxFit.cover), ), - // Gradient overlay Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( @@ -70,10 +66,13 @@ class HomeScreen extends StatelessWidget { ), ), ), - // Main content const Positioned.fill(child: HomeScreenContent()), - // Custom title bar - const Positioned(top: 0, left: 0, right: 0, child: _TitleBar()), + const Positioned( + top: 0, + left: 0, + right: 0, + child: WindowTitleBar(), + ), ], ), ), @@ -81,71 +80,3 @@ class HomeScreen extends StatelessWidget { ); } } - -class _TitleBar extends StatelessWidget { - const _TitleBar(); - - @override - Widget build(BuildContext context) { - final cs = Theme.of(context).colorScheme; - - return GestureDetector( - onPanStart: (_) => windowManager.startDragging(), - child: SizedBox( - height: 36, - child: Row( - children: [ - const Spacer(), - _TitleBarButton( - icon: Icons.remove, - onTap: () => windowManager.minimize(), - color: cs.onSurface, - ), - _TitleBarButton( - icon: Icons.crop_square, - onTap: () async { - if (await windowManager.isMaximized()) { - await windowManager.unmaximize(); - } else { - await windowManager.maximize(); - } - }, - color: cs.onSurface, - ), - _TitleBarButton( - icon: Icons.close, - onTap: () { - unawaited(windowManager.destroy()); - }, - color: MWColors.error, - ), - ], - ), - ), - ); - } -} - -class _TitleBarButton extends StatelessWidget { - const _TitleBarButton({ - required this.icon, - required this.onTap, - required this.color, - }); - - final IconData icon; - final VoidCallback onTap; - final Color color; - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - child: SizedBox( - width: 40, - height: 36, - child: Icon(icon, size: 16, color: color.withAlpha(180)), - ), - ); - } -} diff --git a/lib/app/login_screen/login_screen.dart b/lib/app/login_screen/login_screen.dart index 48f0ff7..8c8e9e6 100644 --- a/lib/app/login_screen/login_screen.dart +++ b/lib/app/login_screen/login_screen.dart @@ -1,12 +1,10 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:moonwell_launcher/app/home_screen/home_screen.dart'; import 'package:moonwell_launcher/app/theme/mw_theme.dart'; +import 'package:moonwell_launcher/app/widgets/window_title_bar.dart'; import 'package:moonwell_launcher/features/launcher/data/launcher_api_client.dart'; import 'package:moonwell_launcher/features/preferences/domain/repositories/preferences_repository.dart'; import 'package:moonwell_launcher/service_container.dart'; -import 'package:window_manager/window_manager.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @@ -53,9 +51,9 @@ class _LoginScreenState extends State { await Navigator.of(context).pushReplacement( PageRouteBuilder( - pageBuilder: (_, __, ___) => + pageBuilder: (_, _, _) => HomeScreen(session: session, manifest: manifest), - transitionsBuilder: (_, animation, __, child) { + transitionsBuilder: (_, animation, _, child) { return FadeTransition(opacity: animation, child: child); }, transitionDuration: const Duration(milliseconds: 400), @@ -83,7 +81,6 @@ class _LoginScreenState extends State { return Scaffold( body: Stack( children: [ - // Background Positioned.fill( child: Image.asset('assets/background.png', fit: BoxFit.cover), ), @@ -101,23 +98,19 @@ class _LoginScreenState extends State { ), ), ), - // Title bar - Positioned(top: 0, left: 0, right: 0, child: _TitleBar(cs: cs)), - // Login form — centered + const Positioned(top: 0, left: 0, right: 0, child: WindowTitleBar()), Center( child: SizedBox( width: 340, child: Column( mainAxisSize: MainAxisSize.min, children: [ - // Logo Image.asset( 'assets/logo.png', height: 160, filterQuality: FilterQuality.high, ), const SizedBox(height: 32), - // Card Container( padding: const EdgeInsets.all(28), decoration: BoxDecoration( @@ -194,70 +187,3 @@ class _LoginScreenState extends State { ); } } - -class _TitleBar extends StatelessWidget { - const _TitleBar({required this.cs}); - final ColorScheme cs; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onPanStart: (_) => windowManager.startDragging(), - child: SizedBox( - height: 36, - child: Row( - children: [ - const Spacer(), - _TitleBarButton( - icon: Icons.remove, - onTap: () => windowManager.minimize(), - color: cs.onSurface, - ), - _TitleBarButton( - icon: Icons.crop_square, - onTap: () async { - if (await windowManager.isMaximized()) { - await windowManager.unmaximize(); - } else { - await windowManager.maximize(); - } - }, - color: cs.onSurface, - ), - _TitleBarButton( - icon: Icons.close, - onTap: () { - unawaited(windowManager.destroy()); - }, - color: MWColors.error, - ), - ], - ), - ), - ); - } -} - -class _TitleBarButton extends StatelessWidget { - const _TitleBarButton({ - required this.icon, - required this.onTap, - required this.color, - }); - - final IconData icon; - final VoidCallback onTap; - final Color color; - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - child: SizedBox( - width: 40, - height: 36, - child: Icon(icon, size: 16, color: color.withAlpha(180)), - ), - ); - } -} diff --git a/lib/app/widgets/window_title_bar.dart b/lib/app/widgets/window_title_bar.dart new file mode 100644 index 0000000..5009b53 --- /dev/null +++ b/lib/app/widgets/window_title_bar.dart @@ -0,0 +1,77 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:moonwell_launcher/app/theme/mw_theme.dart'; +import 'package:window_manager/window_manager.dart'; + +class WindowTitleBar extends StatelessWidget { + const WindowTitleBar({super.key}); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + + return GestureDetector( + onPanStart: (_) => windowManager.startDragging(), + child: SizedBox( + height: 36, + child: Row( + children: [ + const Spacer(), + _TitleBarButton( + icon: Icons.remove, + onTap: () => windowManager.minimize(), + color: cs.onSurface, + ), + _TitleBarButton( + icon: Icons.crop_square, + onTap: () { + unawaited(_toggleMaximized()); + }, + color: cs.onSurface, + ), + _TitleBarButton( + icon: Icons.close, + onTap: () { + unawaited(windowManager.destroy()); + }, + color: MWColors.error, + ), + ], + ), + ), + ); + } + + Future _toggleMaximized() async { + if (await windowManager.isMaximized()) { + await windowManager.unmaximize(); + } else { + await windowManager.maximize(); + } + } +} + +class _TitleBarButton extends StatelessWidget { + const _TitleBarButton({ + required this.icon, + required this.onTap, + required this.color, + }); + + final IconData icon; + final VoidCallback onTap; + final Color color; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: SizedBox( + width: 40, + height: 36, + child: Icon(icon, size: 16, color: color.withAlpha(180)), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 8e61f53..3aa0d7c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,8 @@ // main.dart -import 'package:flutter/material.dart'; import 'dart:async'; -import 'dart:math' as math; -import 'package:flutter/foundation.dart' - show defaultTargetPlatform, TargetPlatform; + +import 'package:flutter/material.dart'; import 'package:moonwell_launcher/app/mw_app.dart'; -import 'package:moonwell_launcher/app/theme/mw_theme.dart'; import 'package:moonwell_launcher/service_container.dart'; import 'package:window_manager/window_manager.dart'; @@ -32,438 +29,3 @@ Future main() async { runApp(const MoonWellApp()); } - -bool get _isDesktop => const { - TargetPlatform.macOS, - TargetPlatform.linux, - TargetPlatform.windows, -}.contains(defaultTargetPlatform); - -class LauncherHome extends StatefulWidget { - const LauncherHome({super.key}); - - @override - State createState() => _LauncherHomeState(); -} - -class _LauncherHomeState extends State { - double _progress = 0.0; // 0..1 - bool _isDownloading = false; - double _kbps = 0; // fake speed - Duration _eta = Duration.zero; - Timer? _timer; - - @override - void dispose() { - _timer?.cancel(); - super.dispose(); - } - - void _startDownload() { - if (_isDownloading || _progress >= 1.0) return; - setState(() => _isDownloading = true); - - _timer = Timer.periodic(const Duration(milliseconds: 160), (t) { - // Fake speed & progress for demo - final chunk = 0.002 + math.Random().nextDouble() * 0.004; - _kbps = 800 + math.Random().nextDouble() * 2200; // 0.8–3.0 MB/s - _progress = (_progress + chunk).clamp(0.0, 1.0); - final remaining = (1 - _progress); - // naive ETA based on current "speed" - _eta = Duration(seconds: math.max(1, (remaining * 120).round())); - - if (_progress >= 1.0) { - _isDownloading = false; - t.cancel(); - } - setState(() {}); - }); - } - - void _pause() { - _timer?.cancel(); - setState(() => _isDownloading = false); - } - - void _reset() { - _timer?.cancel(); - setState(() { - _progress = 0; - _isDownloading = false; - _kbps = 0; - _eta = Duration.zero; - }); - } - - @override - Widget build(BuildContext context) { - final cs = Theme.of(context).colorScheme; - final deco = Theme.of(context).extension()!; - final title = ShaderMask( - shaderCallback: (rect) => - (deco.goldBevel as LinearGradient).createShader(rect), - child: Text( - 'MOONWELL', - style: Theme.of( - context, - ).textTheme.displaySmall?.copyWith(shadows: [deco.textGlow]), - ), - ); - - if (!_isDesktop) { - return Scaffold( - body: Center( - child: Text( - 'Desktop only. Please run on Windows, macOS, or Linux.', - style: Theme.of(context).textTheme.titleLarge, - ), - ), - ); - } - - return Scaffold( - body: Container( - decoration: const BoxDecoration( - // subtle vignette for “night” vibe - gradient: RadialGradient( - center: Alignment(0, -0.5), - radius: 1.2, - colors: [Color(0xFF0F1522), Color(0xFF0B101A)], - ), - ), - child: Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 1100, maxHeight: 760), - child: Padding( - padding: const EdgeInsets.all(20), - child: Row( - children: [ - // LEFT: Game title + download controls - Expanded( - flex: 3, - child: _LeftPane( - title: title, - progress: _progress, - isDownloading: _isDownloading, - kbps: _kbps, - eta: _eta, - onStart: _startDownload, - onPause: _pause, - onReset: _reset, - ), - ), - const SizedBox(width: 20), - // RIGHT: Patch notes - Expanded(flex: 4, child: _PatchNotesPane(color: cs.surface)), - ], - ), - ), - ), - ), - ), - ); - } -} - -class _LeftPane extends StatelessWidget { - const _LeftPane({ - required this.title, - required this.progress, - required this.isDownloading, - required this.kbps, - required this.eta, - required this.onStart, - required this.onPause, - required this.onReset, - }); - - final Widget title; - final double progress; - final bool isDownloading; - final double kbps; - final Duration eta; - final VoidCallback onStart; - final VoidCallback onPause; - final VoidCallback onReset; - - String get _speedLabel { - if (kbps <= 0) return '—'; - if (kbps >= 1024) { - return '${(kbps / 1024).toStringAsFixed(2)} MB/s'; - } - return '${kbps.toStringAsFixed(0)} kB/s'; - } - - @override - Widget build(BuildContext context) { - final cs = Theme.of(context).colorScheme; - final deco = Theme.of(context).extension()!; - final percent = (progress * 100).clamp(0, 100).toStringAsFixed(1); - - final canPlay = progress >= 1.0 && !isDownloading; - - return Card( - margin: EdgeInsets.zero, - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - title, - const SizedBox(height: 16), - Text( - 'Classic MMO Launcher', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: cs.onSurface.withValues(alpha: 0.85), - ), - ), - const SizedBox(height: 28), - - // Progress bar - _GildedProgressBar(value: progress), - - const SizedBox(height: 12), - Row( - children: [ - Text( - '$percent%', - style: Theme.of(context).textTheme.titleMedium, - ), - const Spacer(), - Icon(Icons.speed, size: 18, color: cs.onSurfaceVariant), - const SizedBox(width: 6), - Text(_speedLabel, style: TextStyle(color: cs.onSurfaceVariant)), - const SizedBox(width: 16), - Icon( - Icons.timer_outlined, - size: 18, - color: cs.onSurfaceVariant, - ), - const SizedBox(width: 6), - Text( - eta == Duration.zero - ? '—' - : '${eta.inMinutes}:${(eta.inSeconds % 60).toString().padLeft(2, '0')}', - style: TextStyle(color: cs.onSurfaceVariant), - ), - ], - ), - - const SizedBox(height: 24), - - // Controls row - Row( - children: [ - Expanded( - child: ElevatedButton.icon( - icon: Icon( - canPlay ? Icons.play_arrow_rounded : Icons.download, - ), - label: Text( - canPlay - ? 'Play' - : (isDownloading - ? 'Downloading…' - : (progress == 0 ? 'Install' : 'Resume')), - ), - onPressed: canPlay - ? () {} - : (isDownloading ? null : onStart), - ), - ), - const SizedBox(width: 12), - OutlinedButton.icon( - icon: Icon( - isDownloading ? Icons.pause_rounded : Icons.replay, - ), - label: Text(isDownloading ? 'Pause' : 'Reset'), - onPressed: isDownloading - ? onPause - : (progress > 0 ? onReset : null), - ), - ], - ), - - const SizedBox(height: 20), - // Disk path / build info (placeholders) - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: cs.surfaceContainerHighest.withValues(alpha: 0.5), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: cs.outline), - boxShadow: (deco.cardGlow), - ), - child: Row( - children: [ - const Icon(Icons.folder_open), - const SizedBox(width: 8), - Expanded( - child: Text( - 'Install path: C:/Games/MoonWell', - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 12), - const Icon(Icons.info_outline, size: 18), - const SizedBox(width: 6), - Text( - 'Build: 1.2.0 (live)', - style: TextStyle(color: cs.onSurfaceVariant), - ), - ], - ), - ), - ], - ), - ), - ); - } -} - -class _GildedProgressBar extends StatelessWidget { - const _GildedProgressBar({required this.value}); - final double value; - - @override - Widget build(BuildContext context) { - final cs = Theme.of(context).colorScheme; - final deco = Theme.of(context).extension()!; - return Container( - height: 20, - decoration: BoxDecoration( - color: cs.surfaceContainerHighest, - borderRadius: BorderRadius.circular(14), - border: Border.all(color: cs.outline), - ), - clipBehavior: Clip.antiAlias, - child: Stack( - children: [ - // Fill with gold bevel - FractionallySizedBox( - widthFactor: value.clamp(0.0, 1.0), - alignment: Alignment.centerLeft, - child: DecoratedBox( - decoration: BoxDecoration( - gradient: (deco.goldBevel as LinearGradient), - ), - ), - ), - // Subtle top highlight - Align( - alignment: Alignment.topCenter, - child: Container( - height: 6, - color: Colors.white.withValues(alpha: 0.08), - ), - ), - ], - ), - ); - } -} - -class _PatchNotesPane extends StatelessWidget { - const _PatchNotesPane({required this.color}); - final Color color; - - @override - Widget build(BuildContext context) { - final cs = Theme.of(context).colorScheme; - - // Example static notes — wire your real source here. - final notes = [ - ( - '1.2.0 — “Moonlit March”', - [ - 'New dungeon: *Hall of Tides*.', - 'Launcher supports delta updates with blockmaps.', - 'Improved shader warmup (faster first launch).', - 'Fixed login race condition on slow networks.', - ], - ), - ( - '1.1.5', - [ - 'Balance pass on Ranger & Warlock.', - 'Memory usage reduced ~12% in large towns.', - ], - ), - ]; - - return Card( - margin: EdgeInsets.zero, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header - Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - decoration: BoxDecoration( - color: color, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(20), - ), - border: Border(bottom: BorderSide(color: cs.outline)), - ), - child: Row( - children: [ - const Icon(Icons.article_outlined), - const SizedBox(width: 10), - Text( - 'Patch Notes', - style: Theme.of(context).textTheme.titleLarge, - ), - const Spacer(), - TextButton.icon( - onPressed: () {}, // hook to "Open full changelog" - icon: const Icon(Icons.open_in_new), - label: const Text('Full changelog'), - ), - ], - ), - ), - // Body - Expanded( - child: Scrollbar( - child: ListView.separated( - padding: const EdgeInsets.fromLTRB(20, 16, 20, 20), - itemCount: notes.length, - separatorBuilder: (_, __) => const Divider(), - itemBuilder: (context, i) { - final (title, items) = notes[i]; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 8), - ...items.map( - (s) => Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('• '), - Expanded( - child: Text( - s, - style: TextStyle( - color: cs.onSurface.withValues(alpha: 0.9), - ), - ), - ), - ], - ), - ), - ], - ); - }, - ), - ), - ), - ], - ), - ); - } -}