+2
-2
lib/main.dart
+2
-2
lib/main.dart
···
7
7
import 'package:grain/app_theme.dart';
8
8
import 'package:grain/auth.dart';
9
9
import 'package:grain/screens/home_page.dart';
10
-
import 'package:grain/screens/splash_page.dart';
10
+
import 'package:grain/screens/login_page.dart';
11
11
12
12
import 'providers/profile_provider.dart';
13
13
···
105
105
} else {
106
106
home = isSignedIn
107
107
? MyHomePage(title: 'Grain', onSignOut: () => handleSignOut(context))
108
-
: SplashPage(onSignIn: handleSignIn);
108
+
: LoginPage(onSignIn: handleSignIn);
109
109
}
110
110
return MaterialApp(
111
111
title: 'Grain',
+215
lib/screens/login_page.dart
+215
lib/screens/login_page.dart
···
1
+
import 'package:flutter/material.dart';
2
+
import 'package:grain/auth.dart';
3
+
import 'package:grain/widgets/app_button.dart';
4
+
import 'package:grain/widgets/app_image.dart';
5
+
import 'package:grain/widgets/plain_text_field.dart';
6
+
import 'package:url_launcher/url_launcher.dart';
7
+
8
+
class 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
+
16
+
class _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
+
187
+
class _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
+
}
-92
lib/screens/splash_page.dart
-92
lib/screens/splash_page.dart
···
1
-
import 'package:flutter/material.dart';
2
-
import 'package:grain/auth.dart';
3
-
import 'package:grain/widgets/app_button.dart';
4
-
import 'package:grain/widgets/app_image.dart';
5
-
import 'package:grain/widgets/plain_text_field.dart';
6
-
7
-
class SplashPage extends StatefulWidget {
8
-
final void Function()? onSignIn;
9
-
const SplashPage({super.key, this.onSignIn});
10
-
11
-
@override
12
-
State<SplashPage> createState() => _SplashPageState();
13
-
}
14
-
15
-
class _SplashPageState extends State<SplashPage> {
16
-
final TextEditingController _handleController = TextEditingController(text: '');
17
-
bool _signingIn = false;
18
-
19
-
Future<void> _signInWithBluesky(BuildContext context) async {
20
-
final handle = _handleController.text.trim();
21
-
22
-
if (handle.isEmpty) return;
23
-
setState(() {
24
-
_signingIn = true;
25
-
});
26
-
27
-
try {
28
-
await auth.login(handle);
29
-
30
-
if (widget.onSignIn != null) {
31
-
widget.onSignIn!();
32
-
}
33
-
} finally {
34
-
setState(() {
35
-
_signingIn = false;
36
-
});
37
-
}
38
-
}
39
-
40
-
@override
41
-
Widget build(BuildContext context) {
42
-
final theme = Theme.of(context);
43
-
return Scaffold(
44
-
backgroundColor: theme.scaffoldBackgroundColor,
45
-
body: Stack(
46
-
fit: StackFit.expand,
47
-
children: [
48
-
AppImage(
49
-
url:
50
-
'https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:bcgltzqazw5tb6k2g3ttenbj/bafkreiewhwu3ro5dv7omedphb62db4koa7qtvyzfhiiypg3ru4tvuxkrjy@jpeg',
51
-
fit: BoxFit.cover,
52
-
),
53
-
Container(color: Colors.black.withOpacity(0.4)),
54
-
Center(
55
-
child: Column(
56
-
mainAxisAlignment: MainAxisAlignment.center,
57
-
children: [
58
-
const SizedBox(height: 24),
59
-
Padding(
60
-
padding: const EdgeInsets.symmetric(horizontal: 16),
61
-
child: PlainTextField(
62
-
label: '',
63
-
controller: _handleController,
64
-
hintText: 'Enter your handle',
65
-
enabled: !_signingIn,
66
-
onChanged: (_) {},
67
-
),
68
-
),
69
-
const SizedBox(height: 12),
70
-
Padding(
71
-
padding: const EdgeInsets.symmetric(horizontal: 16),
72
-
child: SizedBox(
73
-
width: double.infinity,
74
-
child: AppButton(
75
-
label: 'Login',
76
-
onPressed: _signingIn ? null : () => _signInWithBluesky(context),
77
-
loading: _signingIn,
78
-
variant: AppButtonVariant.primary,
79
-
height: 44,
80
-
fontSize: 15,
81
-
borderRadius: 6,
82
-
),
83
-
),
84
-
),
85
-
],
86
-
),
87
-
),
88
-
],
89
-
),
90
-
);
91
-
}
92
-
}