From 754048b5652f15e8270c370d36b9642d691f5109 Mon Sep 17 00:00:00 2001 From: efrilm Date: Sat, 25 Oct 2025 00:13:43 +0700 Subject: [PATCH] product and category loader --- .../category_loader/category_loader_bloc.dart | 2 + lib/common/extension/double_extension.dart | 15 ++ lib/common/extension/extension.dart | 3 + lib/common/extension/int_extension.dart | 15 ++ lib/injection.config.dart | 5 + lib/presentation/app_widget.dart | 2 + .../components/card/error_card.dart | 47 ++++ .../components/card/product_card.dart | 110 ++++++++++ lib/presentation/components/image/image.dart | 8 + .../components/image/network_image.dart | 77 +++++++ .../pages/main/pages/home/home_page.dart | 22 +- .../pages/home/widgets/category_tabbar.dart | 129 +++++++++++ .../pages/home/widgets/home_left_panel.dart | 205 +++++++++++++++++- lib/presentation/router/app_router.gr.dart | 4 +- pubspec.lock | 72 ++++++ pubspec.yaml | 2 + 16 files changed, 712 insertions(+), 6 deletions(-) create mode 100644 lib/common/extension/double_extension.dart create mode 100644 lib/common/extension/int_extension.dart create mode 100644 lib/presentation/components/card/error_card.dart create mode 100644 lib/presentation/components/card/product_card.dart create mode 100644 lib/presentation/components/image/image.dart create mode 100644 lib/presentation/components/image/network_image.dart create mode 100644 lib/presentation/pages/main/pages/home/widgets/category_tabbar.dart diff --git a/lib/application/category/category_loader/category_loader_bloc.dart b/lib/application/category/category_loader/category_loader_bloc.dart index 38d4e8c..4f92936 100644 --- a/lib/application/category/category_loader/category_loader_bloc.dart +++ b/lib/application/category/category_loader/category_loader_bloc.dart @@ -4,6 +4,7 @@ import 'dart:developer'; import 'package:bloc/bloc.dart'; import 'package:dartz/dartz.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:injectable/injectable.dart'; import '../../../domain/category/category.dart'; @@ -11,6 +12,7 @@ part 'category_loader_event.dart'; part 'category_loader_state.dart'; part 'category_loader_bloc.freezed.dart'; +@injectable class CategoryLoaderBloc extends Bloc { final ICategoryRepository _categoryRepository; diff --git a/lib/common/extension/double_extension.dart b/lib/common/extension/double_extension.dart new file mode 100644 index 0000000..a8cf993 --- /dev/null +++ b/lib/common/extension/double_extension.dart @@ -0,0 +1,15 @@ +part of 'extension.dart'; + +extension DoubleExt on double { + String get currencyFormatRp => NumberFormat.currency( + locale: 'id', + symbol: 'Rp. ', + decimalDigits: 0, + ).format(this); + + String get currencyFormatRpV2 => NumberFormat.currency( + locale: 'id', + symbol: 'Rp ', + decimalDigits: 0, + ).format(this); +} diff --git a/lib/common/extension/extension.dart b/lib/common/extension/extension.dart index 43176ba..2af6b65 100644 --- a/lib/common/extension/extension.dart +++ b/lib/common/extension/extension.dart @@ -1,3 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; part 'build_context_extension.dart'; +part 'int_extension.dart'; +part 'double_extension.dart'; diff --git a/lib/common/extension/int_extension.dart b/lib/common/extension/int_extension.dart new file mode 100644 index 0000000..88f9de4 --- /dev/null +++ b/lib/common/extension/int_extension.dart @@ -0,0 +1,15 @@ +part of 'extension.dart'; + +extension IntegerExt on int { + String get currencyFormatRp => NumberFormat.currency( + locale: 'id', + symbol: 'Rp. ', + decimalDigits: 0, + ).format(this); + + String get currencyFormatRpV2 => NumberFormat.currency( + locale: 'id', + symbol: 'Rp ', + decimalDigits: 0, + ).format(this); +} diff --git a/lib/injection.config.dart b/lib/injection.config.dart index bf7d081..21300f7 100644 --- a/lib/injection.config.dart +++ b/lib/injection.config.dart @@ -12,6 +12,8 @@ import 'package:apskel_pos_flutter_v2/application/auth/auth_bloc.dart' as _i343; import 'package:apskel_pos_flutter_v2/application/auth/login_form/login_form_bloc.dart' as _i46; +import 'package:apskel_pos_flutter_v2/application/category/category_loader/category_loader_bloc.dart' + as _i1018; import 'package:apskel_pos_flutter_v2/application/outlet/outlet_loader/outlet_loader_bloc.dart' as _i76; import 'package:apskel_pos_flutter_v2/application/product/product_loader/product_loader_bloc.dart' @@ -147,6 +149,9 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i46.LoginFormBloc>( () => _i46.LoginFormBloc(gh<_i776.IAuthRepository>()), ); + gh.factory<_i1018.CategoryLoaderBloc>( + () => _i1018.CategoryLoaderBloc(gh<_i502.ICategoryRepository>()), + ); gh.factory<_i343.AuthBloc>( () => _i343.AuthBloc( gh<_i776.IAuthRepository>(), diff --git a/lib/presentation/app_widget.dart b/lib/presentation/app_widget.dart index 8cbfb23..5a9eed3 100644 --- a/lib/presentation/app_widget.dart +++ b/lib/presentation/app_widget.dart @@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../application/auth/auth_bloc.dart'; import '../application/category/category_loader/category_loader_bloc.dart'; import '../application/outlet/outlet_loader/outlet_loader_bloc.dart'; +import '../application/product/product_loader/product_loader_bloc.dart'; import '../common/theme/theme.dart'; import '../common/constant/app_constant.dart'; import '../injection.dart'; @@ -27,6 +28,7 @@ class _AppWidgetState extends State { BlocProvider(create: (context) => getIt()), BlocProvider(create: (context) => getIt()), BlocProvider(create: (context) => getIt()), + BlocProvider(create: (context) => getIt()), ], child: MaterialApp.router( debugShowCheckedModeBanner: false, diff --git a/lib/presentation/components/card/error_card.dart b/lib/presentation/components/card/error_card.dart new file mode 100644 index 0000000..9653009 --- /dev/null +++ b/lib/presentation/components/card/error_card.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +import '../button/button.dart'; + +class ErrorCard extends StatelessWidget { + final String title; + final String message; + final Function() onTap; + const ErrorCard({ + super.key, + required this.title, + required this.message, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, size: 48, color: Colors.red.shade400), + SizedBox(height: 16), + Text( + title, + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + SizedBox(height: 8), + Padding( + padding: EdgeInsets.symmetric(horizontal: 32), + child: Text( + message, + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey.shade600), + ), + ), + SizedBox(height: 16), + AppElevatedButton.filled( + width: 120, + onPressed: onTap, + label: 'Coba Lagi', + ), + ], + ), + ); + } +} diff --git a/lib/presentation/components/card/product_card.dart b/lib/presentation/components/card/product_card.dart new file mode 100644 index 0000000..e0b775b --- /dev/null +++ b/lib/presentation/components/card/product_card.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; + +import '../../../common/extension/extension.dart'; +import '../../../common/theme/theme.dart'; +import '../../../domain/product/product.dart'; +import '../image/image.dart'; +import '../spaces/space.dart'; + +class ProductCard extends StatelessWidget { + final Product product; + const ProductCard({super.key, required this.product}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () {}, + child: Container( + decoration: BoxDecoration( + color: AppColor.white, + borderRadius: BorderRadius.circular(8.0), + border: Border.all(color: AppColor.disabled), + ), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.all(4.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + child: product.imageUrl == "" + ? Container( + width: double.infinity, + height: 120, + decoration: BoxDecoration( + color: AppColor.disabled.withOpacity(0.4), + ), + child: const Icon( + Icons.image, + color: AppColor.textSecondary, + ), + ) + : AppNetworkImage( + url: product.imageUrl, + fit: BoxFit.fill, + width: double.infinity, + height: 140, + ), + ), + const Spacer(), + Text( + product.name, + style: AppStyle.md.copyWith(fontWeight: FontWeight.w700), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + SpaceHeight(4), + Align( + alignment: Alignment.center, + child: Text( + product.price.currencyFormatRp, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + SpaceHeight(4), + ], + ), + ), + + Positioned( + top: 4, + right: 4, + child: Container( + width: 40, + height: 40, + padding: const EdgeInsets.all(6), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(9.0)), + color: AppColor.primary, + ), + child: Center( + child: Text( + 0.toString(), + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + if (product.isActive == false) + Container( + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + color: AppColor.disabled.withOpacity(0.5), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/components/image/image.dart b/lib/presentation/components/image/image.dart new file mode 100644 index 0000000..1054089 --- /dev/null +++ b/lib/presentation/components/image/image.dart @@ -0,0 +1,8 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; + +import '../../../common/theme/theme.dart'; + +part 'network_image.dart'; diff --git a/lib/presentation/components/image/network_image.dart b/lib/presentation/components/image/network_image.dart new file mode 100644 index 0000000..43d9fc9 --- /dev/null +++ b/lib/presentation/components/image/network_image.dart @@ -0,0 +1,77 @@ +part of 'image.dart'; + +class AppNetworkImage extends StatelessWidget { + final String? url; + final double? height; + final double? width; + final double? borderRadius; + final BoxFit? fit; + final bool? isCanZoom; + final VoidCallback? onTap; + + const AppNetworkImage({ + super.key, + this.url, + this.height, + this.width, + this.borderRadius = 0, + this.fit = BoxFit.cover, + this.isCanZoom = false, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + Widget customPhoto( + double? heightx, + double? widthx, + BoxFit? fitx, + double? radius, + ) { + return CachedNetworkImage( + imageUrl: url.toString(), + memCacheHeight: 120, + memCacheWidth: 120, + placeholder: (context, url) => Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Container( + height: height, + width: width, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(radius ?? 0), + ), + ), + ), + errorWidget: (context, url, error) { + FirebaseCrashlytics.instance.recordError( + error, + StackTrace.current, + reason: 'Failed to load image from: $url', + fatal: false, + ); + return Container( + width: double.infinity, + height: 120, + decoration: BoxDecoration( + color: AppColor.disabled.withOpacity(0.4), + ), + child: const Icon(Icons.image, color: AppColor.disabled), + ); + }, + height: heightx, + width: widthx, + fit: fitx, + ); + } + + return GestureDetector( + onTap: onTap, + child: ClipRRect( + borderRadius: BorderRadius.circular(borderRadius!), + child: customPhoto(height, width, BoxFit.fill, borderRadius), + ), + ); + } +} diff --git a/lib/presentation/pages/main/pages/home/home_page.dart b/lib/presentation/pages/main/pages/home/home_page.dart index b1ac15f..8ae2600 100644 --- a/lib/presentation/pages/main/pages/home/home_page.dart +++ b/lib/presentation/pages/main/pages/home/home_page.dart @@ -1,12 +1,16 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../../../application/category/category_loader/category_loader_bloc.dart'; +import '../../../../../application/product/product_loader/product_loader_bloc.dart'; import '../../../../../common/theme/theme.dart'; +import '../../../../../injection.dart'; import 'widgets/home_left_panel.dart'; import 'widgets/home_right_panel.dart'; @RoutePage() -class HomePage extends StatelessWidget { +class HomePage extends StatelessWidget implements AutoRouteWrapper { const HomePage({super.key}); @override @@ -30,4 +34,20 @@ class HomePage extends StatelessWidget { ), ); } + + @override + Widget wrappedRoute(BuildContext context) => MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => + getIt() + ..add(CategoryLoaderEvent.getCategories()), + ), + BlocProvider( + create: (context) => + getIt()..add(ProductLoaderEvent.getProduct()), + ), + ], + child: this, + ); } diff --git a/lib/presentation/pages/main/pages/home/widgets/category_tabbar.dart b/lib/presentation/pages/main/pages/home/widgets/category_tabbar.dart new file mode 100644 index 0000000..6fe5ad0 --- /dev/null +++ b/lib/presentation/pages/main/pages/home/widgets/category_tabbar.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../../../application/product/product_loader/product_loader_bloc.dart'; +import '../../../../../../common/theme/theme.dart'; +import '../../../../../../domain/category/category.dart'; + +class CategoryTabBar extends StatefulWidget { + final List categories; + final List tabViews; + const CategoryTabBar({ + super.key, + required this.categories, + required this.tabViews, + }); + + @override + State createState() => _CategoryTabBarState(); +} + +class _CategoryTabBarState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + String? selectedCategoryId; + + @override + void initState() { + super.initState(); + _tabController = TabController( + length: widget.categories.length, + vsync: this, + ); + + _tabController.addListener(() { + if (_tabController.indexIsChanging) { + if (_tabController.index == 0) { + context.read().add( + ProductLoaderEvent.getProduct(), + ); + } else { + selectedCategoryId = widget.categories[_tabController.index].id; + context.read().add( + ProductLoaderEvent.getProduct(categoryId: selectedCategoryId), + ); + } + } + }); + } + + @override + void didUpdateWidget(CategoryTabBar oldWidget) { + super.didUpdateWidget(oldWidget); + + // ✅ Update TabController when categories length changes + if (oldWidget.categories.length != widget.categories.length) { + _tabController.dispose(); + _tabController = TabController( + length: widget.categories.length, + vsync: this, + initialIndex: 0, // Reset to first tab + ); + + _tabController.addListener(() { + if (_tabController.indexIsChanging) { + if (_tabController.index == 0) { + context.read().add( + ProductLoaderEvent.getProduct(), + ); + } else { + selectedCategoryId = widget.categories[_tabController.index].id; + context.read().add( + ProductLoaderEvent.getProduct(categoryId: selectedCategoryId), + ); + } + } + }); + } + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Material( + elevation: 0, + color: Colors.white, + borderOnForeground: false, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: TabBar( + controller: _tabController, + isScrollable: true, + tabAlignment: TabAlignment.start, + labelColor: AppColor.primary, + labelStyle: TextStyle(fontWeight: FontWeight.bold), + dividerColor: AppColor.primary, + unselectedLabelColor: AppColor.primary, + indicatorSize: TabBarIndicatorSize.label, + indicatorWeight: 4, + indicatorColor: AppColor.primary, + tabs: widget.categories + .map( + (category) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Tab(text: category.name), + ), + ) + .toList(), + ), + ), + ), + Expanded( + // ✅ ini bagian penting + child: TabBarView( + controller: _tabController, + children: widget.tabViews, + ), + ), + ], + ); + } +} diff --git a/lib/presentation/pages/main/pages/home/widgets/home_left_panel.dart b/lib/presentation/pages/main/pages/home/widgets/home_left_panel.dart index 8d6daf1..ddd2a4f 100644 --- a/lib/presentation/pages/main/pages/home/widgets/home_left_panel.dart +++ b/lib/presentation/pages/main/pages/home/widgets/home_left_panel.dart @@ -1,15 +1,214 @@ -import 'package:flutter/widgets.dart'; +import 'dart:developer'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; + +import '../../../../../../application/category/category_loader/category_loader_bloc.dart'; +import '../../../../../../application/product/product_loader/product_loader_bloc.dart'; +import '../../../../../../common/theme/theme.dart'; +import '../../../../../../domain/category/category.dart'; +import '../../../../../components/card/error_card.dart'; +import '../../../../../components/card/product_card.dart'; +import '../../../../../components/loader/loader_with_text.dart'; +import 'category_tabbar.dart'; import 'home_title.dart'; -class HomeLeftPanel extends StatelessWidget { +class HomeLeftPanel extends StatefulWidget { const HomeLeftPanel({super.key}); + @override + State createState() => _HomeLeftPanelState(); +} + +class _HomeLeftPanelState extends State { + final searchController = TextEditingController(); + final ScrollController scrollController = ScrollController(); + String searchQuery = ''; + + bool _handleScrollNotification( + ScrollNotification notification, + String? categoryId, + ) { + if (notification is ScrollEndNotification && + scrollController.position.extentAfter == 0) { + log('📄 Loading more products...'); + context.read().add(ProductLoaderEvent.loadMore()); + return true; + } + return false; + } + @override Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state.isLoadingMore) { + return Center(child: LoaderWithText()); + } + return state.failureOptionCategory.fold( + () => _buildContent(context, state.categories), + (f) => f.maybeMap( + orElse: () => ErrorCard( + title: 'Error Kategori', + message: 'Terjadi kesalahan saat memuat kategori', + onTap: () => context.read().add( + CategoryLoaderEvent.getCategories(), + ), + ), + dynamicErrorMessage: (value) => ErrorCard( + title: 'Error Kategori', + message: value.erroMessage, + onTap: () => context.read().add( + CategoryLoaderEvent.getCategories(), + ), + ), + localStorageError: (value) => ErrorCard( + title: 'Error Kategori', + message: value.erroMessage, + onTap: () => context.read().add( + CategoryLoaderEvent.getCategories(), + ), + ), + serverError: (value) => ErrorCard( + title: 'Error Kategori', + message: 'Terjadi kesalahan saat memuat kategori', + onTap: () => context.read().add( + CategoryLoaderEvent.getCategories(), + ), + ), + unexpectedError: (value) => ErrorCard( + title: 'Error Kategori', + message: 'Terjadi kesalahan saat memuat kategori', + onTap: () => context.read().add( + CategoryLoaderEvent.getCategories(), + ), + ), + empty: (value) => ErrorCard( + title: 'Error Kategori', + message: 'Kategori kosong', + onTap: () => context.read().add( + CategoryLoaderEvent.getCategories(), + ), + ), + ), + ); + }, + ); + } + + Column _buildContent(BuildContext context, List categories) { return Column( children: [ - HomeTitle(controller: TextEditingController(), onChanged: (value) {}), + HomeTitle( + controller: searchController, + onChanged: (value) { + setState(() { + searchQuery = value; + }); + + // Fast local search + Future.delayed(Duration(milliseconds: 200), () { + if (value == searchController.text) { + log('🔍 Local search: "$value"'); + context.read().add( + ProductLoaderEvent.searchProduct(query: value), + ); + } + }); + }, + ), + Expanded( + child: BlocBuilder( + builder: (context, state) { + return CategoryTabBar( + key: ValueKey(categories.length), + categories: categories, + tabViews: categories.map((category) { + return SizedBox( + child: state.failureOptionProduct.fold( + () => _buildProductGrid( + state.products, + state.hasReachedMax, + state.isLoadingMore, + state.categoryId, + state.page, + ), + (f) => f.maybeMap( + orElse: () => ErrorCard( + title: 'Error Produk', + message: 'Terjadi kesalahan saat memuat produk', + onTap: () => context.read().add( + ProductLoaderEvent.getProduct(), + ), + ), + ), + ), + ); + }).toList(), + ); + }, + ), + ), + ], + ); + } + + Widget _buildProductGrid( + List products, + bool hasReachedMax, + bool isLoadingMore, + String? categoryId, + int currentPage, + ) { + return Column( + children: [ + if (products.isNotEmpty) + Container( + padding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 0, + ).copyWith(bottom: 6), + child: Row( + children: [ + Text( + '${products.length} produk ditemukan', + style: AppStyle.sm.copyWith(color: AppColor.textSecondary), + ), + Spacer(), + if (isLoadingMore) + SizedBox( + width: 12, + height: 12, + child: SpinKitFadingCircle( + color: AppColor.primary, + size: 12, + ), + ), + ], + ), + ), + + Expanded( + child: NotificationListener( + onNotification: (notification) => + _handleScrollNotification(notification, categoryId), + child: GridView.builder( + itemCount: products.length, + controller: scrollController, + padding: const EdgeInsets.all(16), + cacheExtent: 200.0, + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 180, + mainAxisSpacing: 30, + crossAxisSpacing: 30, + childAspectRatio: 180 / 240, + ), + itemBuilder: (context, index) => + ProductCard(product: products[index]), + ), + ), + ), ], ); } diff --git a/lib/presentation/router/app_router.gr.dart b/lib/presentation/router/app_router.gr.dart index fb8a0a1..c1bff48 100644 --- a/lib/presentation/router/app_router.gr.dart +++ b/lib/presentation/router/app_router.gr.dart @@ -56,7 +56,7 @@ class HomeRoute extends _i10.PageRouteInfo { static _i10.PageInfo page = _i10.PageInfo( name, builder: (data) { - return const _i2.HomePage(); + return _i10.WrappedRoute(child: const _i2.HomePage()); }, ); } @@ -152,7 +152,7 @@ class SyncRoute extends _i10.PageRouteInfo { static _i10.PageInfo page = _i10.PageInfo( name, builder: (data) { - return const _i8.SyncPage(); + return _i10.WrappedRoute(child: const _i8.SyncPage()); }, ); } diff --git a/pubspec.lock b/pubspec.lock index b7f6046..84180c9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -161,6 +161,30 @@ packages: url: "https://pub.dev" source: hosted version: "8.12.0" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" characters: dependency: transitive description: @@ -406,6 +430,14 @@ packages: url: "https://pub.dev" source: hosted version: "9.1.1" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" flutter_gen_core: dependency: transitive description: @@ -704,6 +736,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" package_config: dependency: transitive description: @@ -848,6 +888,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" shared_preferences: dependency: "direct main" description: @@ -920,6 +968,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -949,6 +1005,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" sqflite: dependency: "direct main" description: @@ -1069,6 +1133,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" vector_graphics: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c99cfc0..67bacde 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,6 +34,8 @@ dependencies: bloc: ^9.1.0 flutter_bloc: ^9.1.1 sqflite: ^2.4.2 + cached_network_image: ^3.4.1 + shimmer: ^3.0.0 dev_dependencies: flutter_test: