Commit efb4662c authored by DatHV's avatar DatHV
Browse files

update campaign 7day

parent 4c376d38
...@@ -65,7 +65,7 @@ class VoucherListItem extends StatelessWidget { ...@@ -65,7 +65,7 @@ class VoucherListItem extends StatelessWidget {
if (!product.inStock) if (!product.inStock)
Positioned.fill( Positioned.fill(
child: Container( child: Container(
color: Colors.black.withOpacity(0.5), color: Colors.black.withOpacity(0.3),
alignment: Alignment.center, alignment: Alignment.center,
child: const Text( child: const Text(
'Tạm hết', 'Tạm hết',
......
...@@ -19,6 +19,7 @@ class _VoucherListScreenState extends State<VoucherListScreen> { ...@@ -19,6 +19,7 @@ class _VoucherListScreenState extends State<VoucherListScreen> {
late final Map<String, dynamic> args; late final Map<String, dynamic> args;
late final bool enableSearch; late final bool enableSearch;
late final bool isHotProduct; late final bool isHotProduct;
late final bool isFavorite;
late final VoucherListViewModel _viewModel; late final VoucherListViewModel _viewModel;
@override @override
...@@ -27,12 +28,13 @@ class _VoucherListScreenState extends State<VoucherListScreen> { ...@@ -27,12 +28,13 @@ class _VoucherListScreenState extends State<VoucherListScreen> {
args = Get.arguments ?? {}; args = Get.arguments ?? {};
enableSearch = args['enableSearch'] ?? false; enableSearch = args['enableSearch'] ?? false;
isHotProduct = args['isHotProduct'] ?? false; isHotProduct = args['isHotProduct'] ?? false;
_viewModel = Get.put(VoucherListViewModel(isHotProduct: isHotProduct)); isFavorite = args['favorite'] ?? false;
_viewModel = Get.put(VoucherListViewModel(isHotProduct: isHotProduct, isFavorite: isFavorite));
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final String title = isHotProduct ? 'Săn ưu đãi' : 'Tất cả ưu đãi'; final String title = isFavorite ? 'Yêu thích' : (isHotProduct ? 'Săn ưu đãi' : 'Tất cả ưu đãi');
return Scaffold( return Scaffold(
appBar: appBar:
enableSearch enableSearch
...@@ -69,21 +71,22 @@ class _VoucherListScreenState extends State<VoucherListScreen> { ...@@ -69,21 +71,22 @@ class _VoucherListScreenState extends State<VoucherListScreen> {
); );
} }
return RefreshIndicator( return RefreshIndicator(
onRefresh: () => _viewModel.getProducts(reset: true), onRefresh: () => _viewModel.loadData(reset: true),
child: ListView.builder( child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
itemCount: _viewModel.products.length + (_viewModel.hasMore ? 1 : 0), itemCount: _viewModel.products.length + (_viewModel.hasMore ? 1 : 0),
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index >= _viewModel.products.length) { if (index >= _viewModel.products.length) {
_viewModel.getProducts(reset: false); _viewModel.loadData(reset: false);
return const Center( return const Center(
child: Padding(padding: EdgeInsets.all(16), child: CircularProgressIndicator()), child: Padding(padding: EdgeInsets.all(16), child: CircularProgressIndicator()),
); );
} }
final product = _viewModel.products[index]; final product = _viewModel.products[index];
return GestureDetector( return GestureDetector(
onTap: () { onTap: () async {
Get.toNamed(voucherDetailScreen, arguments: {"productId": product.id}); await Get.toNamed(voucherDetailScreen, arguments: {"productId": product.id});
_viewModel.loadData(reset: true);
}, },
child: VoucherListItem(product: product), child: VoucherListItem(product: product),
); );
......
...@@ -6,8 +6,8 @@ import '../models/product_model.dart'; ...@@ -6,8 +6,8 @@ import '../models/product_model.dart';
import '../models/product_type.dart'; import '../models/product_type.dart';
class VoucherListViewModel extends RestfulApiViewModel { class VoucherListViewModel extends RestfulApiViewModel {
VoucherListViewModel({required this.isHotProduct}); VoucherListViewModel({required this.isHotProduct, this.isFavorite = false});
final bool isFavorite;
final bool isHotProduct; final bool isHotProduct;
Timer? _debounce; Timer? _debounce;
var products = <ProductModel>[].obs; var products = <ProductModel>[].obs;
...@@ -24,7 +24,7 @@ class VoucherListViewModel extends RestfulApiViewModel { ...@@ -24,7 +24,7 @@ class VoucherListViewModel extends RestfulApiViewModel {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
getProducts(reset: true); loadData(reset: true);
} }
@override @override
...@@ -38,11 +38,47 @@ class VoucherListViewModel extends RestfulApiViewModel { ...@@ -38,11 +38,47 @@ class VoucherListViewModel extends RestfulApiViewModel {
_searchQuery = value; _searchQuery = value;
_debounce?.cancel(); _debounce?.cancel();
_debounce = Timer(const Duration(seconds: 1), () { _debounce = Timer(const Duration(seconds: 1), () {
getProducts(reset: true); loadData(reset: true);
}); });
} }
Future<void> getProducts({bool reset = false}) async { Future<void> loadData({bool reset = false}) async {
if (isFavorite) {
await _getFavoriteProducts(reset: reset);
} else {
await _getProducts(reset: reset);
}
}
Future<void> _getFavoriteProducts({bool reset = false}) async {
if (isLoading.value) return;
if (reset) {
_currentPage = 0;
_hasMore = true;
products.clear();
} else {
_currentPage = products.length;
}
if (!_hasMore) return;
final body = {"size": _pageSize, "index": _currentPage};
try {
isLoading.value = true;
isLoadMore.value = true;
final result = await client.productsCustomerLikes(body);
final fetchedData = result.data ?? [];
if (fetchedData.isEmpty || fetchedData.length < _pageSize) {
_hasMore = false;
}
products.addAll(fetchedData);
} catch (error) {
print("Error fetching products: $error");
} finally {
isLoading.value = false;
isLoadMore.value = false;
}
}
Future<void> _getProducts({bool reset = false}) async {
if (isLoading.value) return; if (isLoading.value) return;
if (reset) { if (reset) {
_currentPage = 0; _currentPage = 0;
......
...@@ -41,6 +41,7 @@ class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicSta ...@@ -41,6 +41,7 @@ class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicSta
}); });
return; return;
} }
showLoading();
_controller = _controller =
WebViewController() WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted) ..setJavaScriptMode(JavaScriptMode.unrestricted)
...@@ -48,12 +49,14 @@ class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicSta ...@@ -48,12 +49,14 @@ class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicSta
..setNavigationDelegate( ..setNavigationDelegate(
NavigationDelegate( NavigationDelegate(
onPageFinished: (_) async { onPageFinished: (_) async {
hideLoading();
final title = await _controller.getTitle(); final title = await _controller.getTitle();
setState(() { setState(() {
_dynamicTitle = title; _dynamicTitle = title;
}); });
}, },
onWebResourceError: (error) { onWebResourceError: (error) {
hideLoading();
if (error.description != 'about:blank') { if (error.description != 'about:blank') {
showAlertError(content: error.description); showAlertError(content: error.description);
} }
......
...@@ -6,9 +6,12 @@ import '../screen/affiliate/affiliate_tab_screen.dart'; ...@@ -6,9 +6,12 @@ import '../screen/affiliate/affiliate_tab_screen.dart';
import '../screen/affiliate_brand_detail/affiliate_brand_detail_screen.dart'; import '../screen/affiliate_brand_detail/affiliate_brand_detail_screen.dart';
import '../screen/affiliate_brand_detail/affiliate_brand_list_screen.dart'; import '../screen/affiliate_brand_detail/affiliate_brand_list_screen.dart';
import '../screen/affiliate_brand_detail/affiliate_category_grid_screen.dart'; import '../screen/affiliate_brand_detail/affiliate_category_grid_screen.dart';
import '../screen/campaign7day/campaign_7day_screen.dart';
import '../screen/contacts/contacts_list_screen.dart'; import '../screen/contacts/contacts_list_screen.dart';
import '../screen/daily_checkin/daily_checkin_screen.dart'; import '../screen/daily_checkin/daily_checkin_screen.dart';
import '../screen/data_network_service/data_network_service_screen.dart'; import '../screen/data_network_service/data_network_service_screen.dart';
import '../screen/electric_payment/electric_payment_history_screen.dart';
import '../screen/electric_payment/electric_payment_screen.dart';
import '../screen/game/game_cards/game_card_screen.dart'; import '../screen/game/game_cards/game_card_screen.dart';
import '../screen/game/game_tab_screen.dart'; import '../screen/game/game_tab_screen.dart';
import '../screen/history_point_cashback/history_point_cashback_screen.dart'; import '../screen/history_point_cashback/history_point_cashback_screen.dart';
...@@ -23,13 +26,17 @@ import '../screen/onboarding/onboarding_screen.dart'; ...@@ -23,13 +26,17 @@ import '../screen/onboarding/onboarding_screen.dart';
import '../screen/order_menu/order_menu_screen.dart'; import '../screen/order_menu/order_menu_screen.dart';
import '../screen/pageDetail/campaign_detail_screen.dart'; import '../screen/pageDetail/campaign_detail_screen.dart';
import '../screen/personal/personal_edit_screen.dart'; import '../screen/personal/personal_edit_screen.dart';
import '../screen/quiz_campaign/quiz_campaign_screen.dart';
import '../screen/register_campaign/register_form_input_screen.dart'; import '../screen/register_campaign/register_form_input_screen.dart';
import '../screen/setting/setting_screen.dart'; import '../screen/setting/setting_screen.dart';
import '../screen/splash/splash_screen.dart'; import '../screen/splash/splash_screen.dart';
import '../screen/support/support_screen.dart'; import '../screen/support/support_screen.dart';
import '../screen/topup/topup_screen.dart'; import '../screen/topup/topup_screen.dart';
import '../screen/traffic_service/traffic_service_detail_screen.dart';
import '../screen/traffic_service/traffic_service_screen.dart';
import '../screen/transaction/history/transaction_history_detail_screen.dart'; import '../screen/transaction/history/transaction_history_detail_screen.dart';
import '../screen/transaction/transaction_detail_screen.dart'; import '../screen/transaction/transaction_detail_screen.dart';
import '../screen/transaction/transactions_history_screen.dart';
import '../screen/voucher/detail/voucher_detail_screen.dart'; import '../screen/voucher/detail/voucher_detail_screen.dart';
import '../screen/voucher/my_voucher/my_product_list_widget.dart'; import '../screen/voucher/my_voucher/my_product_list_widget.dart';
import '../screen/voucher/voucher_list/voucher_list_screen.dart'; import '../screen/voucher/voucher_list/voucher_list_screen.dart';
...@@ -73,6 +80,13 @@ const affiliateCategoryGridScreen = '/affiliateCategoryGridScreen'; ...@@ -73,6 +80,13 @@ const affiliateCategoryGridScreen = '/affiliateCategoryGridScreen';
const inviteFriendCampaignScreen = '/inviteFriendCampaignScreen'; const inviteFriendCampaignScreen = '/inviteFriendCampaignScreen';
const contactsListScreen = '/contactsListScreen'; const contactsListScreen = '/contactsListScreen';
const dailyCheckInScreen = '/dailyCheckInScreen'; const dailyCheckInScreen = '/dailyCheckInScreen';
const transactionHistoryScreen = '/transactionHistoryScreen';
const electricPaymentScreen = '/electricPaymentScreen';
const electricPaymentHistoryScreen = '/electricPaymentHistoryScreen';
const trafficServiceScreen = '/trafficServiceScreen';
const trafficServiceDetailScreen = '/trafficServiceDetailScreen';
const campaignSevenDayScreen = '/campaignSevenDayScreen';
const surveyQuestionScreen = '/surveyQuestionScreen';
class RouterPage { class RouterPage {
static List<GetPage> pages() { static List<GetPage> pages() {
...@@ -86,7 +100,13 @@ class RouterPage { ...@@ -86,7 +100,13 @@ class RouterPage {
GetPage(name: splashScreen, page: () => SplashScreen()), GetPage(name: splashScreen, page: () => SplashScreen()),
GetPage(name: onboardingScreen, page: () => OnboardingScreen()), GetPage(name: onboardingScreen, page: () => OnboardingScreen()),
GetPage(name: loginScreen, page: () => LoginScreen()), GetPage(name: loginScreen, page: () => LoginScreen()),
GetPage(name: mainScreen, page: () => MainTabScreen(), customTransition: NoSwipeBackTransition()), GetPage(
name: mainScreen,
page: () => MainTabScreen(),
participatesInRootNavigator: true,
fullscreenDialog: true,
binding: BindingsBuilder(() {}),
),
GetPage(name: settingScreen, page: () => SettingScreen()), GetPage(name: settingScreen, page: () => SettingScreen()),
GetPage(name: vouchersScreen, page: () => VoucherListScreen()), GetPage(name: vouchersScreen, page: () => VoucherListScreen()),
GetPage(name: voucherDetailScreen, page: () => VoucherDetailScreen()), GetPage(name: voucherDetailScreen, page: () => VoucherDetailScreen()),
...@@ -119,20 +139,13 @@ class RouterPage { ...@@ -119,20 +139,13 @@ class RouterPage {
GetPage(name: inviteFriendCampaignScreen, page: () => InviteFriendCampaignScreen()), GetPage(name: inviteFriendCampaignScreen, page: () => InviteFriendCampaignScreen()),
GetPage(name: contactsListScreen, page: () => ContactsListScreen()), GetPage(name: contactsListScreen, page: () => ContactsListScreen()),
GetPage(name: dailyCheckInScreen, page: () => DailyCheckInScreen()), GetPage(name: dailyCheckInScreen, page: () => DailyCheckInScreen()),
GetPage(name: transactionHistoryScreen, page: () => TransactionHistoryScreen()),
GetPage(name: electricPaymentScreen, page: () => ElectricPaymentScreen()),
GetPage(name: electricPaymentHistoryScreen, page: () => ElectricPaymentHistoryScreen()),
GetPage(name: trafficServiceScreen, page: () => TrafficServiceScreen()),
GetPage(name: trafficServiceDetailScreen, page: () => TrafficServiceDetailScreen()),
GetPage(name: campaignSevenDayScreen, page: () => Campaign7DayScreen()),
GetPage(name: surveyQuestionScreen, page: () => SurveyQuestionScreen()),
]; ];
} }
}
class NoSwipeBackTransition extends CustomTransition {
@override
Widget buildTransition(
BuildContext context,
Curve? curve,
Alignment? alignment,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return child;
}
} }
\ No newline at end of file
...@@ -24,7 +24,7 @@ class ButtonConfigModel { ...@@ -24,7 +24,7 @@ class ButtonConfigModel {
final bgColor = color?.toColor() ?? Colors.white; final bgColor = color?.toColor() ?? Colors.white;
return AlertButton( return AlertButton(
text: text ?? "", text: text ?? "",
textColor: bgColor.invert, textColor: bgColor.contrastTextColor,
bgColor: bgColor, bgColor: bgColor,
onPressed: () async { onPressed: () async {
DirectionalScreen? directional = DirectionalScreen.build( DirectionalScreen? directional = DirectionalScreen.build(
......
...@@ -24,7 +24,7 @@ class CustomAlertDialog extends StatelessWidget { ...@@ -24,7 +24,7 @@ class CustomAlertDialog extends StatelessWidget {
return Dialog( return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Container( child: Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(0),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(16), color: Colors.white), decoration: BoxDecoration(borderRadius: BorderRadius.circular(16), color: Colors.white),
child: Stack( child: Stack(
children: [ children: [
...@@ -32,38 +32,42 @@ class CustomAlertDialog extends StatelessWidget { ...@@ -32,38 +32,42 @@ class CustomAlertDialog extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_buildHeaderImage(), _buildHeaderImage(),
const SizedBox(height: 2), Padding(
// Title padding: const EdgeInsets.all(16),
if ((alertData.title ?? "").isNotEmpty) child: Column(
Text( children: [
alertData.title!, if ((alertData.title ?? "").isNotEmpty)
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), Text(
textAlign: TextAlign.center, alertData.title!,
), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
const SizedBox(height: 8), textAlign: TextAlign.center,
if (alertData.description != null) ),
HtmlWidget(''' const SizedBox(height: 8),
<div style="text-align: center;"> if (alertData.description != null)
${alertData.description!} HtmlWidget('''
</div> <div style="text-align: center;">
'''), ${alertData.description!}
const SizedBox(height: 4), </div>
if ((alertData.content ?? "").isNotEmpty) '''),
Text( const SizedBox(height: 4),
alertData.content!, if ((alertData.content ?? "").isNotEmpty)
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: BaseColor.primary500), Text(
textAlign: TextAlign.center, alertData.content!,
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: BaseColor.primary500),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
_buildButtons(),
],
), ),
// Buttons ),
const SizedBox(height: 8),
_buildButtons(),
], ],
), ),
// Close Button (X) ở góc phải trên // Close Button (X) ở góc phải trên
if (showCloseButton) if (showCloseButton)
Positioned( Positioned(
top: 0, top: 8,
right: 0, right: 8,
child: GestureDetector( child: GestureDetector(
onTap: () => Get.back(), onTap: () => Get.back(),
child: const Icon(Icons.close, color: Colors.black, size: 24), child: const Icon(Icons.close, color: Colors.black, size: 24),
...@@ -79,10 +83,13 @@ class CustomAlertDialog extends StatelessWidget { ...@@ -79,10 +83,13 @@ class CustomAlertDialog extends StatelessWidget {
if ((alertData.urlHeaderImage ?? "").isNotEmpty) { if ((alertData.urlHeaderImage ?? "").isNotEmpty) {
return ClipRRect( return ClipRRect(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: loadNetworkImage( child: Container(
url: alertData.urlHeaderImage, // color: Colors.grey,
fit: BoxFit.cover, child: loadNetworkImage(
placeholderAsset: "assets/images/ic_pipi_06.png", url: alertData.urlHeaderImage,
fit: BoxFit.fill,
placeholderAsset: "assets/images/ic_pipi_06.png",
),
), ),
); );
} }
......
...@@ -5,6 +5,9 @@ import 'package:get/get.dart'; ...@@ -5,6 +5,9 @@ import 'package:get/get.dart';
class BottomSheetHelper { class BottomSheetHelper {
static void showBottomSheetPopup({ static void showBottomSheetPopup({
required Widget child, required Widget child,
Color backgroundContainerColor = Colors.white,
double horizontalContainerPadding = 8.0,
double bottomContainerPadding = 16.0,
bool isDismissible = true, bool isDismissible = true,
}) { }) {
showModalBottomSheet( showModalBottomSheet(
...@@ -12,11 +15,12 @@ class BottomSheetHelper { ...@@ -12,11 +15,12 @@ class BottomSheetHelper {
isScrollControlled: true, isScrollControlled: true,
isDismissible: isDismissible, isDismissible: isDismissible,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
barrierColor: Colors.black.withOpacity(0.5), barrierColor: Colors.black.withOpacity(0.7),
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)), borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
), ),
builder: (context) { builder: (context) {
final bottom = MediaQuery.of(context).padding.bottom;
return Padding( return Padding(
padding: MediaQuery.of(context).viewInsets.add( padding: MediaQuery.of(context).viewInsets.add(
const EdgeInsets.only(bottom: 0), // 👈 Safe area bottom const EdgeInsets.only(bottom: 0), // 👈 Safe area bottom
...@@ -24,11 +28,15 @@ class BottomSheetHelper { ...@@ -24,11 +28,15 @@ class BottomSheetHelper {
child: Wrap( child: Wrap(
children: [ children: [
Container( Container(
decoration: const BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: backgroundContainerColor,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)), borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
),
padding: EdgeInsets.only(
left: horizontalContainerPadding,
right: horizontalContainerPadding,
bottom: bottomContainerPadding,
), ),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16),
child: child, child: child,
), ),
SizedBox(height: 32,), SizedBox(height: 32,),
......
...@@ -16,7 +16,6 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { ...@@ -16,7 +16,6 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
this.height = 56, this.height = 56,
}); });
/// 🔥 AppBar mặc định với nút back và title
static CustomAppBar back({required String title}) { static CustomAppBar back({required String title}) {
return CustomAppBar(title: title, leftButtons: [CustomBackButton()]); return CustomAppBar(title: title, leftButtons: [CustomBackButton()]);
} }
......
...@@ -9,7 +9,7 @@ class EmptyWidget extends StatelessWidget { ...@@ -9,7 +9,7 @@ class EmptyWidget extends StatelessWidget {
super.key, super.key,
this.imageAsset = 'assets/images/ic_pipi_06.png', this.imageAsset = 'assets/images/ic_pipi_06.png',
this.content = 'Không có dữ liệu hiển thị', this.content = 'Không có dữ liệu hiển thị',
this.size = const Size(120, 120), this.size = const Size(200, 200),
}); });
@override @override
......
...@@ -54,7 +54,7 @@ class CustomNavigationBar extends StatelessWidget implements PreferredSizeWidget ...@@ -54,7 +54,7 @@ class CustomNavigationBar extends StatelessWidget implements PreferredSizeWidget
Text( Text(
title, title,
maxLines: 1, maxLines: 1,
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.bold, color: Colors.white), style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w800, color: Colors.white),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
// Back button bên trái // Back button bên trái
......
...@@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; ...@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
Widget loadNetworkImage({ Widget loadNetworkImage({
required String? url, required String? url,
BoxFit fit = BoxFit.cover, BoxFit fit = BoxFit.contain,
double? width, double? width,
double? height, double? height,
String placeholderAsset = 'assets/images/ic_logo.png', String placeholderAsset = 'assets/images/ic_logo.png',
......
import 'package:contacts_service/contacts_service.dart';
import 'package:permission_handler/permission_handler.dart';
// Future<void> pickContact(BuildContext context) async {
// // Yêu cầu quyền truy cập danh bạ
// final status = await Permission.contacts.request();
// if (!status.isGranted) {
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(content: Text('Bạn cần cấp quyền truy cập danh bạ')),
// );
// return;
// }
//
// // Mở danh sách liên hệ và chọn
// try {
// final Contact? contact = await ContactsService.openDeviceContactPicker();
// if (contact != null && contact.phones != null && contact.phones!.isNotEmpty) {
// final phoneNumber = contact.phones!.first.value;
// print("Số điện thoại được chọn: $phoneNumber");
//
// // TODO: bạn có thể gán vào TextEditingController ở đây
// _phoneController.text = phoneNumber ?? '';
// }
// } catch (e) {
// print("Lỗi khi chọn danh bạ: $e");
// }
// }
...@@ -30,6 +30,8 @@ environment: ...@@ -30,6 +30,8 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_localizations:
sdk: flutter
dio: ^5.8.0+1 dio: ^5.8.0+1
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
...@@ -47,7 +49,7 @@ dependencies: ...@@ -47,7 +49,7 @@ dependencies:
flutter_svg: flutter_svg:
local_auth: local_auth:
pin_code_fields: pin_code_fields:
intl: ^0.18.1 intl: ^0.19.0
webview_flutter: ^4.2.2 webview_flutter: ^4.2.2
webview_flutter_wkwebview: ^3.9.4 webview_flutter_wkwebview: ^3.9.4
qr_flutter: ^4.0.0 qr_flutter: ^4.0.0
...@@ -59,6 +61,10 @@ dependencies: ...@@ -59,6 +61,10 @@ dependencies:
flutter_contacts: ^1.1.6 flutter_contacts: ^1.1.6
permission_handler: ^11.0.0 permission_handler: ^11.0.0
share_plus: ^7.2.1 share_plus: ^7.2.1
file_saver: ^0.2.2
month_picker_dialog:
marquee: ^2.2.3
game_miniapp: game_miniapp:
path: ../mini_app/game_miniapp path: ../mini_app/game_miniapp
dev_dependencies: dev_dependencies:
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment