forked from
grain.social/native
this repo has no description
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}