lum_splash_video/example/lib/main.dart

892 lines
29 KiB
Dart

import 'package:flutter/material.dart';
import 'package:splash_video/splash_video.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
// Initialize SplashVideo: defer first frame & setup MediaKit
// This is safe because our first screen IS a SplashVideoPage
SplashVideo.initialize();
runApp(const MyApp());
}
// Cyberpunk color palette
class CyberpunkColors {
static const neonPink = Color(0xFFFF006E);
static const neonCyan = Color(0xFF00F5FF);
static const neonPurple = Color(0xFFB76CFF);
static const neonYellow = Color(0xFFFFED4E);
static const darkBg = Color(0xFF0A0E27);
static const darkerBg = Color(0xFF050816);
static const glowBlue = Color(0xFF1B1464);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Splash Video Example',
debugShowCheckedModeBanner: false,
theme: ThemeData(
scaffoldBackgroundColor: CyberpunkColors.darkBg,
colorScheme: ColorScheme.dark(
primary: CyberpunkColors.neonCyan,
secondary: CyberpunkColors.neonPink,
// surface: CyberpunkColors.darkBg,
surface: CyberpunkColors.glowBlue,
),
textTheme: const TextTheme(
bodyLarge: TextStyle(color: Colors.white, fontWeight: FontWeight.w300),
bodyMedium: TextStyle(color: Colors.white70, fontWeight: FontWeight.w300),
),
useMaterial3: true,
),
// Show splash video first, then auto-navigate to selector
home: const InitialSplashExample(),
);
}
}
/// Initial splash screen shown on app startup
class InitialSplashExample extends StatelessWidget {
const InitialSplashExample({super.key});
@override
Widget build(BuildContext context) {
return SplashVideo(
source: AssetSource(ExampleSelector.kFilePath),
backgroundColor: Colors.black,
nextScreen: const ExampleSelector(),
videoConfig: const VideoConfig(
scale: VideoScaleMode.cover, // Fill screen, maintain aspect, may crop edges
),
onVideoError: (error) {
debugPrint('Initial Splash Video Error: $error');
// Navigate to selector even on error
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => const ExampleSelector(),
),
);
},
);
}
}
/// Example selector screen to choose different splash examples
class ExampleSelector extends StatelessWidget {
const ExampleSelector({super.key});
static const kFilePath = 'assets/splash.mp4';
static const backgroundColor = Colors.white;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: CyberpunkColors.darkerBg,
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
CyberpunkColors.darkerBg,
CyberpunkColors.darkBg,
CyberpunkColors.glowBlue.withValues(alpha: 0.3),
],
),
),
child: SafeArea(
child: Column(
children: [
const SizedBox(height: 40),
// Cyberpunk header
ShaderMask(
shaderCallback: (bounds) => LinearGradient(
colors: [CyberpunkColors.neonCyan, CyberpunkColors.neonPink],
).createShader(bounds),
child: const Text(
'⚡ SPLASH VIDEO ⚡',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.w900,
color: Colors.white,
letterSpacing: 3,
),
),
),
const SizedBox(height: 10),
const Text(
'CYBERPUNK EDITION',
style: TextStyle(
fontSize: 14,
color: CyberpunkColors.neonYellow,
letterSpacing: 4,
fontWeight: FontWeight.w300,
),
),
const SizedBox(height: 40),
Expanded(
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 20),
children: [
_buildCyberpunkTile(
context,
'AUTO-NAVIGATE',
'Video plays then auto-navigates to home',
Icons.rocket_launch,
CyberpunkColors.neonCyan,
() => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const AutoNavigateExample(),
),
),
),
const SizedBox(height: 16),
_buildCyberpunkTile(
context,
'SCALE MODES DEMO',
'Compare all 7 video scale modes',
Icons.aspect_ratio,
CyberpunkColors.neonYellow,
() => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const ScaleModesExample(),
),
),
),
const SizedBox(height: 16),
_buildCyberpunkTile(
context,
'MANUAL CONTROL',
'Use onVideoComplete for custom logic',
Icons.control_camera,
CyberpunkColors.neonPurple,
() => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const ManualControlExample(),
),
),
),
const SizedBox(height: 16),
_buildCyberpunkTile(
context,
'INFINITE LOOP',
'Video loops until user taps skip',
Icons.all_inclusive,
CyberpunkColors.neonPink,
() => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const LoopingExample(),
),
),
),
const SizedBox(height: 16),
_buildCyberpunkTile(
context,
'OVERLAY MODE',
'Title, footer, and custom overlays',
Icons.layers,
CyberpunkColors.neonYellow,
() => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const OverlayExample(),
),
),
),
],
),
),
],
),
),
),
);
}
Widget _buildCyberpunkTile(
BuildContext context,
String title,
String subtitle,
IconData icon,
Color accentColor,
VoidCallback onTap,
) {
return InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: CyberpunkColors.darkBg.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: accentColor.withValues(alpha: 0.5),
width: 2,
),
boxShadow: [
BoxShadow(
color: accentColor.withValues(alpha: 0.3),
blurRadius: 20,
spreadRadius: -5,
),
],
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: accentColor.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: accentColor.withValues(alpha: 0.5),
blurRadius: 15,
),
],
),
child: Icon(
icon,
color: accentColor,
size: 32,
),
),
const SizedBox(width: 20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: accentColor,
letterSpacing: 1.5,
),
),
const SizedBox(height: 6),
Text(
subtitle,
style: const TextStyle(
fontSize: 13,
color: Colors.white70,
fontWeight: FontWeight.w300,
),
),
],
),
),
Icon(
Icons.arrow_forward_ios,
color: accentColor.withValues(alpha: 0.7),
size: 20,
),
],
),
),
);
}
}
/// Example 1: Auto-navigate to next screen when video completes
class AutoNavigateExample extends StatelessWidget {
const AutoNavigateExample({super.key});
@override
Widget build(BuildContext context) {
return SplashVideo(
source: AssetSource(ExampleSelector.kFilePath),
nextScreen: const HomeScreen(title: 'Auto-Navigate Example'),
backgroundColor: Colors.black,
videoConfig: const VideoConfig(
scale: VideoScaleMode.cover,
),
onVideoError: (error) {
debugPrint('Video Error in AutoNavigateExample: $error');
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => const HomeScreen(title: 'Auto-Navigate (Error)'),
),
);
},
);
}
}
/// Example 2: Manual control with onVideoComplete callback
class ManualControlExample extends StatelessWidget {
const ManualControlExample({super.key});
@override
Widget build(BuildContext context) {
return SplashVideo(
source: AssetSource(ExampleSelector.kFilePath),
backgroundColor: Colors.black,
onVideoError: (error) {
debugPrint('Video Error in ManualControlExample: $error');
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => const HomeScreen(title: 'Manual Control (Error)'),
),
);
},
onVideoComplete: () {
// Custom logic before navigation
debugPrint('Video completed!');
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => const HomeScreen(title: 'Manual Control Example'),
),
);
},
);
}
}
/// Example 3: Looping video with manual skip
class LoopingExample extends StatefulWidget {
const LoopingExample({super.key});
@override
State<LoopingExample> createState() => _LoopingExampleState();
}
class _LoopingExampleState extends State<LoopingExample> {
late final SplashVideoController controller;
@override
void initState() {
super.initState();
controller = SplashVideoController(loopVideo: true);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
void _onSkip() {
controller.skip();
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => const HomeScreen(title: 'Looping Example'),
),
);
}
@override
Widget build(BuildContext context) {
return SplashVideo(
source: AssetSource(ExampleSelector.kFilePath),
controller: controller,
backgroundColor: Colors.black,
onVideoError: (error) {
debugPrint('LoopingExample - Video Error: $error');
// Navigate to home screen on error
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => const HomeScreen(title: 'Looping Example (Error)'),
),
);
},
footerWidget: Padding(
padding: const EdgeInsets.all(20.0),
child: Center(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [CyberpunkColors.neonCyan, CyberpunkColors.neonPurple],
),
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: CyberpunkColors.neonCyan.withValues(alpha: 0.5),
blurRadius: 20,
spreadRadius: 2,
),
],
),
child: ElevatedButton.icon(
onPressed: _onSkip,
icon: const Icon(Icons.skip_next, color: Colors.black),
label: const Text(
'SKIP',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15),
),
),
),
),
),
);
}
}
/// Example: Compare all video scale modes
class ScaleModesExample extends StatefulWidget {
const ScaleModesExample({super.key});
@override
State<ScaleModesExample> createState() => _ScaleModesExampleState();
}
class _ScaleModesExampleState extends State<ScaleModesExample> {
VideoScaleMode currentScaleMode = VideoScaleMode.cover;
final controller = SplashVideoController(loopVideo: true);
final List<(VideoScaleMode, String, String)> scaleModes = [
(VideoScaleMode.cover, 'COVER', 'Fill screen, maintain aspect, may crop'),
(VideoScaleMode.contain, 'CONTAIN', 'Scale to fit inside, maintain aspect, letterbox'),
(VideoScaleMode.fill, 'FILL', 'Stretch to fill (ignores aspect)'),
(VideoScaleMode.fitWidth, 'SCALE WIDTH', 'Match width, maintain aspect'),
(VideoScaleMode.fitHeight, 'SCALE HEIGHT', 'Match height, maintain aspect'),
(VideoScaleMode.scaleDown, 'SCALE DOWN', 'Like contain, but no upscaling'),
(VideoScaleMode.none, 'NONE', 'Original size, centered'),
];
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
// Video player with current scale mode
SplashVideo(
key: ValueKey(currentScaleMode), // Force rebuild when mode changes
source: AssetSource(ExampleSelector.kFilePath),
controller: controller,
backgroundColor: Colors.black,
videoConfig: VideoConfig(
scale: currentScaleMode,
playImmediately: true,
),
),
// Scale mode selector overlay
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withValues(alpha: 0.9),
],
),
),
padding: const EdgeInsets.all(20),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'VIDEO SCALE MODES',
style: TextStyle(
color: CyberpunkColors.neonCyan,
fontSize: 16,
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
const SizedBox(height: 16),
SizedBox(
height: 120,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: scaleModes.length,
itemBuilder: (context, index) {
final (mode, title, desc) = scaleModes[index];
final isSelected = mode == currentScaleMode;
return Padding(
padding: const EdgeInsets.only(right: 12),
child: GestureDetector(
onTap: () => setState(() => currentScaleMode = mode),
child: Container(
width: 140,
decoration: BoxDecoration(
border: Border.all(
color: isSelected
? CyberpunkColors.neonCyan
: Colors.white24,
width: isSelected ? 2 : 1,
),
borderRadius: BorderRadius.circular(12),
color: isSelected
? CyberpunkColors.neonCyan.withValues(alpha: 0.2)
: Colors.white.withValues(alpha: 0.05),
),
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
title,
style: TextStyle(
color: isSelected
? CyberpunkColors.neonCyan
: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
desc,
style: TextStyle(
color: Colors.white70,
fontSize: 11,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
);
},
),
),
],
),
),
),
),
// Back button
Positioned(
top: 40,
left: 16,
child: SafeArea(
child: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
style: IconButton.styleFrom(
backgroundColor: Colors.black.withValues(alpha: 0.5),
),
),
),
),
],
),
);
}
}
/// Example 4: Video with title, footer, and custom overlay
class OverlayExample extends StatelessWidget {
const OverlayExample({super.key});
@override
Widget build(BuildContext context) {
return SplashVideo(
source: AssetSource(ExampleSelector.kFilePath),
nextScreen: const HomeScreen(title: 'Overlay Example'),
backgroundColor: Colors.black,
onVideoError: (error) {
debugPrint('OverlayExample - Video Error: $error');
// Navigate to home screen on error
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => const HomeScreen(title: 'Overlay Example (Error)'),
),
);
},
titleWidget: const Padding(
padding: EdgeInsets.all(20.0),
child: Center(
child: _CyberpunkTitle(),
),
),
footerWidget: Padding(
padding: const EdgeInsets.all(20.0),
child: Center(
child: Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [CyberpunkColors.neonPink, CyberpunkColors.neonCyan],
),
borderRadius: BorderRadius.circular(50),
),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: CyberpunkColors.darkerBg,
borderRadius: BorderRadius.circular(50),
),
child: const CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(CyberpunkColors.neonCyan),
strokeWidth: 3,
),
),
),
),
),
overlayBuilder: (context) => Positioned(
right: 20,
top: 100,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
CyberpunkColors.neonPurple.withValues(alpha: 0.3),
CyberpunkColors.neonCyan.withValues(alpha: 0.3),
],
),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: CyberpunkColors.neonCyan.withValues(alpha: 0.5),
width: 1,
),
boxShadow: [
BoxShadow(
color: CyberpunkColors.neonCyan.withValues(alpha: 0.5),
blurRadius: 10,
),
],
),
child: const Text(
'v1.0.0',
style: TextStyle(
color: CyberpunkColors.neonCyan,
fontSize: 12,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
),
),
);
}
}
/// Home screen that's shown after splash
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key, required this.title});
final String title;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: CyberpunkColors.darkerBg,
body: Container(
decoration: BoxDecoration(
gradient: RadialGradient(
center: Alignment.center,
radius: 1.5,
colors: [
CyberpunkColors.glowBlue.withValues(alpha: 0.3),
CyberpunkColors.darkerBg,
],
),
),
child: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Success icon with glow
Container(
padding: const EdgeInsets.all(30),
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
CyberpunkColors.neonCyan.withValues(alpha: 0.3),
Colors.transparent,
],
),
boxShadow: [
BoxShadow(
color: CyberpunkColors.neonCyan.withValues(alpha: 0.5),
blurRadius: 50,
spreadRadius: 10,
),
],
),
child: const Icon(
Icons.check_circle,
size: 100,
color: CyberpunkColors.neonCyan,
),
),
const SizedBox(height: 40),
ShaderMask(
shaderCallback: (bounds) => LinearGradient(
colors: [CyberpunkColors.neonCyan, CyberpunkColors.neonPink],
).createShader(bounds),
child: Text(
title.toUpperCase(),
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 2,
),
),
),
const SizedBox(height: 20),
Text(
'SPLASH VIDEO COMPLETED',
style: TextStyle(
fontSize: 16,
color: CyberpunkColors.neonYellow,
letterSpacing: 3,
fontWeight: FontWeight.w300,
),
),
const SizedBox(height: 60),
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [CyberpunkColors.neonPink, CyberpunkColors.neonPurple],
),
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: CyberpunkColors.neonPink.withValues(alpha: 0.5),
blurRadius: 20,
spreadRadius: 2,
),
],
),
child: ElevatedButton.icon(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.arrow_back, color: Colors.black),
label: const Text(
'BACK TO EXAMPLES',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 18),
),
),
),
],
),
),
),
),
);
}
}
// Animated cyberpunk title widget
class _CyberpunkTitle extends StatefulWidget {
const _CyberpunkTitle();
@override
State<_CyberpunkTitle> createState() => _CyberpunkTitleState();
}
class _CyberpunkTitleState extends State<_CyberpunkTitle>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
)..repeat(reverse: true);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
CyberpunkColors.neonCyan.withValues(alpha: 0.2 + _controller.value * 0.3),
CyberpunkColors.neonPink.withValues(alpha: 0.2 + _controller.value * 0.3),
],
),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: CyberpunkColors.neonCyan.withValues(alpha: 0.5 + _controller.value * 0.5),
width: 2,
),
boxShadow: [
BoxShadow(
color: CyberpunkColors.neonCyan.withValues(alpha: 0.3 + _controller.value * 0.4),
blurRadius: 20 + _controller.value * 10,
spreadRadius: 5,
),
],
),
child: ShaderMask(
shaderCallback: (bounds) => LinearGradient(
colors: [CyberpunkColors.neonCyan, CyberpunkColors.neonPink],
).createShader(bounds),
child: const Text(
'⚡ WELCOME TO THE FUTURE ⚡',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.w900,
color: Colors.white,
letterSpacing: 2,
),
),
),
);
},
);
}
}