Created
December 24, 2025 19:55
-
-
Save slightfoot/627c6e480adfb0fc1190aea816eff6fe to your computer and use it in GitHub Desktop.
Snow Fall - by Simon Lightfoot :: #HumpdayQandA Christmas Special on 24th November 2025 :: https://www.youtube.com/watch?v=GpDVJvLhPec
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // MIT License | |
| // | |
| // Copyright (c) 2025 Simon Lightfoot | |
| // | |
| // Permission is hereby granted, free of charge, to any person obtaining a copy | |
| // of this software and associated documentation files (the "Software"), to deal | |
| // in the Software without restriction, including without limitation the rights | |
| // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| // copies of the Software, and to permit persons to whom the Software is | |
| // furnished to do so, subject to the following conditions: | |
| // | |
| // The above copyright notice and this permission notice shall be included in all | |
| // copies or substantial portions of the Software. | |
| // | |
| // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| // SOFTWARE. | |
| // | |
| import 'dart:math'; | |
| import 'package:flutter/material.dart'; | |
| import 'package:flutter/rendering.dart'; | |
| void main() { | |
| runApp(const App()); | |
| } | |
| class App extends StatefulWidget { | |
| const App({super.key}); | |
| @override | |
| State<App> createState() => _AppState(); | |
| } | |
| class _AppState extends State<App> { | |
| @override | |
| Widget build(BuildContext context) { | |
| return MaterialApp( | |
| debugShowCheckedModeBanner: false, | |
| home: Scaffold( | |
| backgroundColor: const Color(0xFF1a1a2e), | |
| body: SnowFall( | |
| numberOfFlakes: 150, | |
| child: LoginScreenContent(), | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| // Snowfall Widget | |
| class SnowFall extends StatefulWidget { | |
| const SnowFall({ | |
| super.key, | |
| required this.numberOfFlakes, | |
| required this.child, | |
| }); | |
| final int numberOfFlakes; | |
| final Widget child; | |
| @override | |
| State<SnowFall> createState() => _SnowFallState(); | |
| } | |
| class _SnowFallState extends State<SnowFall> with SingleTickerProviderStateMixin { | |
| final Map<String, SnowSurface> _surfaces = {}; | |
| void registerSurface(String id, BuildContext context, {required double maxSnowDepth}) { | |
| _surfaces[id] = SnowSurface( | |
| id: id, | |
| context: context, | |
| maxSnowDepth: maxSnowDepth, | |
| ); | |
| } | |
| void unregisterSurface(String id) { | |
| _surfaces.remove(id); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| return FallingSnow( | |
| numberOfFlakes: widget.numberOfFlakes, | |
| vsync: this, | |
| surfaces: _surfaces, | |
| child: widget.child, | |
| ); | |
| } | |
| } | |
| class FallingSnow extends SingleChildRenderObjectWidget { | |
| const FallingSnow({ | |
| super.key, | |
| this.numberOfFlakes = 50, | |
| this.minSize = 1.0, | |
| this.maxSize = 6.0, | |
| this.minSpeed = 1.0, | |
| this.maxSpeed = 3.0, | |
| required this.vsync, | |
| required this.surfaces, | |
| required Widget child, | |
| }) : super(child: child); | |
| final int numberOfFlakes; | |
| final double minSize; | |
| final double maxSize; | |
| final double minSpeed; | |
| final double maxSpeed; | |
| final TickerProvider vsync; | |
| final Map<String, SnowSurface> surfaces; | |
| @override | |
| RenderObject createRenderObject(BuildContext context) { | |
| return RenderFallingSnow( | |
| numberOfFlakes: numberOfFlakes, | |
| minSize: minSize, | |
| maxSize: maxSize, | |
| minSpeed: minSpeed, | |
| maxSpeed: maxSpeed, | |
| surfaces: surfaces, | |
| vsync: vsync, | |
| ); | |
| } | |
| @override | |
| void updateRenderObject(BuildContext context, RenderFallingSnow renderObject) { | |
| renderObject | |
| ..numberOfFlakes = numberOfFlakes | |
| ..minSize = minSize | |
| ..maxSize = maxSize | |
| ..minSpeed = minSpeed | |
| ..maxSpeed = maxSpeed | |
| ..surfaces = surfaces; | |
| } | |
| } | |
| /// Widget that registers its position with the parent FallingSnow widget | |
| /// to allow snow to accumulate on it | |
| class SnowCatcher extends StatefulWidget { | |
| const SnowCatcher({ | |
| super.key, | |
| required this.id, | |
| required this.child, | |
| this.maxSnowDepth = 20.0, | |
| }); | |
| final String id; | |
| final Widget child; | |
| final double maxSnowDepth; | |
| @override | |
| State<SnowCatcher> createState() => _SnowCatcherState(); | |
| } | |
| class _SnowCatcherState extends State<SnowCatcher> { | |
| late _SnowFallState _snowFallState; | |
| void _registerWithSnowRenderer() { | |
| _snowFallState = context.findAncestorStateOfType<_SnowFallState>()!; | |
| _snowFallState.registerSurface( | |
| widget.id, | |
| context, | |
| maxSnowDepth: widget.maxSnowDepth, | |
| ); | |
| } | |
| void _unregisterWithSnowRenderer(String id) { | |
| _snowFallState.unregisterSurface(id); | |
| } | |
| @override | |
| void didChangeDependencies() { | |
| super.didChangeDependencies(); | |
| _registerWithSnowRenderer(); | |
| } | |
| @override | |
| void didUpdateWidget(SnowCatcher oldWidget) { | |
| super.didUpdateWidget(oldWidget); | |
| if (oldWidget.id != widget.id) { | |
| _unregisterWithSnowRenderer(oldWidget.id); | |
| _registerWithSnowRenderer(); | |
| } | |
| } | |
| @override | |
| void dispose() { | |
| _unregisterWithSnowRenderer(widget.id); | |
| super.dispose(); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| return widget.child; | |
| } | |
| } | |
| class Snowflake { | |
| Snowflake({ | |
| required this.x, | |
| required this.y, | |
| required this.size, | |
| required this.speed, | |
| required this.swingOffset, | |
| required this.swingAmount, | |
| required this.swingSpeed, | |
| }); | |
| double x; // Horizontal position (0.0 to 1.0) | |
| double y; // Vertical position (0.0 to 1.0) | |
| final double size; | |
| final double speed; | |
| final double swingOffset; | |
| final double swingAmount; | |
| final double swingSpeed; | |
| double time = 0.0; // Track time for sine wave | |
| bool isLanded = false; // Whether the snowflake has landed | |
| Offset? landedPosition; // Absolute position where it landed | |
| } | |
| class SnowSurface { | |
| SnowSurface({ | |
| required this.id, | |
| required this.context, | |
| this.maxSnowDepth = 20.0, | |
| }); | |
| final String id; | |
| final BuildContext context; | |
| final double maxSnowDepth; | |
| final List<Snowflake> accumulatedSnow = []; | |
| bool get isFull => accumulatedSnow.length >= maxSnowDepth; | |
| } | |
| class RenderFallingSnow extends RenderProxyBox { | |
| RenderFallingSnow({ | |
| required int numberOfFlakes, | |
| required double minSize, | |
| required double maxSize, | |
| required double minSpeed, | |
| required double maxSpeed, | |
| required Map<String, SnowSurface> surfaces, | |
| required TickerProvider vsync, | |
| }) : _numberOfFlakes = numberOfFlakes, | |
| _minSize = minSize, | |
| _maxSize = maxSize, | |
| _minSpeed = minSpeed, | |
| _maxSpeed = maxSpeed, | |
| _surfaces = surfaces { | |
| _initializeSnowflakes(); | |
| _controller = AnimationController( | |
| vsync: vsync, | |
| duration: const Duration(days: 1), | |
| )..repeat(); | |
| _controller.addListener(markNeedsPaint); | |
| } | |
| late AnimationController _controller; | |
| late List<Snowflake> _snowflakes; | |
| final Random _random = Random(); | |
| int _numberOfFlakes; | |
| double _minSize; | |
| double _maxSize; | |
| double _minSpeed; | |
| double _maxSpeed; | |
| Map<String, SnowSurface> _surfaces; | |
| int get numberOfFlakes => _numberOfFlakes; | |
| set numberOfFlakes(int value) { | |
| if (_numberOfFlakes == value) return; | |
| _numberOfFlakes = value; | |
| _initializeSnowflakes(); | |
| markNeedsPaint(); | |
| } | |
| double get minSize => _minSize; | |
| set minSize(double value) { | |
| if (_minSize == value) return; | |
| _minSize = value; | |
| _initializeSnowflakes(); | |
| markNeedsPaint(); | |
| } | |
| double get maxSize => _maxSize; | |
| set maxSize(double value) { | |
| if (_maxSize == value) return; | |
| _maxSize = value; | |
| markNeedsPaint(); | |
| } | |
| double get minSpeed => _minSpeed; | |
| set minSpeed(double value) { | |
| if (_minSpeed == value) return; | |
| _minSpeed = value; | |
| _initializeSnowflakes(); | |
| markNeedsPaint(); | |
| } | |
| double get maxSpeed => _maxSpeed; | |
| set maxSpeed(double value) { | |
| if (_maxSpeed == value) return; | |
| _maxSpeed = value; | |
| _initializeSnowflakes(); | |
| markNeedsPaint(); | |
| } | |
| Map<String, SnowSurface> get surfaces => _surfaces; | |
| set surfaces(Map<String, SnowSurface> value) { | |
| if (_surfaces == value) return; | |
| _surfaces = value; | |
| markNeedsPaint(); | |
| } | |
| void _initializeSnowflakes() { | |
| _snowflakes = List.generate( | |
| _numberOfFlakes, | |
| (index) => _createSnowflake(), | |
| ); | |
| } | |
| Snowflake _createSnowflake({double? y}) { | |
| return Snowflake( | |
| x: _random.nextDouble(), | |
| y: y ?? _random.nextDouble(), | |
| size: _minSize + _random.nextDouble() * (_maxSize - _minSize), | |
| speed: _minSpeed + _random.nextDouble() * (_maxSpeed - _minSpeed), | |
| swingOffset: _random.nextDouble() * 2 * pi, | |
| swingAmount: _random.nextDouble() * 0.05 + 0.02, | |
| swingSpeed: _random.nextDouble() * 2.0 + 1.0, | |
| ); | |
| } | |
| @override | |
| void paint(PaintingContext context, Offset offset) { | |
| // Paint child first | |
| if (child != null) { | |
| context.paintChild(child!, offset); | |
| } | |
| // Paint snowflakes on top | |
| final canvas = context.canvas; | |
| canvas.save(); | |
| canvas.translate(offset.dx, offset.dy); | |
| final paint = Paint() | |
| ..color = Colors.white | |
| ..style = PaintingStyle.fill; | |
| for (final flake in List.of(_snowflakes)) { | |
| if (flake.isLanded && flake.landedPosition != null) { | |
| // Draw landed snowflake at its fixed position | |
| paint.color = Colors.white.withValues(alpha: (flake.size / _maxSize)); | |
| canvas.drawCircle( | |
| flake.landedPosition!, | |
| flake.size, | |
| paint, | |
| ); | |
| continue; | |
| } | |
| // Update time for sine wave | |
| flake.time += flake.swingSpeed * 0.02; | |
| // Update vertical position | |
| flake.y += flake.speed * 0.001; | |
| // Update horizontal position with sine wave | |
| flake.x += sin(flake.time + flake.swingOffset) * flake.swingAmount * 0.02; | |
| // Calculate absolute position | |
| final x = flake.x * size.width; | |
| final y = flake.y * size.height; | |
| final flakePos = Offset(x, y); | |
| // Check collision with surfaces | |
| bool landed = false; | |
| for (final surface in _surfaces.values) { | |
| if (surface.isFull) continue; | |
| // First check collision with accumulated snowflakes on this surface | |
| bool landedOnSnow = false; | |
| for (final landedFlake in surface.accumulatedSnow) { | |
| if (landedFlake.landedPosition != null) { | |
| if (_checkSnowflakeCollision( | |
| flakePos, | |
| flake.size, | |
| landedFlake.landedPosition!, | |
| landedFlake.size, | |
| )) { | |
| // Land on top of another snowflake | |
| flake.isLanded = true; | |
| flake.landedPosition = flakePos; | |
| surface.accumulatedSnow.add(flake); | |
| landedOnSnow = true; | |
| landed = true; | |
| break; | |
| } | |
| } | |
| } | |
| if (landedOnSnow) { | |
| // Check if surface just became full | |
| if (surface.isFull) { | |
| // Release all accumulated snowflakes | |
| for (final landedFlake in surface.accumulatedSnow) { | |
| // Convert landed position back to normalized coordinates | |
| if (landedFlake.landedPosition != null) { | |
| landedFlake.x = landedFlake.landedPosition!.dx / size.width; | |
| landedFlake.y = landedFlake.landedPosition!.dy / size.height; | |
| } | |
| // Reset the flake to continue falling | |
| landedFlake.isLanded = false; | |
| landedFlake.landedPosition = null; | |
| landedFlake.time = 0.0; | |
| } | |
| // Clear the surface | |
| surface.accumulatedSnow.clear(); | |
| } | |
| break; | |
| } | |
| // If not landed on snow, check collision with the surface itself | |
| if (_checkCollision(flakePos, flake.size, surface)) { | |
| // Land on this surface | |
| flake.isLanded = true; | |
| flake.landedPosition = flakePos; | |
| surface.accumulatedSnow.add(flake); | |
| //print('flake landed on ${surface.id} ' | |
| // '${surface.accumulatedSnow.length} of ${surface.maxSnowDepth}'); | |
| landed = true; | |
| // Check if surface just became full | |
| if (surface.isFull) { | |
| // Release all accumulated snowflakes | |
| for (final landedFlake in surface.accumulatedSnow) { | |
| // Convert landed position back to normalized coordinates | |
| if (landedFlake.landedPosition != null) { | |
| landedFlake.x = landedFlake.landedPosition!.dx / size.width; | |
| landedFlake.y = landedFlake.landedPosition!.dy / size.height; | |
| } | |
| // Reset the flake to continue falling | |
| landedFlake.isLanded = false; | |
| landedFlake.landedPosition = null; | |
| landedFlake.time = 0.0; | |
| } | |
| // Clear the surface | |
| surface.accumulatedSnow.clear(); | |
| } | |
| break; | |
| } | |
| } | |
| if (!landed) { | |
| // Reset snowflake when it goes off screen | |
| if (flake.y > 1.0) { | |
| flake.y = -0.05; | |
| flake.x = _random.nextDouble(); | |
| flake.time = 0.0; | |
| } | |
| // Wrap around horizontally | |
| if (flake.x < 0.0) { | |
| flake.x = 1.0; | |
| } else if (flake.x > 1.0) { | |
| flake.x = 0.0; | |
| } | |
| } | |
| // Draw falling snowflake | |
| paint.color = Colors.white.withValues(alpha: (flake.size / _maxSize)); | |
| canvas.drawCircle( | |
| flakePos, | |
| flake.size, | |
| paint, | |
| ); | |
| } | |
| canvas.restore(); | |
| } | |
| bool _checkSnowflakeCollision( | |
| Offset fallingFlakePos, | |
| double fallingFlakeSize, | |
| Offset landedFlakePos, | |
| double landedFlakeSize, | |
| ) { | |
| // Check if falling snowflake is touching the top of a landed snowflake | |
| final fallingBottom = fallingFlakePos.dy + fallingFlakeSize; | |
| final landedTop = landedFlakePos.dy - landedFlakeSize; | |
| // Calculate horizontal distance between centers | |
| final horizontalDistance = (fallingFlakePos.dx - landedFlakePos.dx).abs(); | |
| // Check if horizontally close enough (within combined radius) | |
| final combinedRadius = fallingFlakeSize + landedFlakeSize; | |
| if (horizontalDistance <= combinedRadius) { | |
| // Check if vertically touching or just past | |
| if (fallingBottom >= landedTop && fallingBottom <= landedTop + 5) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool _checkCollision(Offset flakePos, double flakeSize, SnowSurface surface) { | |
| final renderBox = surface.context.findRenderObject() as RenderBox?; | |
| if (renderBox == null || !renderBox.hasSize) { | |
| return false; | |
| } | |
| // Get position relative to the RenderFallingSnow | |
| final position = renderBox.localToGlobal(Offset.zero, ancestor: this); | |
| final rect = position & renderBox.size; | |
| // Check if snowflake is touching the top of the surface | |
| final flakeBottom = flakePos.dy + flakeSize; | |
| final surfaceTop = rect.top; | |
| // Check if horizontally aligned with surface | |
| if (flakePos.dx >= rect.left && flakePos.dx <= rect.right) { | |
| // Check if touching or just past the surface | |
| if (flakeBottom >= surfaceTop && flakeBottom <= surfaceTop + 5) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| @override | |
| void dispose() { | |
| _controller.dispose(); | |
| super.dispose(); | |
| } | |
| } | |
| // Login Screen Content | |
| class LoginScreenContent extends StatefulWidget { | |
| const LoginScreenContent({super.key}); | |
| @override | |
| State<LoginScreenContent> createState() => _LoginScreenContentState(); | |
| } | |
| class _LoginScreenContentState extends State<LoginScreenContent> { | |
| final _emailController = TextEditingController(); | |
| final _passwordController = TextEditingController(); | |
| bool _obscurePassword = true; | |
| bool _rememberMe = false; | |
| @override | |
| void dispose() { | |
| _emailController.dispose(); | |
| _passwordController.dispose(); | |
| super.dispose(); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| return Center( | |
| child: SingleChildScrollView( | |
| padding: const EdgeInsets.all(24.0), | |
| child: Container( | |
| constraints: const BoxConstraints(maxWidth: 400), | |
| decoration: BoxDecoration( | |
| color: const Color(0xFF2d2d44).withOpacity(0.95), | |
| borderRadius: BorderRadius.circular(24), | |
| border: Border.all( | |
| color: Colors.white.withOpacity(0.1), | |
| width: 1, | |
| ), | |
| boxShadow: [ | |
| BoxShadow( | |
| color: Colors.black.withOpacity(0.3), | |
| blurRadius: 30, | |
| offset: const Offset(0, 10), | |
| ), | |
| ], | |
| ), | |
| child: Padding( | |
| padding: const EdgeInsets.all(32.0), | |
| child: Column( | |
| mainAxisSize: MainAxisSize.min, | |
| crossAxisAlignment: CrossAxisAlignment.stretch, | |
| children: [ | |
| // Title | |
| Center( | |
| child: const Text( | |
| 'Welcome Back', | |
| style: TextStyle( | |
| fontSize: 28, | |
| fontWeight: FontWeight.bold, | |
| color: Colors.white, | |
| ), | |
| ), | |
| ), | |
| const SizedBox(height: 8), | |
| Center( | |
| child: Text( | |
| 'Sign in to continue', | |
| style: TextStyle( | |
| fontSize: 16, | |
| color: Colors.grey[400], | |
| ), | |
| ), | |
| ), | |
| const SizedBox(height: 32), | |
| // Email Field | |
| SnowCatcher( | |
| id: 'email_field', | |
| maxSnowDepth: 15.0, | |
| child: TextField( | |
| controller: _emailController, | |
| style: const TextStyle(color: Colors.white), | |
| decoration: InputDecoration( | |
| labelText: 'Email', | |
| labelStyle: TextStyle(color: Colors.grey[400]), | |
| hintText: 'Enter your email', | |
| hintStyle: TextStyle(color: Colors.grey[600]), | |
| prefixIcon: Icon(Icons.email_outlined, color: Colors.grey[400]), | |
| filled: true, | |
| fillColor: Colors.white.withOpacity(0.05), | |
| border: OutlineInputBorder( | |
| borderRadius: BorderRadius.circular(12), | |
| borderSide: BorderSide.none, | |
| ), | |
| enabledBorder: OutlineInputBorder( | |
| borderRadius: BorderRadius.circular(12), | |
| borderSide: BorderSide( | |
| color: Colors.white.withOpacity(0.1), | |
| ), | |
| ), | |
| focusedBorder: OutlineInputBorder( | |
| borderRadius: BorderRadius.circular(12), | |
| borderSide: const BorderSide( | |
| color: Color(0xFF667eea), | |
| width: 2, | |
| ), | |
| ), | |
| ), | |
| keyboardType: TextInputType.emailAddress, | |
| ), | |
| ), | |
| const SizedBox(height: 16), | |
| // Password Field | |
| SnowCatcher( | |
| id: 'password_field', | |
| maxSnowDepth: 15.0, | |
| child: TextField( | |
| controller: _passwordController, | |
| obscureText: _obscurePassword, | |
| style: const TextStyle(color: Colors.white), | |
| decoration: InputDecoration( | |
| labelText: 'Password', | |
| labelStyle: TextStyle(color: Colors.grey[400]), | |
| hintText: 'Enter your password', | |
| hintStyle: TextStyle(color: Colors.grey[600]), | |
| prefixIcon: Icon(Icons.lock_outline, color: Colors.grey[400]), | |
| suffixIcon: IconButton( | |
| icon: Icon( | |
| _obscurePassword | |
| ? Icons.visibility_outlined | |
| : Icons.visibility_off_outlined, | |
| color: Colors.grey[400], | |
| ), | |
| onPressed: () { | |
| setState(() { | |
| _obscurePassword = !_obscurePassword; | |
| }); | |
| }, | |
| ), | |
| filled: true, | |
| fillColor: Colors.white.withOpacity(0.05), | |
| border: OutlineInputBorder( | |
| borderRadius: BorderRadius.circular(12), | |
| borderSide: BorderSide.none, | |
| ), | |
| enabledBorder: OutlineInputBorder( | |
| borderRadius: BorderRadius.circular(12), | |
| borderSide: BorderSide( | |
| color: Colors.white.withOpacity(0.1), | |
| ), | |
| ), | |
| focusedBorder: OutlineInputBorder( | |
| borderRadius: BorderRadius.circular(12), | |
| borderSide: const BorderSide( | |
| color: Color(0xFF667eea), | |
| width: 2, | |
| ), | |
| ), | |
| ), | |
| ), | |
| ), | |
| const SizedBox(height: 16), | |
| // Remember Me & Forgot Password | |
| Row( | |
| mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
| children: [ | |
| Row( | |
| children: [ | |
| Checkbox( | |
| value: _rememberMe, | |
| onChanged: (value) { | |
| setState(() { | |
| _rememberMe = value ?? false; | |
| }); | |
| }, | |
| activeColor: const Color(0xFF667eea), | |
| checkColor: Colors.white, | |
| side: BorderSide(color: Colors.grey[600]!), | |
| ), | |
| const Text( | |
| 'Remember me', | |
| style: TextStyle(color: Colors.white), | |
| ), | |
| ], | |
| ), | |
| TextButton( | |
| onPressed: () {}, | |
| child: const Text( | |
| 'Forgot Password?', | |
| style: TextStyle( | |
| color: Color(0xFF667eea), | |
| ), | |
| ), | |
| ), | |
| ], | |
| ), | |
| const SizedBox(height: 24), | |
| // Login Button | |
| SnowCatcher( | |
| id: 'sign_in_button', | |
| maxSnowDepth: 25.0, | |
| child: ElevatedButton( | |
| onPressed: () { | |
| // Handle login | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| const SnackBar( | |
| content: Text('Login button pressed!'), | |
| duration: Duration(seconds: 2), | |
| ), | |
| ); | |
| }, | |
| style: ElevatedButton.styleFrom( | |
| backgroundColor: const Color(0xFF667eea), | |
| foregroundColor: Colors.white, | |
| padding: const EdgeInsets.symmetric(vertical: 16), | |
| shape: RoundedRectangleBorder( | |
| borderRadius: BorderRadius.circular(12), | |
| ), | |
| elevation: 2, | |
| ), | |
| child: const Text( | |
| 'Sign In', | |
| style: TextStyle( | |
| fontSize: 16, | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| ), | |
| ), | |
| const SizedBox(height: 16), | |
| // Divider | |
| Row( | |
| children: [ | |
| Expanded( | |
| child: Divider(color: Colors.white.withOpacity(0.2)), | |
| ), | |
| Padding( | |
| padding: const EdgeInsets.symmetric(horizontal: 16), | |
| child: Text( | |
| 'OR', | |
| style: TextStyle(color: Colors.grey[500]), | |
| ), | |
| ), | |
| Expanded( | |
| child: Divider(color: Colors.white.withOpacity(0.2)), | |
| ), | |
| ], | |
| ), | |
| const SizedBox(height: 16), | |
| // Social Login Buttons | |
| Row( | |
| children: [ | |
| Expanded( | |
| child: OutlinedButton.icon( | |
| onPressed: () {}, | |
| icon: const Icon(Icons.g_mobiledata, size: 24), | |
| label: const Text('Google'), | |
| style: OutlinedButton.styleFrom( | |
| foregroundColor: Colors.white, | |
| padding: const EdgeInsets.symmetric(vertical: 12), | |
| shape: RoundedRectangleBorder( | |
| borderRadius: BorderRadius.circular(12), | |
| ), | |
| side: BorderSide( | |
| color: Colors.white.withOpacity(0.2), | |
| ), | |
| backgroundColor: Colors.white.withOpacity(0.05), | |
| ), | |
| ), | |
| ), | |
| const SizedBox(width: 12), | |
| Expanded( | |
| child: OutlinedButton.icon( | |
| onPressed: () {}, | |
| icon: const Icon(Icons.apple, size: 24), | |
| label: const Text('Apple'), | |
| style: OutlinedButton.styleFrom( | |
| foregroundColor: Colors.white, | |
| padding: const EdgeInsets.symmetric(vertical: 12), | |
| shape: RoundedRectangleBorder( | |
| borderRadius: BorderRadius.circular(12), | |
| ), | |
| side: BorderSide( | |
| color: Colors.white.withOpacity(0.2), | |
| ), | |
| backgroundColor: Colors.white.withOpacity(0.05), | |
| ), | |
| ), | |
| ), | |
| ], | |
| ), | |
| const SizedBox(height: 24), | |
| // Sign Up Link | |
| Row( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| Text( | |
| "Don't have an account? ", | |
| style: TextStyle(color: Colors.grey[400]), | |
| ), | |
| TextButton( | |
| onPressed: () {}, | |
| style: TextButton.styleFrom( | |
| padding: EdgeInsets.zero, | |
| minimumSize: const Size(0, 0), | |
| tapTargetSize: MaterialTapTargetSize.shrinkWrap, | |
| ), | |
| child: const Text( | |
| 'Sign Up', | |
| style: TextStyle( | |
| color: Color(0xFF667eea), | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| ), | |
| ], | |
| ), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment