Initial commit

This commit is contained in:
2025-07-23 01:48:04 +01:00
parent f077873f02
commit 9fbdd97b15
133 changed files with 5363 additions and 0 deletions

View 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
View 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
View 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;
}

View 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');
}
}
}

View 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'],
);
}
}