Initial commit
This commit is contained in:
215
lib/components/login/login.dart
Normal file
215
lib/components/login/login.dart
Normal file
@@ -0,0 +1,215 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mileograph_flutter/services/authservice.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class LoginPage extends StatelessWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [LoginPanel()],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LoginPanel extends StatefulWidget {
|
||||
const LoginPanel({super.key});
|
||||
|
||||
@override
|
||||
State<LoginPanel> createState() => _LoginPanelState();
|
||||
}
|
||||
|
||||
class _LoginPanelState extends State<LoginPanel> {
|
||||
bool registerMode = false;
|
||||
|
||||
void toggleMode() {
|
||||
setState(() {
|
||||
registerMode = !registerMode;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final authService = context.read<AuthService>();
|
||||
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: 400,
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: registerMode
|
||||
? RegisterPanelContent(
|
||||
onBack: toggleMode,
|
||||
authService: authService,
|
||||
)
|
||||
: LoginPanelContent(
|
||||
registerCb: toggleMode,
|
||||
authService: authService,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LoginPanelContent extends StatefulWidget {
|
||||
const LoginPanelContent({
|
||||
required this.registerCb,
|
||||
required this.authService,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final VoidCallback registerCb;
|
||||
final AuthService authService;
|
||||
|
||||
@override
|
||||
State<LoginPanelContent> createState() => _LoginPanelContentState();
|
||||
}
|
||||
|
||||
class _LoginPanelContentState extends State<LoginPanelContent> {
|
||||
final _usernameController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
|
||||
void login() async {
|
||||
final username = _usernameController.text;
|
||||
final password = _passwordController.text;
|
||||
|
||||
final auth = context.read<AuthService>();
|
||||
|
||||
try {
|
||||
auth.login(username, password);
|
||||
print('Login successful');
|
||||
} catch (e) {
|
||||
// Handle error
|
||||
print('Login failed: $e');
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Login failed')));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 50),
|
||||
child: Text(
|
||||
"Login",
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
controller: _usernameController,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: "Username",
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
controller: _passwordController,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: "Password",
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 10,
|
||||
children: [
|
||||
FilledButton(onPressed: login, child: Text("Login")),
|
||||
ElevatedButton(
|
||||
onPressed: widget.registerCb,
|
||||
child: Text("Register"),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RegisterPanelContent extends StatelessWidget {
|
||||
const RegisterPanelContent({required this.onBack, required this.authService});
|
||||
final VoidCallback onBack;
|
||||
final AuthService authService;
|
||||
void register() {}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.arrow_back),
|
||||
onPressed: onBack,
|
||||
tooltip: 'Back to login',
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
"Register",
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Spacer to balance the row visually
|
||||
SizedBox(width: 48), // matches IconButton size
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: "Username",
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: "Display Name",
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: "Email",
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: "Password",
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: "Invite Code",
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 10,
|
||||
children: [
|
||||
FilledButton(onPressed: register, child: Text("Register")),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
150
lib/main.dart
Normal file
150
lib/main.dart
Normal file
@@ -0,0 +1,150 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mileograph_flutter/services/apiService.dart';
|
||||
import 'package:mileograph_flutter/services/authservice.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'components/login/login.dart';
|
||||
|
||||
void main() {
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
Provider<ApiService>(
|
||||
create: (_) =>
|
||||
ApiService(baseUrl: 'https://dev.mileograph.co.uk/api/v1'),
|
||||
),
|
||||
ChangeNotifierProvider<AuthService>(
|
||||
create: (context) => AuthService(api: context.read<ApiService>()),
|
||||
),
|
||||
],
|
||||
child: MyApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final auth = context.read<AuthService>();
|
||||
|
||||
Widget fullPage;
|
||||
Widget subPage;
|
||||
if (!auth.isLoggedIn) {
|
||||
fullPage = LoginPage();
|
||||
} else {
|
||||
fullPage = MyHomePage(title: "Mileograph");
|
||||
}
|
||||
|
||||
return MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
theme: ThemeData(
|
||||
// This is the theme of your application.
|
||||
//
|
||||
// TRY THIS: Try running your application with "flutter run". You'll see
|
||||
// the application has a purple toolbar. Then, without quitting the app,
|
||||
// try changing the seedColor in the colorScheme below to Colors.green
|
||||
// and then invoke "hot reload" (save your changes or press the "hot
|
||||
// reload" button in a Flutter-supported IDE, or press "r" if you used
|
||||
// the command line to start the app).
|
||||
//
|
||||
// Notice that the counter didn't reset back to zero; the application
|
||||
// state is not lost during the reload. To reset the state, use hot
|
||||
// restart instead.
|
||||
//
|
||||
// This works for code too, not just values: Most code changes can be
|
||||
// tested with just a hot reload.
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blueGrey),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.blueGrey,
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
),
|
||||
themeMode: ThemeMode.system,
|
||||
home: const MyHomePage(title: 'Mile-O-Graph'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
const MyHomePage({super.key, required this.title});
|
||||
|
||||
// This widget is the home page of your application. It is stateful, meaning
|
||||
// that it has a State object (defined below) that contains fields that affect
|
||||
// how it looks.
|
||||
|
||||
// This class is the configuration for the state. It holds the values (in this
|
||||
// case the title) provided by the parent (in this case the App widget) and
|
||||
// used by the build method of the State. Fields in a Widget subclass are
|
||||
// always marked "final".
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
State<MyHomePage> createState() => _MyHomePageState();
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
int pageIndex = 0;
|
||||
bool loggedIn = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// This method is rerun every time setState is called, for instance as done
|
||||
// by the _incrementCounter method above.
|
||||
//
|
||||
// The Flutter framework has been optimized to make rerunning build methods
|
||||
// fast, so that you can just rebuild anything that needs updating rather
|
||||
// than having to individually change instances of widgets.
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// TRY THIS: Try changing the color here to a specific color (to
|
||||
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
|
||||
// change color while the other colors stay the same.
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
// Here we take the value from the MyHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: Text(widget.title),
|
||||
leading: IconButton(onPressed: null, icon: Icon(Icons.menu)),
|
||||
actions: [
|
||||
IconButton(onPressed: null, icon: Icon(Icons.account_circle)),
|
||||
IconButton(onPressed: null, icon: Icon(Icons.settings)),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: pageIndex,
|
||||
onDestinationSelected: (int index) {
|
||||
setState(() {
|
||||
pageIndex = index;
|
||||
});
|
||||
},
|
||||
destinations: [
|
||||
NavigationDestination(icon: Icon(Icons.home), label: "Home"),
|
||||
NavigationDestination(icon: Icon(Icons.list), label: "Entries"),
|
||||
NavigationDestination(icon: Icon(Icons.train), label: "Traction"),
|
||||
NavigationDestination(icon: Icon(Icons.route), label: "Trips"),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.account_circle),
|
||||
label: "User",
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Dashboard(),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: null,
|
||||
tooltip: 'New Entry',
|
||||
child: const Icon(Icons.add),
|
||||
), // This trailing comma makes auto-formatting nicer for build methods.
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Dashboard extends StatelessWidget {
|
||||
Dashboard({super.key});
|
||||
Widget build(BuildContext context) {
|
||||
return Text("Logged in");
|
||||
}
|
||||
}
|
||||
36
lib/objects/objects.dart
Normal file
36
lib/objects/objects.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DestinationObject {
|
||||
const DestinationObject(
|
||||
this.label,
|
||||
this.icon,
|
||||
this.selectedIcon,
|
||||
this.pageWidget,
|
||||
);
|
||||
|
||||
final String label;
|
||||
final Widget icon;
|
||||
final Widget selectedIcon;
|
||||
final Widget pageWidget;
|
||||
}
|
||||
|
||||
class UserData {
|
||||
const UserData(this.username, this.full_name, this.user_id, this.email);
|
||||
|
||||
final String user_id;
|
||||
final String username;
|
||||
final String full_name;
|
||||
final String email;
|
||||
}
|
||||
|
||||
class AuthenticatedUserData extends UserData {
|
||||
const AuthenticatedUserData({
|
||||
required String user_id,
|
||||
required String username,
|
||||
required String full_name,
|
||||
required String email,
|
||||
required this.access_token,
|
||||
}) : super(username, full_name, user_id, email);
|
||||
|
||||
final String access_token;
|
||||
}
|
||||
78
lib/services/apiService.dart
Normal file
78
lib/services/apiService.dart
Normal file
@@ -0,0 +1,78 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class ApiService {
|
||||
final String baseUrl;
|
||||
|
||||
ApiService({required this.baseUrl});
|
||||
|
||||
Future<dynamic> get(String endpoint, {Map<String, String>? headers}) async {
|
||||
final response = await http.get(
|
||||
Uri.parse('$baseUrl$endpoint'),
|
||||
headers: headers,
|
||||
);
|
||||
return _processResponse(response);
|
||||
}
|
||||
|
||||
Future<dynamic> post(
|
||||
String endpoint,
|
||||
dynamic data, {
|
||||
Map<String, String>? headers,
|
||||
}) async {
|
||||
final response = await http.post(
|
||||
Uri.parse('$baseUrl$endpoint'),
|
||||
headers: _jsonHeaders(headers),
|
||||
body: jsonEncode(data),
|
||||
);
|
||||
return _processResponse(response);
|
||||
}
|
||||
|
||||
Future<dynamic> postForm(String endpoint, Map<String, String> data) async {
|
||||
final response = await http.post(
|
||||
Uri.parse('$baseUrl$endpoint'),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'accept': 'application/json',
|
||||
},
|
||||
body: data, // http package handles form-encoding for Map<String, String>
|
||||
);
|
||||
return _processResponse(response);
|
||||
}
|
||||
|
||||
Future<dynamic> put(
|
||||
String endpoint,
|
||||
dynamic data, {
|
||||
Map<String, String>? headers,
|
||||
}) async {
|
||||
final response = await http.put(
|
||||
Uri.parse('$baseUrl$endpoint'),
|
||||
headers: _jsonHeaders(headers),
|
||||
body: jsonEncode(data),
|
||||
);
|
||||
return _processResponse(response);
|
||||
}
|
||||
|
||||
Future<dynamic> delete(
|
||||
String endpoint, {
|
||||
Map<String, String>? headers,
|
||||
}) async {
|
||||
final response = await http.delete(
|
||||
Uri.parse('$baseUrl$endpoint'),
|
||||
headers: headers,
|
||||
);
|
||||
return _processResponse(response);
|
||||
}
|
||||
|
||||
Map<String, String> _jsonHeaders(Map<String, String>? extra) {
|
||||
return {'Content-Type': 'application/json', if (extra != null) ...extra};
|
||||
}
|
||||
|
||||
dynamic _processResponse(http.Response res) {
|
||||
final body = res.body.isNotEmpty ? jsonDecode(res.body) : null;
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
return body;
|
||||
} else {
|
||||
throw Exception('API error ${res.statusCode}: $body');
|
||||
}
|
||||
}
|
||||
}
|
||||
68
lib/services/authservice.dart
Normal file
68
lib/services/authservice.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:mileograph_flutter/objects/objects.dart';
|
||||
import 'package:mileograph_flutter/services/apiService.dart';
|
||||
|
||||
class AuthService extends ChangeNotifier {
|
||||
final ApiService api;
|
||||
|
||||
AuthService({required this.api});
|
||||
|
||||
AuthenticatedUserData? _user;
|
||||
|
||||
bool get isLoggedIn => _user != null;
|
||||
String? get token => _user?.access_token;
|
||||
String? get userId => _user?.user_id;
|
||||
String? get username => _user?.username;
|
||||
String? get fullName => _user?.full_name;
|
||||
|
||||
void setLoginData({
|
||||
required String userId,
|
||||
required String username,
|
||||
required String fullName,
|
||||
required String accessToken,
|
||||
required String email,
|
||||
}) {
|
||||
_user = AuthenticatedUserData(
|
||||
user_id: userId,
|
||||
username: username,
|
||||
full_name: fullName,
|
||||
access_token: accessToken,
|
||||
email: email,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> login(String username, String password) async {
|
||||
final formData = {
|
||||
'grant_type': 'password',
|
||||
'username': username,
|
||||
'password': password,
|
||||
'scope': '',
|
||||
'client_id': 'string',
|
||||
'client_secret': 'string',
|
||||
};
|
||||
|
||||
// 1. Get token
|
||||
final tokenResponse = await api.postForm('/token', formData);
|
||||
final accessToken = tokenResponse['access_token'];
|
||||
|
||||
// 2. Get user details
|
||||
final userResponse = await api.get(
|
||||
'/users/me',
|
||||
headers: {
|
||||
'Authorization': 'Bearer $accessToken',
|
||||
'accept': 'application/json',
|
||||
},
|
||||
);
|
||||
|
||||
// 3. Populate state
|
||||
setLoginData(
|
||||
userId: userResponse['user_id'],
|
||||
username: userResponse['username'],
|
||||
fullName: userResponse['full_name'],
|
||||
accessToken: accessToken,
|
||||
email: userResponse['email'],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user