chore: Halkyon Todo's scaffold joins the repository in apps/todo
Its server, client and app as `serverpod create` made them, with the switch to server-side sessions that was waiting uncommitted, as files: its one commit holds the scaffold alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
649499be23
commit
314a901fa1
5 changed files with 16151 additions and 0 deletions
32
apps/todo/halkyon_todo_flutter/lib/client.dart
Normal file
32
apps/todo/halkyon_todo_flutter/lib/client.dart
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:halkyon_todo_client/halkyon_todo_client.dart';
|
||||
import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart';
|
||||
import 'package:serverpod_flutter/serverpod_flutter.dart';
|
||||
|
||||
// When you are running the app on a physical device, you need to set the
|
||||
// server URL to the IP address of your computer. You can find the IP
|
||||
// address by running `ipconfig` on Windows or `ifconfig` on Mac/Linux.
|
||||
//
|
||||
// You can set the variable when running or building your app like this:
|
||||
// E.g. `flutter run --dart-define=SERVER_URL=https://api.example.com/`.
|
||||
//
|
||||
// Otherwise, the server URL is fetched from the assets/config.json file or
|
||||
// defaults to http://$localhost:8080/ if not found.
|
||||
final serverUrl = getServerUrl();
|
||||
|
||||
/// Sets up a global client object that can be used to talk to the server from
|
||||
/// anywhere in our app. The client is generated from your server code
|
||||
/// and is set up to connect to a Serverpod running on a local server on
|
||||
/// the default port. You will need to modify this to connect to staging or
|
||||
/// production servers.
|
||||
/// In a larger app, you may want to use the dependency injection of your choice
|
||||
/// instead of using a global client object. This is just a simple example.
|
||||
late final Client client;
|
||||
|
||||
Future<void> initializeClient() async {
|
||||
client = Client(await serverUrl)
|
||||
..connectivityMonitor = FlutterConnectivityMonitor()
|
||||
..authSessionManager = FlutterAuthSessionManager();
|
||||
unawaited(client.auth.initialize());
|
||||
}
|
||||
15
apps/todo/halkyon_todo_flutter/lib/driver.dart
Normal file
15
apps/todo/halkyon_todo_flutter/lib/driver.dart
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// This file is never meant to be used on real apps, just for development.
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:flutter_driver/driver_extension.dart';
|
||||
|
||||
import 'main.dart' as app;
|
||||
|
||||
void main() {
|
||||
enableFlutterDriverExtension(
|
||||
// Text entry emulation is disabled by default to allow manual usage of the
|
||||
// app, but will be required for agents to type through the app. Set this
|
||||
// parameter to true to switch from manual to automated usage.
|
||||
enableTextEntryEmulation: false,
|
||||
);
|
||||
app.main();
|
||||
}
|
||||
61
apps/todo/halkyon_todo_flutter/lib/main.dart
Normal file
61
apps/todo/halkyon_todo_flutter/lib/main.dart
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'client.dart';
|
||||
import 'screens/greetings_screen.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await initializeClient();
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
/// Builds a theme for the given [brightness].
|
||||
ThemeData _buildTheme(Brightness brightness) {
|
||||
return ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.blue,
|
||||
brightness: brightness,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Serverpod Demo',
|
||||
theme: _buildTheme(Brightness.light),
|
||||
darkTheme: _buildTheme(Brightness.dark),
|
||||
themeMode: ThemeMode.system,
|
||||
home: const MyHomePage(title: 'Serverpod Example'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage extends StatelessWidget {
|
||||
const MyHomePage({super.key, required this.title});
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(title)),
|
||||
body: const GreetingsScreen(),
|
||||
// To test authentication in this example app, uncomment the line below
|
||||
// and comment out the line above. This wraps the GreetingsScreen with a
|
||||
// SignInScreen, which automatically shows a sign-in UI when the user is
|
||||
// not authenticated and displays the GreetingsScreen once they sign in.
|
||||
//
|
||||
// body: SignInScreen(
|
||||
// child: GreetingsScreen(
|
||||
// onSignOut: () async {
|
||||
// await client.auth.signOutDevice();
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
);
|
||||
}
|
||||
}
|
||||
122
apps/todo/halkyon_todo_flutter/lib/screens/greetings_screen.dart
Normal file
122
apps/todo/halkyon_todo_flutter/lib/screens/greetings_screen.dart
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../client.dart';
|
||||
|
||||
class GreetingsScreen extends StatefulWidget {
|
||||
final Future<void> Function()? onSignOut;
|
||||
const GreetingsScreen({super.key, this.onSignOut});
|
||||
|
||||
@override
|
||||
State<GreetingsScreen> createState() => _GreetingsScreenState();
|
||||
}
|
||||
|
||||
class _GreetingsScreenState extends State<GreetingsScreen> {
|
||||
/// Holds the last result or null if no result exists yet.
|
||||
String? _resultMessage;
|
||||
|
||||
/// Holds the last error message that we've received from the server or null
|
||||
/// if no error exists yet.
|
||||
String? _errorMessage;
|
||||
|
||||
final _textEditingController = TextEditingController();
|
||||
|
||||
/// Calls the `hello` method of the `greeting` endpoint. Will set either the
|
||||
/// `_resultMessage` or `_errorMessage` field, depending on if the call
|
||||
/// is successful.
|
||||
void _callHello() async {
|
||||
try {
|
||||
final result = await client.greeting.hello(_textEditingController.text);
|
||||
setState(() {
|
||||
_errorMessage = null;
|
||||
_resultMessage = result.message;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_errorMessage = '$e';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
if (widget.onSignOut != null) ...[
|
||||
const Text('You are connected'),
|
||||
ElevatedButton(
|
||||
onPressed: widget.onSignOut,
|
||||
child: const Text('Sign out'),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 32),
|
||||
TextField(
|
||||
controller: _textEditingController,
|
||||
onSubmitted: (_) => _callHello(),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Enter your name',
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
onPressed: _callHello,
|
||||
icon: const Icon(Icons.send),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ResultDisplay(
|
||||
resultMessage: _resultMessage,
|
||||
errorMessage: _errorMessage,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// ResultDisplays shows the result of the call. Either the returned result
|
||||
/// from the `example.greeting` endpoint method or an error message.
|
||||
class ResultDisplay extends StatelessWidget {
|
||||
final String? resultMessage;
|
||||
final String? errorMessage;
|
||||
|
||||
const ResultDisplay({super.key, this.resultMessage, this.errorMessage});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
String text;
|
||||
Color backgroundColor;
|
||||
Color foregroundColor;
|
||||
if (errorMessage != null) {
|
||||
backgroundColor = colors.errorContainer;
|
||||
foregroundColor = colors.onErrorContainer;
|
||||
text = errorMessage!;
|
||||
} else if (resultMessage != null) {
|
||||
backgroundColor = colors.primaryContainer;
|
||||
foregroundColor = colors.onPrimaryContainer;
|
||||
text = resultMessage!;
|
||||
} else {
|
||||
backgroundColor = colors.surfaceContainerHighest;
|
||||
foregroundColor = colors.onSurfaceVariant;
|
||||
text = 'No server response yet.';
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: const BoxConstraints(minHeight: 60),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
// Reuses the text field's border so the shape and width always match.
|
||||
decoration: ShapeDecoration(
|
||||
color: backgroundColor,
|
||||
shape: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: foregroundColor),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(text, style: TextStyle(color: foregroundColor)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart';
|
||||
|
||||
import '../client.dart';
|
||||
|
||||
class SignInScreen extends StatefulWidget {
|
||||
final Widget child;
|
||||
const SignInScreen({super.key, required this.child});
|
||||
|
||||
@override
|
||||
State<SignInScreen> createState() => _SignInScreenState();
|
||||
}
|
||||
|
||||
class _SignInScreenState extends State<SignInScreen> {
|
||||
bool _isSignedIn = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
client.auth.authInfoListenable.addListener(_updateSignedInState);
|
||||
_isSignedIn = client.auth.isAuthenticated;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
client.auth.authInfoListenable.removeListener(_updateSignedInState);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateSignedInState() {
|
||||
setState(() {
|
||||
_isSignedIn = client.auth.isAuthenticated;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
return _isSignedIn
|
||||
? widget.child
|
||||
: Center(
|
||||
child: SignInWidget(
|
||||
client: client,
|
||||
onAuthenticated: () {
|
||||
context.showSnackBar(
|
||||
message: 'User authenticated.',
|
||||
backgroundColor: colors.primaryContainer,
|
||||
foregroundColor: colors.onPrimaryContainer,
|
||||
);
|
||||
},
|
||||
onError: (error) {
|
||||
context.showSnackBar(
|
||||
message: 'Authentication failed: $error',
|
||||
backgroundColor: colors.errorContainer,
|
||||
foregroundColor: colors.onErrorContainer,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on BuildContext {
|
||||
void showSnackBar({
|
||||
required String message,
|
||||
required Color backgroundColor,
|
||||
required Color foregroundColor,
|
||||
}) {
|
||||
ScaffoldMessenger.of(this).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message, style: TextStyle(color: foregroundColor)),
|
||||
backgroundColor: backgroundColor,
|
||||
duration: const Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue