refactor(ui): Minor refactoring, extract repeating title widget
This commit is contained in:
@@ -1,11 +1,10 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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/bloc/home_screen_bloc.dart';
|
||||||
import 'package:moonwell_launcher/app/home_screen/home_screen_content.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/login_screen/login_screen.dart';
|
||||||
import 'package:moonwell_launcher/app/theme/mw_theme.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/application/client_sync_use_case.dart';
|
||||||
import 'package:moonwell_launcher/features/launcher/data/game_installation_service.dart';
|
import 'package:moonwell_launcher/features/launcher/data/game_installation_service.dart';
|
||||||
import 'package:moonwell_launcher/features/launcher/data/launcher_api_client.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/launcher/domain/entities/launcher_session.dart';
|
||||||
import 'package:moonwell_launcher/features/preferences/domain/repositories/preferences_repository.dart';
|
import 'package:moonwell_launcher/features/preferences/domain/repositories/preferences_repository.dart';
|
||||||
import 'package:moonwell_launcher/service_container.dart';
|
import 'package:moonwell_launcher/service_container.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
|
||||||
|
|
||||||
class HomeScreen extends StatelessWidget {
|
class HomeScreen extends StatelessWidget {
|
||||||
const HomeScreen({super.key, required this.session, required this.manifest});
|
const HomeScreen({super.key, required this.session, required this.manifest});
|
||||||
@@ -38,8 +36,8 @@ class HomeScreen extends StatelessWidget {
|
|||||||
listener: (context, state) async {
|
listener: (context, state) async {
|
||||||
await Navigator.of(context).pushReplacement(
|
await Navigator.of(context).pushReplacement(
|
||||||
PageRouteBuilder(
|
PageRouteBuilder(
|
||||||
pageBuilder: (_, __, ___) => const LoginScreen(),
|
pageBuilder: (_, _, _) => const LoginScreen(),
|
||||||
transitionsBuilder: (_, animation, __, child) {
|
transitionsBuilder: (_, animation, _, child) {
|
||||||
return FadeTransition(opacity: animation, child: child);
|
return FadeTransition(opacity: animation, child: child);
|
||||||
},
|
},
|
||||||
transitionDuration: const Duration(milliseconds: 250),
|
transitionDuration: const Duration(milliseconds: 250),
|
||||||
@@ -49,11 +47,9 @@ class HomeScreen extends StatelessWidget {
|
|||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
// Background image
|
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Image.asset('assets/background.png', fit: BoxFit.cover),
|
child: Image.asset('assets/background.png', fit: BoxFit.cover),
|
||||||
),
|
),
|
||||||
// Gradient overlay
|
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: DecoratedBox(
|
child: DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -70,81 +66,16 @@ class HomeScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Main content
|
|
||||||
const Positioned.fill(child: HomeScreenContent()),
|
const Positioned.fill(child: HomeScreenContent()),
|
||||||
// Custom title bar
|
const Positioned(
|
||||||
const Positioned(top: 0, left: 0, right: 0, child: _TitleBar()),
|
top: 0,
|
||||||
],
|
left: 0,
|
||||||
),
|
right: 0,
|
||||||
),
|
child: WindowTitleBar(),
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:moonwell_launcher/app/home_screen/home_screen.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/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/launcher/data/launcher_api_client.dart';
|
||||||
import 'package:moonwell_launcher/features/preferences/domain/repositories/preferences_repository.dart';
|
import 'package:moonwell_launcher/features/preferences/domain/repositories/preferences_repository.dart';
|
||||||
import 'package:moonwell_launcher/service_container.dart';
|
import 'package:moonwell_launcher/service_container.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
|
||||||
|
|
||||||
class LoginScreen extends StatefulWidget {
|
class LoginScreen extends StatefulWidget {
|
||||||
const LoginScreen({super.key});
|
const LoginScreen({super.key});
|
||||||
@@ -53,9 +51,9 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
|
|
||||||
await Navigator.of(context).pushReplacement(
|
await Navigator.of(context).pushReplacement(
|
||||||
PageRouteBuilder(
|
PageRouteBuilder(
|
||||||
pageBuilder: (_, __, ___) =>
|
pageBuilder: (_, _, _) =>
|
||||||
HomeScreen(session: session, manifest: manifest),
|
HomeScreen(session: session, manifest: manifest),
|
||||||
transitionsBuilder: (_, animation, __, child) {
|
transitionsBuilder: (_, animation, _, child) {
|
||||||
return FadeTransition(opacity: animation, child: child);
|
return FadeTransition(opacity: animation, child: child);
|
||||||
},
|
},
|
||||||
transitionDuration: const Duration(milliseconds: 400),
|
transitionDuration: const Duration(milliseconds: 400),
|
||||||
@@ -83,7 +81,6 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
// Background
|
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Image.asset('assets/background.png', fit: BoxFit.cover),
|
child: Image.asset('assets/background.png', fit: BoxFit.cover),
|
||||||
),
|
),
|
||||||
@@ -101,23 +98,19 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Title bar
|
const Positioned(top: 0, left: 0, right: 0, child: WindowTitleBar()),
|
||||||
Positioned(top: 0, left: 0, right: 0, child: _TitleBar(cs: cs)),
|
|
||||||
// Login form — centered
|
|
||||||
Center(
|
Center(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 340,
|
width: 340,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// Logo
|
|
||||||
Image.asset(
|
Image.asset(
|
||||||
'assets/logo.png',
|
'assets/logo.png',
|
||||||
height: 160,
|
height: 160,
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
// Card
|
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(28),
|
padding: const EdgeInsets.all(28),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -194,70 +187,3 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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<void> _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)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-440
@@ -1,11 +1,8 @@
|
|||||||
// main.dart
|
// main.dart
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
|
||||||
import 'package:flutter/foundation.dart'
|
import 'package:flutter/material.dart';
|
||||||
show defaultTargetPlatform, TargetPlatform;
|
|
||||||
import 'package:moonwell_launcher/app/mw_app.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:moonwell_launcher/service_container.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
@@ -32,438 +29,3 @@ Future<void> main() async {
|
|||||||
|
|
||||||
runApp(const MoonWellApp());
|
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<LauncherHome> createState() => _LauncherHomeState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LauncherHomeState extends State<LauncherHome> {
|
|
||||||
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<MoonWellDecorations>()!;
|
|
||||||
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<MoonWellDecorations>()!;
|
|
||||||
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<MoonWellDecorations>()!;
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user