this repo has no description
at main 215 lines 7.7 kB view raw
1import 'package:flutter/material.dart'; 2import 'package:grain/auth.dart'; 3import 'package:grain/widgets/app_button.dart'; 4import 'package:grain/widgets/app_image.dart'; 5import 'package:grain/widgets/plain_text_field.dart'; 6import 'package:url_launcher/url_launcher.dart'; 7 8class LoginPage extends StatefulWidget { 9 final void Function()? onSignIn; 10 const LoginPage({super.key, this.onSignIn}); 11 12 @override 13 State<LoginPage> createState() => _LoginPageState(); 14} 15 16class _LoginPageState extends State<LoginPage> { 17 final TextEditingController _handleController = TextEditingController(text: ''); 18 bool _signingIn = false; 19 20 Future<void> _signInWithBluesky(BuildContext context) async { 21 final handle = _handleController.text.trim(); 22 23 if (handle.isEmpty) return; 24 setState(() { 25 _signingIn = true; 26 }); 27 28 try { 29 await auth.login(handle); 30 31 if (widget.onSignIn != null) { 32 widget.onSignIn!(); 33 } 34 } finally { 35 setState(() { 36 _signingIn = false; 37 }); 38 } 39 } 40 41 @override 42 Widget build(BuildContext context) { 43 final theme = Theme.of(context); 44 return Scaffold( 45 backgroundColor: theme.scaffoldBackgroundColor, 46 body: Stack( 47 fit: StackFit.expand, 48 children: [ 49 AppImage( 50 url: 51 'https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:bcgltzqazw5tb6k2g3ttenbj/bafkreiewhwu3ro5dv7omedphb62db4koa7qtvyzfhiiypg3ru4tvuxkrjy@jpeg', 52 fit: BoxFit.cover, 53 ), 54 Container(color: Colors.black.withOpacity(0.4)), 55 Center( 56 child: Column( 57 mainAxisAlignment: MainAxisAlignment.center, 58 children: [ 59 const SizedBox(height: 24), 60 Padding( 61 padding: const EdgeInsets.symmetric(horizontal: 16), 62 child: PlainTextField( 63 label: '', 64 controller: _handleController, 65 hintText: 'Enter your handle or pds host', 66 enabled: !_signingIn, 67 onChanged: (_) {}, 68 ), 69 ), 70 const SizedBox(height: 12), 71 Padding( 72 padding: const EdgeInsets.symmetric(horizontal: 16), 73 child: SizedBox( 74 width: double.infinity, 75 child: AppButton( 76 label: 'Login', 77 onPressed: _signingIn ? null : () => _signInWithBluesky(context), 78 loading: _signingIn, 79 variant: AppButtonVariant.primary, 80 height: 44, 81 fontSize: 15, 82 borderRadius: 6, 83 ), 84 ), 85 ), 86 const SizedBox(height: 8), 87 Padding( 88 padding: const EdgeInsets.symmetric(horizontal: 16), 89 child: Container( 90 width: double.infinity, 91 decoration: BoxDecoration( 92 color: Colors.black.withOpacity(0.7), 93 borderRadius: BorderRadius.circular(6), 94 ), 95 padding: const EdgeInsets.all(12), 96 child: Text( 97 'e.g., user.bsky.social, user.grain.social, example.com, https://pds.example.com', 98 style: const TextStyle( 99 color: Colors.white, 100 fontSize: 13, 101 fontFamily: 'monospace', 102 ), 103 ), 104 ), 105 ), 106 ], 107 ), 108 ), 109 Positioned( 110 left: 0, 111 right: 0, 112 bottom: 0, 113 child: Padding( 114 padding: const EdgeInsets.fromLTRB(16, 8, 16, 32), 115 child: Column( 116 crossAxisAlignment: CrossAxisAlignment.stretch, 117 children: [ 118 Row( 119 crossAxisAlignment: CrossAxisAlignment.end, 120 mainAxisAlignment: MainAxisAlignment.spaceBetween, 121 children: [ 122 Flexible( 123 child: Column( 124 mainAxisSize: MainAxisSize.min, 125 mainAxisAlignment: MainAxisAlignment.end, 126 crossAxisAlignment: CrossAxisAlignment.start, 127 children: [ 128 Wrap( 129 crossAxisAlignment: WrapCrossAlignment.center, 130 spacing: 8, 131 runSpacing: 4, 132 children: [ 133 const Text( 134 '© 2025 Grain Social. All rights reserved.', 135 style: TextStyle(color: Colors.white, fontSize: 12), 136 ), 137 _LinkText('Terms', 'https://grain.social/support/terms'), 138 const Text( 139 '|', 140 style: TextStyle(color: Colors.white, fontSize: 12), 141 ), 142 _LinkText('Privacy', 'https://grain.social/support/privacy'), 143 const Text( 144 '|', 145 style: TextStyle(color: Colors.white, fontSize: 12), 146 ), 147 _LinkText('Copyright', 'https://grain.social/support/copyright'), 148 ], 149 ), 150 ], 151 ), 152 ), 153 Flexible( 154 child: Column( 155 mainAxisSize: MainAxisSize.min, 156 mainAxisAlignment: MainAxisAlignment.end, 157 crossAxisAlignment: CrossAxisAlignment.end, 158 children: [ 159 Wrap( 160 crossAxisAlignment: WrapCrossAlignment.center, 161 children: [ 162 const Text( 163 'Photo by ', 164 style: TextStyle(color: Colors.white, fontSize: 12), 165 ), 166 _LinkText( 167 '@chadtmiller.com', 168 'https://grain.social/profile/chadtmiller.com', 169 ), 170 ], 171 ), 172 ], 173 ), 174 ), 175 ], 176 ), 177 ], 178 ), 179 ), 180 ), 181 ], 182 ), 183 ); 184 } 185} 186 187class _LinkText extends StatelessWidget { 188 final String text; 189 final String url; 190 const _LinkText(this.text, this.url); 191 192 @override 193 Widget build(BuildContext context) { 194 return GestureDetector( 195 onTap: () async { 196 final uri = Uri.parse(url); 197 if (await canLaunchUrl(uri)) { 198 await launchUrl(uri); 199 } else { 200 ScaffoldMessenger.of( 201 context, 202 ).showSnackBar(SnackBar(content: Text('Could not open link: $url'))); 203 } 204 }, 205 child: Text( 206 text, 207 style: const TextStyle( 208 color: Colors.white, 209 fontSize: 12, 210 decoration: TextDecoration.underline, 211 ), 212 ), 213 ); 214 } 215}