From 09405b5da7b6841e92ad5817279f9563c2d84186 Mon Sep 17 00:00:00 2001 From: mandreshope Date: Thu, 3 Jul 2025 21:09:49 +0300 Subject: [PATCH] feat: Implement local product storage using ObjectBox Configures the ObjectBox ProductEntity to allow assigning IDs. Adds a helper to convert ProductStruct to ProductEntity for easier storage. Saves scanned products directly to the local ObjectBox store. Updates the ProductListPage to fetch and display locally stored products from ObjectBox upon initialization. Includes minor UI adjustments in the scanned product component and scanner page, and fixes loading state handling in HomePageModel. --- .../entities/product/product_entity.dart | 2 +- lib/backend/objectbox/objectbox-model.json | 2 +- lib/backend/objectbox/objectbox.g.dart | 2 +- lib/backend/objectbox/objectbox_manager.dart | 1 + .../schema/product/product_struct.dart | 12 ++ lib/components/product_scanned_component.dart | 6 +- lib/pages/home_page/home_page_model.dart | 2 +- .../product_list_page/product_list_page.dart | 62 +++++++ .../product_list_page_model.dart | 35 ++++ .../product_list_page_model.freezed.dart | 163 ++++++++++++++++++ lib/pages/scanner_page/scanner_page.dart | 20 ++- 11 files changed, 296 insertions(+), 11 deletions(-) create mode 100644 lib/pages/product_list_page/product_list_page_model.dart create mode 100644 lib/pages/product_list_page/product_list_page_model.freezed.dart diff --git a/lib/backend/objectbox/entities/product/product_entity.dart b/lib/backend/objectbox/entities/product/product_entity.dart index c13a29e..3e85c5a 100644 --- a/lib/backend/objectbox/entities/product/product_entity.dart +++ b/lib/backend/objectbox/entities/product/product_entity.dart @@ -4,7 +4,7 @@ import 'package:objectbox/objectbox.dart'; /// Modèle de base de données ObjectBox @Entity() class ProductEntity { - @Id() + @Id(assignable: true) int id; String? code; diff --git a/lib/backend/objectbox/objectbox-model.json b/lib/backend/objectbox/objectbox-model.json index 0cdd6b2..e65b9e2 100644 --- a/lib/backend/objectbox/objectbox-model.json +++ b/lib/backend/objectbox/objectbox-model.json @@ -12,7 +12,7 @@ "id": "1:1853465479129290672", "name": "id", "type": 6, - "flags": 1 + "flags": 129 }, { "id": "2:4521897043130066476", diff --git a/lib/backend/objectbox/objectbox.g.dart b/lib/backend/objectbox/objectbox.g.dart index be94e25..19611ef 100644 --- a/lib/backend/objectbox/objectbox.g.dart +++ b/lib/backend/objectbox/objectbox.g.dart @@ -29,7 +29,7 @@ final _entities = [ id: const obx_int.IdUid(1, 1853465479129290672), name: 'id', type: 6, - flags: 1, + flags: 129, ), obx_int.ModelProperty( id: const obx_int.IdUid(2, 4521897043130066476), diff --git a/lib/backend/objectbox/objectbox_manager.dart b/lib/backend/objectbox/objectbox_manager.dart index 36f626e..c01c6d7 100644 --- a/lib/backend/objectbox/objectbox_manager.dart +++ b/lib/backend/objectbox/objectbox_manager.dart @@ -3,6 +3,7 @@ import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:barcode_scanner/backend/objectbox/objectbox.g.dart'; +export 'package:barcode_scanner/backend/objectbox/objectbox.g.dart'; late ObjectboxManager objectboxManager; diff --git a/lib/backend/schema/product/product_struct.dart b/lib/backend/schema/product/product_struct.dart index 1796ef4..901233e 100644 --- a/lib/backend/schema/product/product_struct.dart +++ b/lib/backend/schema/product/product_struct.dart @@ -1,3 +1,4 @@ +import 'package:barcode_scanner/backend/objectbox/entities/product/product_entity.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'product_struct.freezed.dart'; @@ -18,3 +19,14 @@ abstract class ProductStruct with _$ProductStruct { factory ProductStruct.fromJson(Map json) => _$ProductStructFromJson(json); } + +extension ProductStructExt on ProductStruct { + ProductEntity get toEntity => ProductEntity( + id: id, + name: name, + description: description, + price: price, + quantity: quantity, + image: image, + ); +} diff --git a/lib/components/product_scanned_component.dart b/lib/components/product_scanned_component.dart index 1a9aa71..3e8641c 100644 --- a/lib/components/product_scanned_component.dart +++ b/lib/components/product_scanned_component.dart @@ -73,7 +73,7 @@ class _ProductScannedComponentState extends State { alignment: Alignment.topCenter, child: Image.network( widget.productStruct.image ?? '', - height: MediaQuery.sizeOf(context).height * .2, + height: MediaQuery.sizeOf(context).height * .12, ), ), SizedBox(height: 15), @@ -164,7 +164,9 @@ class _ProductScannedComponentState extends State { }, child: Text( 'Voir détails', - style: AppTheme.of(context).bodyMedium, + style: AppTheme.of(context).bodyMedium.override( + color: AppTheme.of(context).white, + ), ), ), ), diff --git a/lib/pages/home_page/home_page_model.dart b/lib/pages/home_page/home_page_model.dart index fb314ea..6746384 100644 --- a/lib/pages/home_page/home_page_model.dart +++ b/lib/pages/home_page/home_page_model.dart @@ -29,7 +29,7 @@ class HomePageModel extends StateNotifier { Future getUserConnected() async { state = state.copyWith(loading: true); final user = await UserStruct(id: '1').getFromLocalStorage(); - state = state.copyWith(user: user); + state = state.copyWith(user: user, loading: false); } } diff --git a/lib/pages/product_list_page/product_list_page.dart b/lib/pages/product_list_page/product_list_page.dart index 4173c16..ff32bb4 100644 --- a/lib/pages/product_list_page/product_list_page.dart +++ b/lib/pages/product_list_page/product_list_page.dart @@ -1,5 +1,9 @@ +import 'package:barcode_scanner/components/loading_progress_component.dart'; +import 'package:barcode_scanner/pages/product_list_page/product_list_page_model.dart'; +import 'package:barcode_scanner/router/go_secure_router_builder.dart'; import 'package:barcode_scanner/themes/app_theme.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class ProductListPage extends ConsumerStatefulWidget { @@ -11,8 +15,17 @@ class ProductListPage extends ConsumerStatefulWidget { } class _ProductListPageState extends ConsumerState { + @override + void initState() { + super.initState(); + SchedulerBinding.instance.addPostFrameCallback((_) { + ref.read(productListPageModelProvider.notifier).getProductsLocal(); + }); + } + @override Widget build(BuildContext context) { + final state = ref.watch(productListPageModelProvider); return Scaffold( backgroundColor: AppTheme.of(context).primaryBackground, appBar: AppBar( @@ -33,6 +46,55 @@ class _ProductListPageState extends ConsumerState { backgroundColor: AppTheme.of(context).primaryBackground, elevation: 0, ), + body: state.loading + ? Center(child: LoadingProgressComponent()) + : Builder( + builder: (context) { + if (state.products.isEmpty) { + return Center(child: Text('Aucun donnée trouvée')); + } else { + return ListView.builder( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 20), + itemCount: state.products.length, + itemBuilder: (context, index) { + final product = state.products[index]; + return ListTile( + onTap: () { + ProductFormRoute().push(context); + }, + leading: Container( + height: 60, + width: 60, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(60), + image: DecorationImage( + image: NetworkImage(product.image ?? ''), + ), + ), + ), + title: Text( + product.name ?? '', + style: AppTheme.of(context).titleMedium, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Code: ${product.id}", + style: AppTheme.of(context).bodyMedium, + ), + Text( + "Quatité: ${product.quantity ?? ''}", + style: AppTheme.of(context).bodyMedium, + ), + ], + ), + ); + }, + ); + } + }, + ), ); } } diff --git a/lib/pages/product_list_page/product_list_page_model.dart b/lib/pages/product_list_page/product_list_page_model.dart new file mode 100644 index 0000000..7099634 --- /dev/null +++ b/lib/pages/product_list_page/product_list_page_model.dart @@ -0,0 +1,35 @@ +import 'package:barcode_scanner/backend/objectbox/entities/product/product_entity.dart'; +import 'package:barcode_scanner/backend/objectbox/objectbox_manager.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'product_list_page_model.freezed.dart'; + +/// The provider for the AuthViewModel, using Riverpod's StateNotifierProvider +/// with autoDispose to manage the lifecycle of the view model. +final productListPageModelProvider = + StateNotifierProvider((ref) { + return ProductListPageModel(); + }); + +class ProductListPageModel extends StateNotifier { + /// Constructor initializes the TaskRepository using the provider reference. + ProductListPageModel() : super(const ProductListPageState()); + + Future getProductsLocal() async { + Box productStore = objectboxManager.store + .box(); + state = state.copyWith(loading: true); + final products = await productStore.getAllAsync(); + state = state.copyWith(products: products, loading: false); + } +} + +@freezed +abstract class ProductListPageState with _$ProductListPageState { + const factory ProductListPageState({ + @Default([]) List products, + @Default(false) bool loading, + }) = _ProductListPageState; +} diff --git a/lib/pages/product_list_page/product_list_page_model.freezed.dart b/lib/pages/product_list_page/product_list_page_model.freezed.dart new file mode 100644 index 0000000..7c3660e --- /dev/null +++ b/lib/pages/product_list_page/product_list_page_model.freezed.dart @@ -0,0 +1,163 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'product_list_page_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$ProductListPageState implements DiagnosticableTreeMixin { + + List get products; bool get loading; +/// Create a copy of ProductListPageState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ProductListPageStateCopyWith get copyWith => _$ProductListPageStateCopyWithImpl(this as ProductListPageState, _$identity); + + +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'ProductListPageState')) + ..add(DiagnosticsProperty('products', products))..add(DiagnosticsProperty('loading', loading)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ProductListPageState&&const DeepCollectionEquality().equals(other.products, products)&&(identical(other.loading, loading) || other.loading == loading)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(products),loading); + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'ProductListPageState(products: $products, loading: $loading)'; +} + + +} + +/// @nodoc +abstract mixin class $ProductListPageStateCopyWith<$Res> { + factory $ProductListPageStateCopyWith(ProductListPageState value, $Res Function(ProductListPageState) _then) = _$ProductListPageStateCopyWithImpl; +@useResult +$Res call({ + List products, bool loading +}); + + + + +} +/// @nodoc +class _$ProductListPageStateCopyWithImpl<$Res> + implements $ProductListPageStateCopyWith<$Res> { + _$ProductListPageStateCopyWithImpl(this._self, this._then); + + final ProductListPageState _self; + final $Res Function(ProductListPageState) _then; + +/// Create a copy of ProductListPageState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? products = null,Object? loading = null,}) { + return _then(_self.copyWith( +products: null == products ? _self.products : products // ignore: cast_nullable_to_non_nullable +as List,loading: null == loading ? _self.loading : loading // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +} + + +/// @nodoc + + +class _ProductListPageState with DiagnosticableTreeMixin implements ProductListPageState { + const _ProductListPageState({final List products = const [], this.loading = false}): _products = products; + + + final List _products; +@override@JsonKey() List get products { + if (_products is EqualUnmodifiableListView) return _products; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_products); +} + +@override@JsonKey() final bool loading; + +/// Create a copy of ProductListPageState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ProductListPageStateCopyWith<_ProductListPageState> get copyWith => __$ProductListPageStateCopyWithImpl<_ProductListPageState>(this, _$identity); + + +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'ProductListPageState')) + ..add(DiagnosticsProperty('products', products))..add(DiagnosticsProperty('loading', loading)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProductListPageState&&const DeepCollectionEquality().equals(other._products, _products)&&(identical(other.loading, loading) || other.loading == loading)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_products),loading); + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'ProductListPageState(products: $products, loading: $loading)'; +} + + +} + +/// @nodoc +abstract mixin class _$ProductListPageStateCopyWith<$Res> implements $ProductListPageStateCopyWith<$Res> { + factory _$ProductListPageStateCopyWith(_ProductListPageState value, $Res Function(_ProductListPageState) _then) = __$ProductListPageStateCopyWithImpl; +@override @useResult +$Res call({ + List products, bool loading +}); + + + + +} +/// @nodoc +class __$ProductListPageStateCopyWithImpl<$Res> + implements _$ProductListPageStateCopyWith<$Res> { + __$ProductListPageStateCopyWithImpl(this._self, this._then); + + final _ProductListPageState _self; + final $Res Function(_ProductListPageState) _then; + +/// Create a copy of ProductListPageState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? products = null,Object? loading = null,}) { + return _then(_ProductListPageState( +products: null == products ? _self._products : products // ignore: cast_nullable_to_non_nullable +as List,loading: null == loading ? _self.loading : loading // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + +// dart format on diff --git a/lib/pages/scanner_page/scanner_page.dart b/lib/pages/scanner_page/scanner_page.dart index 287c69b..171c521 100644 --- a/lib/pages/scanner_page/scanner_page.dart +++ b/lib/pages/scanner_page/scanner_page.dart @@ -1,7 +1,10 @@ import 'dart:async'; import 'package:barcode_scanner/backend/api/api_calls.dart'; +import 'package:barcode_scanner/backend/objectbox/entities/product/product_entity.dart'; +import 'package:barcode_scanner/backend/objectbox/objectbox_manager.dart'; import 'package:barcode_scanner/backend/schema/product/product_struct.dart'; +import 'package:barcode_scanner/components/loading_progress_component.dart'; import 'package:barcode_scanner/components/product_scanned_component.dart'; import 'package:barcode_scanner/router/go_secure_router_builder.dart'; import 'package:barcode_scanner/themes/app_theme.dart'; @@ -107,11 +110,14 @@ class _ScannerPageState extends ConsumerState debugPrint('Image : ${product["image_url"]}'); final productStruct = ProductStruct( id: int.parse(product["id"]), - name: product["product_name"], image: product["image_thumb_url"], - description: product["categories"], + name: product["generic_name"], + description: product["product_name"], quantity: product["quantity"], ); + Box productStore = objectboxManager.store + .box(); + productStore.put(productStruct.toEntity); //show dialog await showDialog( barrierDismissible: false, @@ -179,10 +185,13 @@ class _ScannerPageState extends ConsumerState final productStruct = ProductStruct( id: int.parse(product["id"]), image: product["image_thumb_url"], - name: product["product_name"], - description: product["categories"], + name: product["generic_name"], + description: product["product_name"], quantity: product["quantity"], ); + Box productStore = objectboxManager.store + .box(); + productStore.put(productStruct.toEntity); //show dialog await showDialog( barrierDismissible: false, @@ -234,6 +243,7 @@ class _ScannerPageState extends ConsumerState @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: AppTheme.of(context).primaryBackground, appBar: AppBar( backgroundColor: AppTheme.of(context).primaryBackground, automaticallyImplyLeading: false, @@ -263,7 +273,7 @@ class _ScannerPageState extends ConsumerState elevation: 0.0, ), body: loading - ? Center(child: CircularProgressIndicator()) + ? Center(child: LoadingProgressComponent()) : SizedBox( width: double.infinity, height: double.infinity,