first commit

This commit is contained in:
Alzalia 2025-08-05 10:32:18 +02:00
commit faf67cc6d8
148 changed files with 6580 additions and 0 deletions

View file

@ -0,0 +1,8 @@
import "package:flutter/widgets.dart";
import "package:nested/nested.dart";
import "package:provider/provider.dart";
import "package:seshat/data/services/websocket_client.dart";
List<SingleChildWidget> get providers {
return [Provider(create: (context) => WebsocketClient())];
}

View file

@ -0,0 +1,47 @@
import 'dart:convert';
import 'package:seshat/data/services/api_client.dart';
import 'package:seshat/data/services/websocket_client.dart';
import 'package:seshat/domain/models/owner.dart';
import 'package:seshat/utils/result.dart';
class OwnerRepository {
OwnerRepository({
required ApiClient apiClient,
required WebsocketClient wsClient,
}) : _apiClient = apiClient,
_wsClient = wsClient;
final ApiClient _apiClient;
final WebsocketClient _wsClient;
List<Owner>? _cachedData;
Future<Result<List<Owner>>> getOwners() async {
if (_cachedData == null) {
final result = await _apiClient.getOwners();
if (result is Ok<List<Owner>>) {
_cachedData = result.value;
}
return result;
} else {
return Result.ok(_cachedData!);
}
}
Stream<Owner> liveOwners() async* {
await for (String data in _wsClient.connect()) {
Map<String, dynamic> decodedData = jsonDecode(
data,
).cast<Map<String, dynamic>>();
Owner owner = Owner.fromJSON(decodedData);
if (_cachedData == null) {
getOwners();
} else {
_cachedData!.add(owner);
yield* Stream.value(owner);
}
}
}
}

View file

@ -0,0 +1,8 @@
import 'package:seshat/domain/models/owner.dart';
import 'package:seshat/utils/result.dart';
class ApiClient {
Future<Result<List<Owner>>> getOwners() async {
return Result.ok(<Owner>[]);
}
}

View file

@ -0,0 +1,10 @@
import 'package:web_socket_channel/web_socket_channel.dart';
class WebsocketClient {
static const String _host = "ws://bal.ninjdai.fr:3000";
Stream<dynamic> connect() {
final channel = WebSocketChannel.connect(Uri.parse("$_host/ws"));
return channel.stream;
}
}

View file

@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
class Owner extends ChangeNotifier {
Owner({
required this.firstName,
required this.lastName,
required this.contact,
required this.id,
});
String firstName;
String lastName;
String contact;
int id;
factory Owner.fromJSON(Map<String, dynamic> json) => Owner(
contact: json["contact"],
firstName: json["first_name"],
lastName: json["last_name"],
id: json["id"],
);
}

81
lib/main.dart Normal file
View file

@ -0,0 +1,81 @@
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'package:seshat/config/dependencies.dart';
import 'package:seshat/routing/router.dart';
// TODO: In router, make it so that the navbar is integrated in everyscreen that needs it -> consistancy (should be at least, hope so) so it stops jumping. Then, make it so that the pages are children of scaffold. That's all ?
void main() {
Logger.root.level = Level.ALL;
// runApp(MultiProvider(providers: providers, child: const MyApp()));
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp.router(routerConfig: router());
}
}
// class Root extends StatefulWidget {
// const Root({super.key});
// @override
// State<Root> createState() => _RootState();
// }
// class _RootState extends State<Root> {
// var selectedIndex = 1;
// @override
// Widget build(BuildContext context) {
// final theme = Theme.of(context);
// return AnnotatedRegion(
// value: SystemUiOverlayStyle(
// statusBarBrightness: Brightness.light,
// statusBarIconBrightness: Brightness.light,
// ),
// child: Scaffold(
// appBar: null,
// bottomNavigationBar: NavigationBar(
// destinations: const <Widget>[
// NavigationDestination(icon: Icon(Icons.home), label: "Home"),
// NavigationDestination(
// icon: Icon(Icons.plus_one_outlined),
// label: "Ajout",
// ),
// NavigationDestination(icon: Icon(Icons.sell), label: "Vente"),
// ],
// selectedIndex: selectedIndex,
// onDestinationSelected: (int index) {
// setState(() {
// selectedIndex = index;
// });
// },
// indicatorColor: theme.highlightColor,
// ),
// body: switch (selectedIndex) {
// 0 => Add(title: "Home Page"),
// 1 => MultiProvider(
// providers: [
// ChangeNotifierProvider(
// create: (context) => TabScreen("scanPage"),
// ),
// ChangeNotifierProvider(
// create: (context) =>
// Owner("UEAuvergne", "unionetudianteauvergne@gmail.com"),
// ),
// ],
// child: Add(title: "Ajout"),
// ),
// _ => Add(title: "Wildcard"),
// },
// ),
// );
// }
// }

34
lib/routing/router.dart Normal file
View file

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:seshat/routing/routes.dart';
import 'package:seshat/ui/add_page/view_model/add_view_model.dart';
import 'package:seshat/ui/add_page/widgets/add_page.dart';
import 'package:seshat/ui/core/ui/navigation_bar.dart';
import 'package:seshat/ui/home_page/home_page.dart';
import 'package:seshat/ui/sell_page/sell_page.dart';
GoRouter router() => GoRouter(
initialLocation: Routes.add,
routes: [
GoRoute(
path: Routes.home,
pageBuilder: (context, state) => NoTransitionPage(child: HomePage()),
routes: [
GoRoute(
path: Routes.add,
pageBuilder: (context, state) =>
NoTransitionPage(child: AddPage(viewModel: AddViewModel())),
// routes: [
// GoRoute(path: Routes.addForm),
// GoRoute(path: Routes.addOwner),
// GoRoute(path: Routes.addPrice),
// ],
),
GoRoute(
path: Routes.sell,
pageBuilder: (context, state) => NoTransitionPage(child: SellPage()),
),
],
),
],
);

13
lib/routing/routes.dart Normal file
View file

@ -0,0 +1,13 @@
abstract final class Routes {
// ==[ HOME ]==
static const home = '/';
// ==[ ADD ]==
static const add = '/add';
static const addOwner = '/add/owner';
static const addPrice = '/add/price';
static const addForm = '/add/form';
// ==[ SELL ]==
static const sell = '/sell';
}

View file

@ -0,0 +1,75 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:seshat/domain/models/owner.dart';
class AddViewModel extends ChangeNotifier {
AddViewModel();
final _log = Logger("AddViewModel");
Owner? _currentOwner;
Owner? get currentOwner => _currentOwner;
set currentOwner(Owner? owner) {
_currentOwner = owner;
notifyListeners();
}
List<Owner>? _owners = [
Owner(
firstName: "Jean",
lastName: "Henri",
contact: "contact@gmail.com",
id: 1,
),
Owner(
firstName: "Jeanette",
lastName: "Henriette",
contact: "contact@gmail.com",
id: 2,
),
Owner(
firstName: "Jacques",
lastName: "Gerard",
contact: "contact@gmail.com",
id: 3,
),
Owner(
firstName: "Jacquelines",
lastName: "Geraldine",
contact: "contact@gmail.com",
id: 4,
),
Owner(
firstName: "Louis",
lastName: "Valentin",
contact: "contact@gmail.com",
id: 5,
),
Owner(
firstName: "Louise",
lastName: "Valentine",
contact: "contact@gmail.com",
id: 6,
),
];
List<Owner>? get owners => _owners;
Owner addOwner(String firstName, String lastName, String contact) {
_owners!.add(
Owner(
firstName: firstName,
lastName: lastName,
contact: contact,
id: _owners!.last.id + 1,
),
);
notifyListeners();
return Owner(
firstName: firstName,
lastName: lastName,
contact: contact,
id: 0,
);
}
}

View file

@ -0,0 +1,120 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:seshat/ui/add_page/view_model/add_view_model.dart';
import 'package:seshat/ui/add_page/widgets/owner_popup.dart';
import 'package:seshat/ui/core/ui/navigation_bar.dart';
class AddPage extends StatefulWidget {
const AddPage({super.key, required this.viewModel});
final AddViewModel viewModel;
@override
State<AddPage> createState() => _AddPageState();
}
class _AddPageState extends State<AddPage> {
bool askPrice = true;
@override
Widget build(BuildContext context) {
final MobileScannerController controller = MobileScannerController(
formats: [BarcodeFormat.ean13],
detectionTimeoutMs: 1000,
);
// return Consumer<TabScreen>(
// builder: (context, screen, child) {
return Scaffold(
bottomNavigationBar: AppNavigationBar(startIndex: 1),
body: Stack(
children: [
ColoredBox(color: Colors.black),
MobileScanner(
controller: controller,
onDetect: (barcodes) {
onBarcodeScan(barcodes, controller);
},
),
SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Center(
child: Card(
margin: EdgeInsets.symmetric(horizontal: 50),
child: Column(
children: [
ListTile(
leading: Icon(Icons.person),
title: TextButton(
child: Text("No"),
onPressed: () => _ownerDialogBuilder(
context,
controller,
widget.viewModel,
),
),
),
ListTile(
leading: Icon(Icons.attach_money),
title: TextButton(
child: Text(
(askPrice)
? "Demander à chaque fois"
: "Prix libre toujours",
),
onPressed: () {
setState(() {
askPrice = !askPrice;
});
},
),
),
],
),
),
),
Expanded(child: SizedBox()),
SvgPicture.asset('assets/scan-overlay.svg'),
Expanded(child: SizedBox()),
TextButton(
onPressed: () {},
child: Text("Enregistrer manuellement"),
),
],
),
),
],
),
);
// },
// );
}
}
void onBarcodeScan(
BarcodeCapture barcodes,
MobileScannerController controller,
) {
return;
}
Future<void> _ownerDialogBuilder(
BuildContext context,
MobileScannerController controller,
AddViewModel viewModel,
) {
controller.stop();
void onPressAccept(BuildContext localContext) {
controller.start();
Navigator.of(localContext).pop();
}
return showDialog(
context: context,
barrierDismissible: false,
builder: (context) =>
OwnerPopup(viewModel: viewModel, onPressAccept: onPressAccept),
);
}

View file

View file

@ -0,0 +1,172 @@
import 'package:flutter/material.dart';
import 'package:seshat/domain/models/owner.dart';
import 'package:seshat/ui/add_page/view_model/add_view_model.dart';
class OwnerPopup extends StatefulWidget {
const OwnerPopup({
super.key,
required this.viewModel,
required this.onPressAccept,
});
final AddViewModel viewModel;
final Function(BuildContext) onPressAccept;
@override
State<OwnerPopup> createState() => _OwnerPopupState();
}
class _OwnerPopupState extends State<OwnerPopup> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
bool showNewOwner = false;
String? firstName;
String? lastName;
String? contact;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return ListenableBuilder(
listenable: widget.viewModel,
builder: (context, child) => AlertDialog(
title: Center(child: Text("Propriétaire du livre")),
content: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Center(
child: Text(
(widget.viewModel.currentOwner == null)
? "Choix actuel : aucun"
: "Choix actuel : ${widget.viewModel.currentOwner!.firstName} ${widget.viewModel.currentOwner!.lastName}",
),
),
SizedBox(height: 5),
(showNewOwner)
? SizedBox()
: DropdownMenu<Owner>(
enableFilter: true,
label: Text("Rechercher un·e propriétaire"),
dropdownMenuEntries: [
for (var owner in widget.viewModel.owners!)
DropdownMenuEntry(
value: owner,
label: "${owner.firstName} ${owner.lastName}",
style: ButtonStyle(
backgroundColor:
(widget.viewModel.currentOwner == owner)
? WidgetStatePropertyAll<Color>(
theme.highlightColor,
)
: WidgetStatePropertyAll<Color>(
theme.canvasColor,
),
),
),
],
initialSelection: widget.viewModel.currentOwner,
onSelected: (Owner? owner) {
widget.viewModel.currentOwner = owner;
},
),
SizedBox(height: 20),
TextButton(
onPressed: () {
setState(() {
showNewOwner = !showNewOwner;
});
},
child: Text(
(showNewOwner) ? "Annuler" : "Ajouter un propriétaire",
),
),
(!showNewOwner)
? SizedBox()
: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(
labelText: "Nom",
border: OutlineInputBorder(),
),
onSaved: (newValue) {
setState(() {
lastName = newValue;
});
},
validator: (value) {
if (value == null || value.isEmpty) {
return "Indiquez un nom";
}
return null;
},
),
SizedBox(height: 10),
TextFormField(
decoration: InputDecoration(
labelText: "Prénom",
border: OutlineInputBorder(),
),
onSaved: (newValue) {
setState(() {
firstName = newValue;
});
},
validator: (value) {
if (value == null || value.isEmpty) {
return "Indiquez un prénom";
}
return null;
},
),
SizedBox(height: 10),
TextFormField(
decoration: InputDecoration(
labelText: "Contact",
border: OutlineInputBorder(),
),
onSaved: (newValue) {
setState(() {
contact = newValue;
});
},
validator: (value) {
if (value == null || value.isEmpty) {
return "Indiquez un moyen de contact";
}
return null;
},
),
SizedBox(height: 10),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
setState(() {
widget.viewModel.currentOwner = widget.viewModel
.addOwner(firstName!, lastName!, contact!);
showNewOwner = false;
});
}
},
child: Text("Créer"),
),
],
),
),
],
),
actions: [
TextButton(
onPressed: () {
widget.onPressAccept(context);
},
child: Text("Valider"),
),
],
),
);
}
}

View file

@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:provider/provider.dart';
// class ScanPage extends StatefulWidget {
// const ScanPage({super.key});
// @override
// State<ScanPage> createState() => _ScanPage();
// }
// class _ScanPage extends State<ScanPage> {
// final MobileScannerController controller = MobileScannerController(
// detectionTimeoutMs: 1000,
// );
// @override
// Widget build(BuildContext context) {
// return Stack(
// children: <Widget>[
// MobileScanner(
// controller: controller,
// onDetect: (result) {
// print(result.barcodes.first.rawValue);
// },
// ),
// SafeArea(
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.center,
// children: [
// Center(
// child: Card(
// margin: EdgeInsets.symmetric(horizontal: 50),
// child: Column(
// children: [
// Consumer<TabScreen>(
// builder: (context, screen, child) {
// return ListTile(
// leading: Icon(Icons.person),
// title: TextButton(
// child: Text("No"),
// onPressed: () {
// screen.change("ownerPage");
// },
// ),
// );
// },
// ),
// ListTile(
// leading: Icon(Icons.attach_money),
// title: TextButton(
// child: Text("Demander à chaque fois"),
// onPressed: () {
// return;
// },
// ),
// ),
// ],
// ),
// ),
// ),
// Expanded(child: SizedBox()),
// SvgPicture.asset('assets/scan-overlay.svg'),
// Expanded(child: SizedBox()),
// TextButton(
// onPressed: () {},
// child: Text("Enregistrer manuellement"),
// ),
// ],
// ),
// ),
// ],
// );
// }
// }

View file

@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart';
class AppNavigationBar extends StatefulWidget {
const AppNavigationBar({super.key, required this.startIndex});
final int startIndex;
@override
State<AppNavigationBar> createState() => _AppNavigationBarState();
}
class _AppNavigationBarState extends State<AppNavigationBar> {
final Logger _log = Logger("chose");
@override
Widget build(BuildContext context) {
var selectedIndex = widget.startIndex;
_log.info("We at $selectedIndex");
return NavigationBar(
destinations: [
NavigationDestination(icon: Icon(Icons.home), label: "Home"),
NavigationDestination(icon: Icon(Icons.add), label: "Ajout"),
NavigationDestination(icon: Icon(Icons.remove), label: "Vente"),
],
selectedIndex: selectedIndex,
onDestinationSelected: (index) {
setState(() {
selectedIndex = index;
});
switch (index) {
case 0:
context.go('/');
break;
case 1:
context.go('/add');
break;
case 2:
context.go('/sell');
break;
default:
context.go("/");
}
},
);
}
}

View file

@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:seshat/ui/core/ui/navigation_bar.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: AppNavigationBar(startIndex: 0),
body: Center(child: Text("Home page.")),
);
// return Center(child: Text("Home page;"));
}
}

View file

@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:seshat/ui/core/ui/navigation_bar.dart';
class SellPage extends StatelessWidget {
const SellPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: AppNavigationBar(startIndex: 2),
body: Center(child: Text("Sell page.")),
);
// return Center(child: Text("Sell page."));
}
}

97
lib/utils/command.dart Normal file
View file

@ -0,0 +1,97 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'result.dart';
typedef CommandAction0<T> = Future<Result<T>> Function();
typedef CommandAction1<T, A> = Future<Result<T>> Function(A);
/// Facilitates interaction with a ViewModel.
///
/// Encapsulates an action,
/// exposes its running and error states,
/// and ensures that it can't be launched again until it finishes.
///
/// Use [Command0] for actions without arguments.
/// Use [Command1] for actions with one argument.
///
/// Actions must return a [Result].
///
/// Consume the action result by listening to changes,
/// then call to [clearResult] when the state is consumed.
abstract class Command<T> extends ChangeNotifier {
Command();
bool _running = false;
/// True when the action is running.
bool get running => _running;
Result<T>? _result;
/// true if action completed with error
bool get error => _result is Error;
/// true if action completed successfully
bool get completed => _result is Ok;
/// Get last action result
Result? get result => _result;
/// Clear last action result
void clearResult() {
_result = null;
notifyListeners();
}
/// Internal execute implementation
Future<void> _execute(CommandAction0<T> action) async {
// Ensure the action can't launch multiple times.
// e.g. avoid multiple taps on button
if (_running) return;
// Notify listeners.
// e.g. button shows loading state
_running = true;
_result = null;
notifyListeners();
try {
_result = await action();
} finally {
_running = false;
notifyListeners();
}
}
}
/// [Command] without arguments.
/// Takes a [CommandAction0] as action.
class Command0<T> extends Command<T> {
Command0(this._action);
final CommandAction0<T> _action;
/// Executes the action.
Future<void> execute() async {
await _execute(_action);
}
}
/// [Command] with one argument.
/// Takes a [CommandAction1] as action.
class Command1<T, A> extends Command<T> {
Command1(this._action);
final CommandAction1<T, A> _action;
/// Executes the action with the argument.
Future<void> execute(A argument) async {
await _execute(() => _action(argument));
}
}

48
lib/utils/result.dart Normal file
View file

@ -0,0 +1,48 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// Utility class to wrap result data
///
/// Evaluate the result using a switch statement:
/// ```dart
/// switch (result) {
/// case Ok(): {
/// print(result.value);
/// }
/// case Error(): {
/// print(result.error);
/// }
/// }
/// ```
sealed class Result<T> {
const Result();
/// Creates a successful [Result], completed with the specified [value].
const factory Result.ok(T value) = Ok._;
/// Creates an error [Result], completed with the specified [error].
const factory Result.error(Exception error) = Error._;
}
/// Subclass of Result for values
final class Ok<T> extends Result<T> {
const Ok._(this.value);
/// Returned value in result
final T value;
@override
String toString() => 'Result<$T>.ok($value)';
}
/// Subclass of Result for errors
final class Error<T> extends Result<T> {
const Error._(this.error);
/// Returned error in result
final Exception error;
@override
String toString() => 'Result<$T>.error($error)';
}