1503 lines
52 KiB
Dart
1503 lines
52 KiB
Dart
import 'dart:async';
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:carousel_slider/carousel_slider.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:geolocator/geolocator.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:latlong2/latlong.dart';
|
|
import 'package:niogu_ecommerce_v1/core/constant/app_asset.dart';
|
|
import 'package:niogu_ecommerce_v1/core/constant/app_color.dart';
|
|
import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart';
|
|
import 'package:niogu_ecommerce_v1/core/enums/campaign_type.dart';
|
|
import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart';
|
|
import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart';
|
|
import 'package:niogu_ecommerce_v1/core/router/app_route.dart';
|
|
import 'package:niogu_ecommerce_v1/core/system/system_setting.dart';
|
|
import 'package:niogu_ecommerce_v1/core/utils/currency_format.dart';
|
|
import 'package:niogu_ecommerce_v1/core/utils/extenstion_format.dart';
|
|
import 'package:niogu_ecommerce_v1/core/utils/log_message.dart';
|
|
import 'package:niogu_ecommerce_v1/core/utils/time_zone.dart';
|
|
import 'package:niogu_ecommerce_v1/core/widgets/custom_empty_screen.dart';
|
|
import 'package:niogu_ecommerce_v1/core/widgets/custom_snackbar.dart';
|
|
import 'package:niogu_ecommerce_v1/features/cart/presentation/providers/cart_provider.dart';
|
|
import 'package:niogu_ecommerce_v1/features/favorite/domain/entities/favorite.dart';
|
|
import 'package:niogu_ecommerce_v1/features/favorite/presentation/providers/favorite_provider.dart';
|
|
import 'package:niogu_ecommerce_v1/features/home/domain/entities/home.dart';
|
|
import 'package:niogu_ecommerce_v1/features/home/presentation/providers/home_provider.dart';
|
|
import 'package:niogu_ecommerce_v1/features/order/presentation/providers/order_provider.dart';
|
|
import 'package:niogu_ecommerce_v1/features/product/presentation/providers/product_provider.dart';
|
|
import 'package:shimmer/shimmer.dart';
|
|
import 'package:sizer/sizer.dart';
|
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
class HomeScreen extends ConsumerStatefulWidget {
|
|
const HomeScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<HomeScreen> createState() => _HomeScreenState();
|
|
}
|
|
|
|
class _HomeScreenState extends ConsumerState<HomeScreen> {
|
|
late ScrollController _recommendedController;
|
|
|
|
late Timer _recommendedTimer;
|
|
|
|
int _currentBannerIndex = 0;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_recommendedController = ScrollController();
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_instanceService();
|
|
_startChefAutoScroll();
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_recommendedTimer.cancel();
|
|
_recommendedController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _instanceService() async {
|
|
final echoService = ref.read(echoServiceProvider);
|
|
|
|
await echoService.init(
|
|
listener: (_, data) async {
|
|
final tables = List<String>.from(data['changed_tables']);
|
|
|
|
final currentRoute = GoRouter.of(
|
|
context,
|
|
).routerDelegate.currentConfiguration.last.matchedLocation;
|
|
|
|
final configureTables = ['tenants', 'tenant_payment_methods'];
|
|
|
|
final changedTables = [
|
|
'outlets',
|
|
'campaigns',
|
|
'categories',
|
|
'products',
|
|
'product_variants',
|
|
'outlet_inventories',
|
|
'orders',
|
|
];
|
|
|
|
if (tables.any((table) => configureTables.contains(table))) {
|
|
ref.read(configurationControllerProvider.notifier).refresh();
|
|
}
|
|
|
|
if (tables.any((table) => changedTables.contains(table))) {
|
|
if (currentRoute == '/home') {
|
|
ref.read(homeControllerProvider.notifier).refresh();
|
|
} else if (currentRoute == '/carts') {
|
|
ref.read(productBestSellerControllerProvider.notifier).refresh();
|
|
} else if (currentRoute == '/orders') {
|
|
ref.read(orderReportControllerProvider.notifier).refresh();
|
|
}
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
void _startChefAutoScroll() {
|
|
_recommendedTimer = Timer.periodic(const Duration(seconds: 3), (timer) {
|
|
if (_recommendedController.hasClients) {
|
|
final maxScroll = _recommendedController.position.maxScrollExtent;
|
|
final currentScroll = _recommendedController.position.pixels;
|
|
final delta = 70.w;
|
|
|
|
if (currentScroll >= maxScroll - 10) {
|
|
_recommendedController.jumpTo(0);
|
|
} else {
|
|
_recommendedController.animateTo(
|
|
currentScroll + delta,
|
|
duration: const Duration(milliseconds: 800),
|
|
curve: Curves.easeInOut,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> _launchWhatsApp() async {
|
|
final phoneNumber = ref.read(currentOutletPhoneProvider)!;
|
|
|
|
final Uri whatsappUri = Uri.parse(
|
|
"https://wa.me/${phoneNumber.normalizePhoneNumber()}",
|
|
);
|
|
|
|
try {
|
|
final bool launched = await launchUrl(
|
|
whatsappUri,
|
|
mode: LaunchMode.externalApplication,
|
|
);
|
|
|
|
if (!launched && mounted) {
|
|
CustomSnackbar.showError(context, "Tidak dapat membuka whatsApp");
|
|
}
|
|
} catch (e) {
|
|
LogMessage.log.e("Error launching whatsApp: $e");
|
|
if (mounted) {
|
|
CustomSnackbar.showError(
|
|
context,
|
|
"Terjadi kesalahan saat membuka whatsApp",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _fetchProductById(String id) async {
|
|
try {
|
|
final productDetail = await ref
|
|
.read(productRepositoryProvider)
|
|
.fetchProductById(id);
|
|
|
|
if (productDetail == null) {
|
|
CustomSnackbar.showError(context, "Produk tidak ditemukan");
|
|
ref.read(homeControllerProvider.notifier).refresh();
|
|
return;
|
|
}
|
|
|
|
context.pushNamed(AppRoute.productDetailScreen, extra: productDetail);
|
|
} on ServerException catch (e, st) {
|
|
LogMessage.log.i(e.toString(), error: e, stackTrace: st);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final configureState = ref.watch(configurationControllerProvider);
|
|
|
|
final homeState = ref.watch(homeControllerProvider);
|
|
|
|
final currentOutletId = ref.watch(currentOutletIdProvider);
|
|
|
|
final currentOutletPhone = ref.watch(currentOutletPhoneProvider);
|
|
|
|
final favoriteState = ref.watch(favoriteControllerProvider);
|
|
|
|
return SafeArea(
|
|
top: false,
|
|
bottom: true,
|
|
right: false,
|
|
left: false,
|
|
child: Scaffold(
|
|
backgroundColor: Colors.white,
|
|
body: Stack(
|
|
children: [
|
|
homeState.when(
|
|
data: (home) {
|
|
if (home == null) return _buildErrorState();
|
|
final currentOutlet = home.currentOutlet;
|
|
|
|
final campaigns = home.campaigns;
|
|
|
|
final categories = home.categories;
|
|
|
|
final otherOutlets = home.otherOutlets;
|
|
|
|
final recommendations = home.recommendations;
|
|
|
|
final allProducts = home.allProducts;
|
|
|
|
return RefreshIndicator(
|
|
onRefresh: () async {
|
|
await ref
|
|
.read(homeControllerProvider.notifier)
|
|
.refresh();
|
|
},
|
|
color: AppColor.primaryColor,
|
|
backgroundColor: Colors.white,
|
|
child: CustomScrollView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
slivers: [
|
|
_buildSliverAppBar(
|
|
location: currentOutlet.location,
|
|
name: currentOutlet.name,
|
|
),
|
|
|
|
_buildSliverSearch(categories),
|
|
|
|
SliverToBoxAdapter(
|
|
child: Column(
|
|
children: [
|
|
_buildCampaign(
|
|
campaigns: campaigns,
|
|
categories: categories,
|
|
),
|
|
|
|
_buildCategory(categories: categories),
|
|
|
|
_buildOutletSection(outlets: otherOutlets),
|
|
|
|
_buildRecommendation(
|
|
products: recommendations,
|
|
currentOutletId: currentOutletId!,
|
|
favorites: favoriteState,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
_buildAllProducts(
|
|
products: allProducts,
|
|
currentOutletId: currentOutletId,
|
|
favorites: favoriteState,
|
|
),
|
|
|
|
SliverToBoxAdapter(child: SizedBox(height: 15.h)),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
error: (error, stackTrace) => _buildErrorState(),
|
|
loading: () => _buildHomeLoading(),
|
|
),
|
|
|
|
if (configureState.isRefreshing || homeState.isRefreshing)
|
|
Container(
|
|
color: Colors.black.withOpacity(0.5),
|
|
child: Center(
|
|
child: const CircularProgressIndicator(
|
|
color: AppColor.primaryColor,
|
|
backgroundColor: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
|
|
if (configureState.hasValue && homeState.hasValue)
|
|
AnimatedPositioned(
|
|
duration: const Duration(milliseconds: 500),
|
|
curve: Curves.easeOutBack,
|
|
bottom: -1.85.h,
|
|
left: 0,
|
|
right: 0,
|
|
child: _buildClosedBanner(
|
|
onlineOpenTime: configureState.value!.onlineOpenTime!,
|
|
onlineCloseTime: configureState.value!.onlineCloseTime!,
|
|
isCloseService: configureState.value!.isCloseService!,
|
|
isActive: homeState.value!.currentOutlet.isActive,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
floatingActionButton: currentOutletPhone == null
|
|
? null
|
|
: Padding(
|
|
padding: EdgeInsets.only(bottom: 2.h),
|
|
child: FloatingActionButton(
|
|
onPressed: _launchWhatsApp,
|
|
backgroundColor: Colors.transparent,
|
|
shape: const CircleBorder(),
|
|
elevation: 0,
|
|
child: Image.asset(
|
|
AppAsset.WHATSAPP,
|
|
width: 12.5.w,
|
|
height: 12.5.w,
|
|
fit: BoxFit.cover,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildSliverAppBar({String? location, required String name}) {
|
|
return SliverAppBar(
|
|
pinned: true,
|
|
backgroundColor: AppColor.primaryColor,
|
|
expandedHeight: kIsWeb ? 12.h : 8.5.h,
|
|
shadowColor: Colors.transparent,
|
|
surfaceTintColor: Colors.transparent,
|
|
flexibleSpace: FlexibleSpaceBar(
|
|
background: Padding(
|
|
padding: EdgeInsets.fromLTRB(4.w, 5.h, 4.w, 0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
if (location != null)
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
"Lokasi Outlet",
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: AppFontSize.small.sp,
|
|
),
|
|
),
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.location_on,
|
|
color: Colors.white,
|
|
size: 5.w,
|
|
),
|
|
Expanded(
|
|
child: Text(
|
|
location,
|
|
overflow: TextOverflow.ellipsis,
|
|
maxLines: 1,
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: AppFontSize.small.sp,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
SizedBox(width: 8.w),
|
|
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
Text(
|
|
"Outlet Saat Ini",
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: AppFontSize.small.sp,
|
|
),
|
|
),
|
|
Text(
|
|
name,
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: AppFontSize.small.sp,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSliverSearch(List<CategoryItem> categories) {
|
|
return SliverPersistentHeader(
|
|
pinned: true,
|
|
delegate: _SliverSearchDelegate(
|
|
child: Center(
|
|
child: Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
Container(height: 6.h, color: AppColor.primaryColor),
|
|
Positioned(
|
|
top: 1.h,
|
|
left: 4.w,
|
|
right: 4.w,
|
|
child: Container(
|
|
height: 6.5.h,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: TextField(
|
|
onTap: () => context.pushNamed(
|
|
AppRoute.searchScreen,
|
|
extra: categories,
|
|
),
|
|
style: TextStyle(fontSize: AppFontSize.small.sp),
|
|
textAlignVertical: TextAlignVertical.center,
|
|
decoration: InputDecoration(
|
|
hintText: "Cari produk",
|
|
hintStyle: TextStyle(fontSize: AppFontSize.small.sp),
|
|
prefixIcon: Icon(Icons.search, size: 3.5.w),
|
|
border: InputBorder.none,
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
readOnly: true,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCampaign({
|
|
required List<CampaignByOutlet> campaigns,
|
|
required List<CategoryItem> categories,
|
|
}) {
|
|
if (campaigns.isEmpty) {
|
|
campaigns.add(CampaignByOutlet(image: AppAsset.BANNER_MOCK_2));
|
|
}
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: EdgeInsets.fromLTRB(4.w, 4.h, 4.w, 1.h),
|
|
child: Text(
|
|
"Informasi Hari Ini",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: AppFontSize.small.sp,
|
|
color: Color(0xFF102851),
|
|
),
|
|
),
|
|
),
|
|
CarouselSlider(
|
|
options: CarouselOptions(
|
|
height: 20.h,
|
|
viewportFraction: 0.92,
|
|
enlargeCenterPage: false,
|
|
autoPlay: true,
|
|
onPageChanged: (index, reason) {
|
|
setState(() {
|
|
_currentBannerIndex = index;
|
|
});
|
|
},
|
|
),
|
|
items: campaigns.map((banner) {
|
|
return GestureDetector(
|
|
onTap: () async {
|
|
final campaignType = banner.campaignType;
|
|
|
|
if (campaignType != null) {
|
|
switch (campaignType) {
|
|
case CampaignType.product:
|
|
await _fetchProductById(banner.actionRefId!);
|
|
break;
|
|
case CampaignType.category:
|
|
final category = categories.firstWhere(
|
|
(category) => category.id == banner.actionRefId,
|
|
);
|
|
context.pushNamed(
|
|
AppRoute.productCategoryScreen,
|
|
extra: category,
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
child: CachedNetworkImage(
|
|
imageUrl: banner.image ?? 'error',
|
|
imageBuilder: (context, imageProvider) {
|
|
return Container(
|
|
margin: EdgeInsets.symmetric(horizontal: 1.w),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade100,
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
image: DecorationImage(
|
|
image: imageProvider,
|
|
fit: BoxFit.cover,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
placeholder: (context, url) {
|
|
return Shimmer.fromColors(
|
|
baseColor: Colors.grey.shade300,
|
|
highlightColor: Colors.grey.shade100,
|
|
child: Container(
|
|
margin: EdgeInsets.symmetric(horizontal: 1.w),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
errorWidget: (context, url, error) {
|
|
return Container(
|
|
margin: EdgeInsets.symmetric(horizontal: 1.w),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade100,
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
image: DecorationImage(
|
|
image: AssetImage(AppAsset.BANNER_MOCK_2),
|
|
fit: BoxFit.cover,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
|
|
SizedBox(height: 2.h),
|
|
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: campaigns.asMap().entries.map((entry) {
|
|
final index = entry.key;
|
|
return Container(
|
|
width: _currentBannerIndex == index ? 15.0 : 6.0,
|
|
height: 6.0,
|
|
margin: const EdgeInsets.symmetric(horizontal: 3.0),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(1.w),
|
|
color: _currentBannerIndex == index
|
|
? AppColor.primaryColor
|
|
: Colors.grey.shade300,
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildCategory({required List<CategoryItem> categories}) {
|
|
return Column(
|
|
children: [
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 2.h),
|
|
child: Text(
|
|
"Kategori Produk",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: AppFontSize.small.sp,
|
|
color: Color(0xFF102851),
|
|
),
|
|
),
|
|
),
|
|
|
|
if (categories.isEmpty)
|
|
CustomEmptyScreen(
|
|
icon: Icons.grid_off_outlined,
|
|
title: "Kategori Belum Tersedia",
|
|
subtitle: "Nantikan pilihan kategori menarik segera",
|
|
height: 18.h,
|
|
)
|
|
else
|
|
SizedBox(
|
|
height: 14.h,
|
|
child: ListView.builder(
|
|
scrollDirection: Axis.horizontal,
|
|
padding: EdgeInsets.symmetric(horizontal: 4.w),
|
|
itemCount: categories.length,
|
|
itemBuilder: (context, index) {
|
|
final category = categories[index];
|
|
return GestureDetector(
|
|
onTap: () {
|
|
context.pushNamed(
|
|
AppRoute.productCategoryScreen,
|
|
extra: category,
|
|
);
|
|
},
|
|
child: Padding(
|
|
padding: EdgeInsets.only(right: 5.w),
|
|
child: Column(
|
|
children: [
|
|
CachedNetworkImage(
|
|
imageUrl: category.image ?? 'error',
|
|
imageBuilder: (context, imageProvider) {
|
|
return Container(
|
|
height: 18.w,
|
|
width: 18.w,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
border: BoxBorder.all(
|
|
color: Colors.grey.shade300,
|
|
),
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
image: DecorationImage(
|
|
image: imageProvider,
|
|
fit: BoxFit.cover,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 10,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
placeholder: (context, url) {
|
|
return Shimmer.fromColors(
|
|
baseColor: Colors.grey.shade300,
|
|
highlightColor: Colors.grey.shade100,
|
|
child: Container(
|
|
height: 18.w,
|
|
width: 18.w,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
errorWidget: (context, url, error) {
|
|
return Container(
|
|
height: 18.w,
|
|
width: 18.w,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
border: BoxBorder.all(
|
|
color: Colors.grey.shade300,
|
|
),
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 10,
|
|
),
|
|
],
|
|
),
|
|
child: Icon(
|
|
Icons.image_outlined,
|
|
size: 5.w,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
|
|
SizedBox(height: 1.h),
|
|
Text(
|
|
category.name,
|
|
style: TextStyle(
|
|
fontSize: (AppFontSize.small - 2).sp,
|
|
color: Color(0xFF102851),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildOutletSection({required List<OtherOutlet> outlets}) {
|
|
if (outlets.isEmpty) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: EdgeInsets.all(4.w),
|
|
child: Text(
|
|
"Kunjungi Outlet Lain",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: AppFontSize.small.sp,
|
|
),
|
|
),
|
|
),
|
|
CustomEmptyScreen(
|
|
icon: Icons.storefront_outlined,
|
|
title: "Hanya 1 Outlet Tersedia",
|
|
subtitle: "Belum ada outlet lain di wilayah ini",
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: EdgeInsets.all(4.w),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
"Kunjungi Outlet Lain",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: AppFontSize.small.sp,
|
|
),
|
|
),
|
|
|
|
GestureDetector(
|
|
onTap: () async {
|
|
LocationPermission permission =
|
|
await Geolocator.checkPermission();
|
|
if (permission == LocationPermission.denied) {
|
|
permission = await Geolocator.requestPermission();
|
|
}
|
|
|
|
Position position = await Geolocator.getCurrentPosition();
|
|
|
|
final userLocation = LatLng(
|
|
position.latitude,
|
|
position.longitude,
|
|
);
|
|
|
|
final outlets = await ref
|
|
.read(homeRepositoryProvider)
|
|
.fetchOutlets();
|
|
|
|
if (outlets.isEmpty) return;
|
|
|
|
context.pushNamed(
|
|
AppRoute.outletMapScreen,
|
|
extra: {'user_location': userLocation, 'outlets': outlets},
|
|
);
|
|
},
|
|
child: Container(
|
|
padding: EdgeInsets.all(1.5.w),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(1.w),
|
|
border: BoxBorder.all(color: AppColor.primaryColor),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
Icons.location_on_outlined,
|
|
size: 3.5.w,
|
|
color: AppColor.primaryColor,
|
|
),
|
|
|
|
SizedBox(width: 2.w),
|
|
|
|
Text(
|
|
"Lihat Dipeta",
|
|
style: TextStyle(
|
|
color: AppColor.primaryColor,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
SizedBox(
|
|
height: 20.h,
|
|
child: ListView.builder(
|
|
scrollDirection: Axis.horizontal,
|
|
padding: EdgeInsets.symmetric(horizontal: 4.w),
|
|
itemCount: outlets.length,
|
|
itemBuilder: (context, index) {
|
|
final outlet = outlets[index];
|
|
return GestureDetector(
|
|
onTap: () async {
|
|
await SystemSetting.switchOutlet(
|
|
outletId: outlet.id,
|
|
outletName: outlet.name,
|
|
outletPhone: outlet.phoneNumber,
|
|
outletLocation: outlet.location,
|
|
outletCoordinate: outlet.coordinate,
|
|
);
|
|
|
|
ref.read(currentOutletIdProvider.notifier).state = outlet.id;
|
|
|
|
ref.read(currentOutletNameProvider.notifier).state =
|
|
outlet.name;
|
|
|
|
ref.read(currentOutletPhoneProvider.notifier).state =
|
|
outlet.phoneNumber;
|
|
|
|
ref.read(currentOutletLocationProvider.notifier).state =
|
|
outlet.location;
|
|
|
|
ref.read(currentOutletCoordinateProvider.notifier).state =
|
|
outlet.coordinate;
|
|
|
|
await ref.read(homeControllerProvider.notifier).refresh();
|
|
|
|
CustomSnackbar.showSuccess(
|
|
context,
|
|
"Berhasil mengunjungi ${outlet.name}",
|
|
);
|
|
},
|
|
child: CachedNetworkImage(
|
|
imageUrl: outlet.image ?? 'error',
|
|
imageBuilder: (context, imageProvider) {
|
|
return Stack(
|
|
children: [
|
|
Container(
|
|
width: 75.w,
|
|
margin: EdgeInsets.only(right: 4.w),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
image: DecorationImage(
|
|
image: imageProvider,
|
|
fit: BoxFit.cover,
|
|
),
|
|
),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [Colors.transparent, Colors.black87],
|
|
),
|
|
),
|
|
padding: EdgeInsets.all(4.w),
|
|
child: _buildBranchInformation(
|
|
name: outlet.name,
|
|
location: outlet.location,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
placeholder: (context, url) {
|
|
return Shimmer.fromColors(
|
|
baseColor: Colors.grey.shade300,
|
|
highlightColor: Colors.grey.shade100,
|
|
child: Container(
|
|
width: 75.w,
|
|
margin: EdgeInsets.only(right: 4.w),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
errorWidget: (context, url, error) {
|
|
return Container(
|
|
width: 75.w,
|
|
margin: EdgeInsets.only(right: 4.w),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
image: DecorationImage(
|
|
image: AssetImage(AppAsset.OUTLET_MOCK),
|
|
fit: BoxFit.cover,
|
|
),
|
|
),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [Colors.transparent, Colors.black87],
|
|
),
|
|
),
|
|
padding: EdgeInsets.all(4.w),
|
|
child: _buildBranchInformation(
|
|
name: outlet.name,
|
|
location: outlet.location,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildBranchInformation({required String name, String? location}) {
|
|
return Column(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
name,
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: AppFontSize.small.sp,
|
|
),
|
|
),
|
|
if (location != null)
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.location_on, size: 4.w, color: Colors.white70),
|
|
|
|
SizedBox(width: 0.75.w),
|
|
Expanded(
|
|
child: Text(
|
|
location,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
color: Colors.white70,
|
|
fontSize: (AppFontSize.small - 2).sp,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
/**
|
|
SizedBox(width: 2.5.w),
|
|
|
|
Row(
|
|
children: [
|
|
Icon(Icons.straighten, size: 4.w, color: Colors.white70),
|
|
|
|
SizedBox(width: 0.75.w),
|
|
|
|
Text(
|
|
"4km",
|
|
style: TextStyle(
|
|
color: Colors.white70,
|
|
fontSize: (AppFontSize.small - 2).sp,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
*/
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildRecommendation({
|
|
required List<ProductItem> products,
|
|
required String currentOutletId,
|
|
required Map<String, SelectedFavorite> favorites,
|
|
}) {
|
|
if (products.isEmpty) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: EdgeInsets.all(4.w),
|
|
child: Text(
|
|
"Rekomendasi Produk",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: AppFontSize.small.sp,
|
|
),
|
|
),
|
|
),
|
|
CustomEmptyScreen(
|
|
icon: Icons.auto_awesome_outlined,
|
|
title: "Belum Ada Rekomendasi",
|
|
subtitle: "Produk pilihan akan muncul di sini nanti",
|
|
),
|
|
],
|
|
);
|
|
}
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: EdgeInsets.all(4.w),
|
|
child: Text(
|
|
"Rekomendasi Produk",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: AppFontSize.small.sp,
|
|
color: Color(0xFF102851),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(
|
|
height: 32.h,
|
|
child: ListView.builder(
|
|
controller: _recommendedController,
|
|
scrollDirection: Axis.horizontal,
|
|
padding: EdgeInsets.symmetric(horizontal: 4.w),
|
|
itemCount: products.length,
|
|
itemBuilder: (context, index) {
|
|
final product = products[index];
|
|
|
|
final isFavorite = favorites.containsKey(
|
|
"$currentOutletId-${product.id}",
|
|
);
|
|
return Container(
|
|
width: 70.w,
|
|
margin: EdgeInsets.only(right: 4.w),
|
|
child: _buildProductCard(product, currentOutletId, isFavorite),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildAllProducts({
|
|
required List<ProductItem> products,
|
|
required String currentOutletId,
|
|
required Map<String, SelectedFavorite> favorites,
|
|
}) {
|
|
if (products.isEmpty) {
|
|
return SliverToBoxAdapter(
|
|
child: CustomEmptyScreen(
|
|
icon: Icons.inventory_outlined,
|
|
title: "Katalog Kosong",
|
|
subtitle: "Nantikan produk-produk terbaru kami",
|
|
height: 40.h,
|
|
),
|
|
);
|
|
}
|
|
return SliverMainAxisGroup(
|
|
slivers: [
|
|
SliverPersistentHeader(
|
|
pinned: true,
|
|
delegate: _SliverSearchDelegate(
|
|
child: Container(
|
|
color: Colors.white,
|
|
padding: EdgeInsets.all(4.w),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
"Semua Produk",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: AppFontSize.small.sp,
|
|
),
|
|
),
|
|
Icon(Icons.filter_list),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SliverPadding(
|
|
padding: EdgeInsets.symmetric(horizontal: 4.w),
|
|
sliver: SliverGrid(
|
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
crossAxisCount: 2,
|
|
mainAxisSpacing: 10,
|
|
crossAxisSpacing: 10,
|
|
childAspectRatio: 0.75,
|
|
),
|
|
delegate: SliverChildBuilderDelegate((context, index) {
|
|
final product = products[index];
|
|
|
|
final isFavorite = favorites.containsKey(
|
|
"$currentOutletId-${product.id}",
|
|
);
|
|
|
|
return _buildProductCard(product, currentOutletId, isFavorite);
|
|
}, childCount: products.length),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildProductCard(
|
|
ProductItem product,
|
|
String currentOutletId,
|
|
bool isFavorite,
|
|
) {
|
|
return GestureDetector(
|
|
onTap: () async => await _fetchProductById(product.id),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.04),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
flex: 3,
|
|
child: CachedNetworkImage(
|
|
imageUrl: product.image ?? 'error',
|
|
imageBuilder: (context, imageProvider) {
|
|
return Container(
|
|
width: double.infinity,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade100,
|
|
borderRadius: BorderRadius.vertical(
|
|
top: Radius.circular(2.5.w),
|
|
),
|
|
image: DecorationImage(
|
|
image: imageProvider,
|
|
fit: BoxFit.cover,
|
|
),
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
Positioned(
|
|
top: 1.w,
|
|
right: 1.w,
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
final currentOutletId = ref.read(
|
|
currentOutletIdProvider,
|
|
);
|
|
ref
|
|
.read(favoriteControllerProvider.notifier)
|
|
.toggle(product, currentOutletId!);
|
|
},
|
|
child: CircleAvatar(
|
|
radius: 14,
|
|
backgroundColor: Colors.white.withOpacity(0.9),
|
|
child: Icon(
|
|
isFavorite
|
|
? Icons.favorite
|
|
: Icons.favorite_outline,
|
|
size: 5.w,
|
|
color: isFavorite ? Colors.red : Colors.grey,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
placeholder: (context, url) {
|
|
return Shimmer.fromColors(
|
|
baseColor: Colors.grey.shade300,
|
|
highlightColor: Colors.grey.shade100,
|
|
child: Container(
|
|
width: double.infinity,
|
|
margin: EdgeInsets.only(right: 4.w),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.vertical(
|
|
top: Radius.circular(2.5.w),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
errorWidget: (context, url, error) {
|
|
return Container(
|
|
width: double.infinity,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade100,
|
|
borderRadius: BorderRadius.vertical(
|
|
top: Radius.circular(2.5.w),
|
|
),
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
Center(
|
|
child: Icon(
|
|
Icons.image_outlined,
|
|
color: Colors.grey.shade300,
|
|
size: 10.w,
|
|
),
|
|
),
|
|
Positioned(
|
|
top: 1.w,
|
|
right: 1.w,
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
ref
|
|
.read(favoriteControllerProvider.notifier)
|
|
.toggle(product, currentOutletId);
|
|
},
|
|
child: CircleAvatar(
|
|
radius: 14,
|
|
backgroundColor: Colors.white.withOpacity(0.9),
|
|
child: Icon(
|
|
isFavorite
|
|
? Icons.favorite
|
|
: Icons.favorite_outline,
|
|
size: 5.w,
|
|
color: isFavorite ? Colors.red : Colors.grey,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
|
|
Expanded(
|
|
flex: 2,
|
|
child: Padding(
|
|
padding: EdgeInsets.all(3.w),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
product.name,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
color: const Color(0xFF102851),
|
|
),
|
|
),
|
|
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
"${product.totalSold.toCompact} Terjual",
|
|
style: TextStyle(
|
|
color: Colors.black87,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
),
|
|
),
|
|
Row(
|
|
children: [
|
|
Icon(Icons.star, color: Colors.orange, size: 3.5.w),
|
|
SizedBox(width: 1.w),
|
|
Text(
|
|
product.averageRating.toStringAsFixed(1),
|
|
style: TextStyle(
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
CurrencyFormat.formatToIdr(product.sellingPrice, 0),
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: AppFontSize.small.sp,
|
|
color: AppColor.primaryColor,
|
|
),
|
|
),
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.thumb_up,
|
|
color: Colors.red,
|
|
size: 3.5.w,
|
|
),
|
|
SizedBox(width: 1.w),
|
|
Text(
|
|
product.likes.toString(),
|
|
style: TextStyle(
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
color: Colors.red,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildErrorState() {
|
|
return RefreshIndicator(
|
|
onRefresh: () => ref.read(homeControllerProvider.notifier).refresh(),
|
|
color: AppColor.primaryColor,
|
|
backgroundColor: Colors.white,
|
|
child: SingleChildScrollView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
child: SizedBox(
|
|
height: 80.h,
|
|
child: CustomEmptyScreen(
|
|
icon: Icons.cloud_off_outlined,
|
|
title: "Terjadi Kesalahan Koneksi",
|
|
subtitle: "Tarik ke bawah untuk mencoba lagi",
|
|
height: 40.h,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHomeLoading() {
|
|
return Shimmer.fromColors(
|
|
baseColor: Colors.grey.shade300,
|
|
highlightColor: Colors.grey.shade100,
|
|
child: SingleChildScrollView(
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(height: 8.5.h, color: Colors.white),
|
|
Padding(
|
|
padding: EdgeInsets.all(4.w),
|
|
child: Container(
|
|
height: 6.5.h,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
),
|
|
),
|
|
),
|
|
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 4.w),
|
|
child: Container(
|
|
height: 20.h,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
),
|
|
),
|
|
),
|
|
|
|
SizedBox(height: 4.h),
|
|
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 4.w),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: List.generate(
|
|
4,
|
|
(index) => Column(
|
|
children: [
|
|
Container(
|
|
width: 15.w,
|
|
height: 15.w,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
),
|
|
),
|
|
SizedBox(height: 1.h),
|
|
Container(
|
|
width: 12.w,
|
|
height: 1.5.h,
|
|
color: Colors.white,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
SizedBox(height: 4.h),
|
|
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 4.w),
|
|
child: Container(
|
|
height: 25.h,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildClosedBanner({
|
|
required String onlineOpenTime,
|
|
required String onlineCloseTime,
|
|
required bool isCloseService,
|
|
required bool isActive,
|
|
}) {
|
|
final timeFormat = DateFormat('HH.mm');
|
|
|
|
final open = timeFormat.parse(onlineOpenTime).hour;
|
|
|
|
final close = timeFormat.parse(onlineCloseTime).hour;
|
|
|
|
final now = DateTime.now().hour;
|
|
|
|
if (!isCloseService && (now >= open && now < close) && isActive)
|
|
return const SizedBox();
|
|
|
|
final timeZone = TimeZone.getCurrentTimeZone();
|
|
|
|
final operatingHour =
|
|
"$onlineOpenTime $timeZone - $onlineCloseTime $timeZone";
|
|
|
|
return Container(
|
|
margin: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h),
|
|
padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.5.h),
|
|
decoration: BoxDecoration(
|
|
color: Colors.red.withOpacity(0.85),
|
|
borderRadius: BorderRadius.circular(3.w),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, -2),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.info_outline, color: Colors.white, size: 5.w),
|
|
SizedBox(width: 3.w),
|
|
Expanded(
|
|
child: Text(
|
|
!isActive
|
|
? "Outlet ini tutup sementara, kunjungi outlet lain kami"
|
|
: isCloseService
|
|
? "Toko sedang tutup sementara"
|
|
: "Toko sedang tutup. Jam layanan pembelian online: $operatingHour",
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: AppFontSize.small.sp,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SliverSearchDelegate extends SliverPersistentHeaderDelegate {
|
|
final Widget child;
|
|
_SliverSearchDelegate({required this.child});
|
|
|
|
@override
|
|
Widget build(BuildContext context, shrinkOffset, bool overlapsContent) =>
|
|
child;
|
|
|
|
@override
|
|
double get maxExtent => 6.h;
|
|
@override
|
|
double get minExtent => 6.h;
|
|
|
|
@override
|
|
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
|
|
false;
|
|
}
|