diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml
index f74085f..c887eb5 100644
--- a/android/app/src/main/res/drawable-v21/launch_background.xml
+++ b/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -1,12 +1,8 @@
-
-
-
-
-
+ -
+
+
+
+
diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml
index 304732f..c887eb5 100644
--- a/android/app/src/main/res/drawable/launch_background.xml
+++ b/android/app/src/main/res/drawable/launch_background.xml
@@ -1,12 +1,8 @@
-
-
-
-
-
+ -
+
+
+
+
diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard
index f2e259c..a4ee227 100644
--- a/ios/Runner/Base.lproj/LaunchScreen.storyboard
+++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -19,7 +19,7 @@
-
+
diff --git a/lib/app.dart b/lib/app.dart
index 8b1f83a..cfd596a 100644
--- a/lib/app.dart
+++ b/lib/app.dart
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'l10n/gen/app_localizations.dart';
-import 'ui/screens/home_screen.dart';
+import 'ui/screens/splash_screen.dart';
class BlockSeasonsApp extends StatelessWidget {
const BlockSeasonsApp({super.key});
@@ -26,7 +26,7 @@ class BlockSeasonsApp extends StatelessWidget {
),
useMaterial3: true,
),
- home: const HomeScreen(),
+ home: const SplashScreen(),
);
}
}
diff --git a/lib/ui/screens/splash_screen.dart b/lib/ui/screens/splash_screen.dart
new file mode 100644
index 0000000..4253827
--- /dev/null
+++ b/lib/ui/screens/splash_screen.dart
@@ -0,0 +1,123 @@
+import 'package:flutter/material.dart';
+
+import 'home_screen.dart';
+
+/// Logo-assembly splash: four glossy blocks fly in to form a 2x2 mark, the
+/// wordmark fades in, then we hand off. SaveRepository is already opened in
+/// main() so this doubles as perceived-zero loading time.
+class SplashScreen extends StatefulWidget {
+ const SplashScreen({super.key});
+
+ /// Where the splash goes when finished; the season title card task
+ /// repoints this.
+ static Widget Function() nextScreen = () => const HomeScreen();
+
+ @override
+ State createState() => _SplashScreenState();
+}
+
+class _SplashScreenState extends State
+ with SingleTickerProviderStateMixin {
+ late final AnimationController _c = AnimationController(
+ vsync: this,
+ duration: const Duration(milliseconds: 1900),
+ )..addStatusListener((status) {
+ if (status == AnimationStatus.completed && mounted) {
+ Navigator.of(context).pushReplacement(
+ MaterialPageRoute(builder: (_) => SplashScreen.nextScreen()),
+ );
+ }
+ });
+
+ /// (color, fly-in direction unit, 2x2 slot unit) per block.
+ static const _blocks = [
+ (Color(0xFFFF7EB3), Offset(-1.2, -0.4), Offset(-0.5, -0.5)),
+ (Color(0xFFFFD166), Offset(1.2, -0.4), Offset(0.5, -0.5)),
+ (Color(0xFF6FCDF5), Offset(-1.2, 0.6), Offset(-0.5, 0.5)),
+ (Color(0xFF7EDB9C), Offset(1.2, 0.6), Offset(0.5, 0.5)),
+ ];
+
+ @override
+ void initState() {
+ super.initState();
+ _c.forward();
+ }
+
+ @override
+ void dispose() {
+ _c.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ const blockSize = 46.0;
+ const gap = 3.0;
+ return Scaffold(
+ backgroundColor: const Color(0xFF0E1430),
+ body: AnimatedBuilder(
+ animation: _c,
+ builder: (context, _) {
+ final titleT = const Interval(0.60, 0.88, curve: Curves.easeOut)
+ .transform(_c.value);
+ return Stack(
+ alignment: Alignment.center,
+ children: [
+ for (var i = 0; i < _blocks.length; i++)
+ _block(i, blockSize, gap),
+ Transform.translate(
+ offset: Offset(0, 78 + 12 * (1 - titleT)),
+ child: Opacity(
+ opacity: titleT,
+ child: const Text(
+ 'BLOCK SEASONS',
+ style: TextStyle(
+ fontSize: 26,
+ fontWeight: FontWeight.w900,
+ letterSpacing: 4,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ },
+ ),
+ );
+ }
+
+ Widget _block(int i, double size, double gap) {
+ final (color, from, to) = _blocks[i];
+ final t = Interval(0.06 * i, 0.45 + 0.06 * i, curve: Curves.easeOutBack)
+ .transform(_c.value);
+ final begin = Offset(from.dx * 160, from.dy * 280);
+ final end = Offset(to.dx * (size + gap), to.dy * (size + gap));
+ final pos = Offset.lerp(begin, end, t)!;
+ return Transform.translate(
+ offset: pos,
+ child: Container(
+ width: size,
+ height: size,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(11),
+ gradient: LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: [
+ Color.lerp(color, Colors.white, 0.28)!,
+ color,
+ Color.lerp(color, Colors.black, 0.22)!,
+ ],
+ ),
+ boxShadow: [
+ BoxShadow(
+ color: color.withValues(alpha: 0.55),
+ blurRadius: 18,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/test/widget_test.dart b/test/widget_test.dart
index 05be38c..8183482 100644
--- a/test/widget_test.dart
+++ b/test/widget_test.dart
@@ -16,6 +16,10 @@ void main() {
child: const BlockSeasonsApp(),
),
);
+ // Splash (~1.9s) → (later: season title card) → home.
+ await tester.pump(const Duration(milliseconds: 2100));
+ await tester.pump(const Duration(milliseconds: 2000));
+ await tester.pump(const Duration(milliseconds: 2000));
await tester.pumpAndSettle();
expect(find.text('Block Seasons'), findsOneWidget);