Commit fda33894 authored by DatHV's avatar DatHV
Browse files

cap nhat giao dien

parent 75178f29
......@@ -2,7 +2,7 @@ import 'package:json_annotation/json_annotation.dart';
part 'faqs_model.g.dart';
@JsonSerializable()
class FAQItemModel {
class PageItemModel {
final String? thumbnail;
@JsonKey(name: "page_id")
final String? pageId;
......@@ -11,7 +11,7 @@ class FAQItemModel {
final String? publishAtDate;
final String? chapeau;
FAQItemModel({
PageItemModel({
this.thumbnail,
this.pageId,
this.title,
......@@ -19,13 +19,13 @@ class FAQItemModel {
this.chapeau,
});
factory FAQItemModel.fromJson(Map<String, dynamic> json) => _$FAQItemModelFromJson(json);
Map<String, dynamic> toJson() => _$FAQItemModelToJson(this);
factory PageItemModel.fromJson(Map<String, dynamic> json) => _$PageItemModelFromJson(json);
Map<String, dynamic> toJson() => _$PageItemModelToJson(this);
}
@JsonSerializable()
class FAQItemModelResponse {
final List<FAQItemModel>? items;
final List<PageItemModel>? items;
FAQItemModelResponse({
this.items,
......
......@@ -6,15 +6,16 @@ part of 'faqs_model.dart';
// JsonSerializableGenerator
// **************************************************************************
FAQItemModel _$FAQItemModelFromJson(Map<String, dynamic> json) => FAQItemModel(
thumbnail: json['thumbnail'] as String?,
pageId: json['page_id'] as String?,
title: json['title'] as String?,
publishAtDate: json['publish_at_date'] as String?,
chapeau: json['chapeau'] as String?,
);
PageItemModel _$PageItemModelFromJson(Map<String, dynamic> json) =>
PageItemModel(
thumbnail: json['thumbnail'] as String?,
pageId: json['page_id'] as String?,
title: json['title'] as String?,
publishAtDate: json['publish_at_date'] as String?,
chapeau: json['chapeau'] as String?,
);
Map<String, dynamic> _$FAQItemModelToJson(FAQItemModel instance) =>
Map<String, dynamic> _$PageItemModelToJson(PageItemModel instance) =>
<String, dynamic>{
'thumbnail': instance.thumbnail,
'page_id': instance.pageId,
......@@ -28,7 +29,7 @@ FAQItemModelResponse _$FAQItemModelResponseFromJson(
) => FAQItemModelResponse(
items:
(json['items'] as List<dynamic>?)
?.map((e) => FAQItemModel.fromJson(e as Map<String, dynamic>))
?.map((e) => PageItemModel.fromJson(e as Map<String, dynamic>))
.toList(),
);
......
......@@ -6,6 +6,7 @@ import 'package:mypoint_flutter_app/widgets/back_button.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../resouce/base_color.dart';
import '../../shared/router_gage.dart';
import 'faqs_viewmodel.dart';
class FAQScreen extends BaseScreen {
......@@ -52,7 +53,7 @@ class _FAQScreenState extends BaseState<FAQScreen> with BasicState {
return GestureDetector(
onTap: () {
if (item.pageId != null && item.pageId!.isNotEmpty) {
Get.to(() => CampaignDetailScreen(pageId: item.pageId));
Get.toNamed(campaignDetailScreen, arguments: {"id": item.pageId});
} else {
Get.snackbar(
"Thông báo",
......
......@@ -5,7 +5,7 @@ import '../../base/restful_api_viewmodel.dart';
import 'faqs_model.dart';
class FAQViewModel extends RestfulApiViewModel {
var faqItems = <FAQItemModel>[].obs;
var faqItems = <PageItemModel>[].obs;
var isLoading = true.obs;
@override
......@@ -17,7 +17,7 @@ class FAQViewModel extends RestfulApiViewModel {
Future<void> fetchFAQItems() async {
showLoading();
isLoading(true);
client.websiteFolderGetPageList().then((value) {
client.websiteFolderGetPageList({"folder_uri": "FAQ"}).then((value) {
hideLoading();
isLoading(false);
faqItems.value = value.data?.items ?? [];
......
......@@ -32,7 +32,7 @@ class PreviewFlashSale {
@JsonKey(name: 'is_flash_sale_price')
final bool? isFlashSalePrice;
@JsonKey(name: 'header_img')
final String? headerImg;
String? headerImg;
@JsonKey(name: 'fs_quantity_per_person_total')
final int? fsQuantityPerPersonTotal;
@JsonKey(name: 'fs_quantity_per_person_bought')
......
import 'package:flutter/cupertino.dart';
import '../../../widgets/image_loader.dart';
import '../models/achievement_model.dart';
class AchievementCarousel extends StatelessWidget {
final List<AchievementModel> items;
final void Function(AchievementModel)? onTap;
const AchievementCarousel({super.key, required this.items, this.onTap});
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
if (items.isEmpty) return const SizedBox.shrink();
return SizedBox(
height: width*180/230/1.6,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: items.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (context, index) => AchievementCard(
achievement: items[index],
onTap: () => onTap?.call(items[index]),
),
),
);
}
}
class AchievementCard extends StatelessWidget {
final AchievementModel achievement;
final void Function()? onTap;
const AchievementCard({super.key, required this.achievement, this.onTap});
@override
Widget build(BuildContext context) {
final imageUrl = achievement.images?.first.imageUrl;
return GestureDetector(
onTap: onTap,
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: loadNetworkImage(
url: imageUrl,
fit: BoxFit.cover,
width: 280,
height: 140,
placeholderAsset: 'assets/images/ic_logo.png',
),
),
);
}
}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:infinite_carousel/infinite_carousel.dart';
class BannerCarousel extends StatefulWidget {
final List<String> imageUrls;
const BannerCarousel({super.key, required this.imageUrls});
@override
State<BannerCarousel> createState() => _BannerCarouselState();
}
class _BannerCarouselState extends State<BannerCarousel> {
late InfiniteScrollController _controller;
int _currentIndex = 0;
Timer? _autoPlayTimer;
final bool _isDragging = false;
bool _isUserScrolling = false;
@override
void initState() {
super.initState();
_controller = InfiniteScrollController(initialItem: 0);
_startAutoPlay();
}
void _startAutoPlay() {
_autoPlayTimer?.cancel();
_autoPlayTimer = Timer.periodic(const Duration(seconds: 2), (_) {
if (!_isDragging && !_isUserScrolling) {
_controller.nextItem(duration: const Duration(milliseconds: 500));
}
});
}
void _pauseAutoPlayTemporarily() {
_isUserScrolling = true;
_autoPlayTimer?.cancel();
Future.delayed(const Duration(seconds: 1), () {
_isUserScrolling = false;
_startAutoPlay();
});
}
@override
void dispose() {
_autoPlayTimer?.cancel();
_controller.dispose();
super.dispose();
}
//343/135
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width * 0.9;
return GestureDetector(
onPanDown: (_) => _pauseAutoPlayTemporarily(),
child: SizedBox(
height: width*135/343 + 16,
child: Stack(
alignment: Alignment.bottomCenter,
children: [
InfiniteCarousel.builder(
itemCount: widget.imageUrls.length,
itemExtent: width,
scrollBehavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
loop: true,
center: true,
anchor: 0.0,
velocityFactor: 0.1, // ✅ fix lỗi: snap từng page
controller: _controller,
onIndexChanged: (index) => setState(() => _currentIndex = index % widget.imageUrls.length),
itemBuilder: (context, itemIndex, realIndex) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
widget.imageUrls[itemIndex % widget.imageUrls.length],
fit: BoxFit.cover,
width: double.infinity,
),
),
);
},
),
Positioned(
bottom: 12,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: widget.imageUrls.asMap().entries.map((entry) {
return GestureDetector(
onTap: () => _controller.animateToItem(entry.key, duration: const Duration(milliseconds: 500)),
child: Container(
width: 8.0,
height: 8.0,
margin: const EdgeInsets.symmetric(horizontal: 4.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _currentIndex == entry.key
? Colors.white
: Colors.white.withOpacity(0.4),
),
),
);
}).toList(),
),
),
],
),
),
);
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../../preference/data_preference.dart';
import '../../../preference/point/header_home_model.dart';
import '../../../shared/router_gage.dart';
class HomeGreetingHeader extends StatelessWidget {
final double? heightContent;
HeaderHomeModel dataHeader;
String level = "Hạng Đồng";
HomeGreetingHeader({
super.key,
this.heightContent,
required this.dataHeader,
});
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final heightSize = heightContent ?? (width * 86 / 375 + 112);
final name = DataPreference.instance.profile?.workingSite?.name ?? "Quý Khách";
double heightWhiteBox = 112;
return Stack(
children: [
Container(
height: heightSize,
width: double.infinity,
decoration: const BoxDecoration(
image: DecorationImage(image: AssetImage('assets/images/bg_header_navi.png'), fit: BoxFit.cover),
),
// child: loadNetworkImage(url: dataHeader.background, fit: BoxFit.cover, placeholderAsset: "assets/images/bg_header_navi.png"),
),
Positioned(
bottom: heightWhiteBox + 16,
left: 16,
child: Image.asset(
'assets/images/ic_logo_mypoint.png',
height: 24,
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
height: heightWhiteBox,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topRight: Radius.circular(16),
topLeft: Radius.circular(16),
), //.circular(20),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Xin chào $name!',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87),
),
Row(
children: [
GestureDetector(
onTap: _onSearchTap,
child: Container(
padding: EdgeInsets.all(8),
child: Image.asset('assets/images/ic_search_black.png', width: 32, height: 32),
),
),
GestureDetector(
onTap: _onNotificationTap,
child: Container(
padding: EdgeInsets.all(8),
child: Stack(
children: [
Image.asset('assets/images/ic_notify_black.png', width: 32, height: 32),
if (1 > 0)
Positioned(
right: 6,
top: 4,
child: Container(
height: 8,
width: 8,
decoration: BoxDecoration(color: Colors.red, shape: BoxShape.circle),
),
),
],
),
),
),
],
),
],
),
const SizedBox(height: 2),
Row(
children: [
_buildStatItem(icon: "assets/images/ic_point_gray.png", value: dataHeader.totalPointActive.toString(), onTap: _onPointTap),
SizedBox(width: 12),
_buildStatItem(
icon: "assets/images/ic_voucher_gray.png",
value: dataHeader.totalVoucher.toString(),
onTap: _onMyVoucherTap,
),
SizedBox(width: 12),
_buildStatItem(icon: "assets/images/ic_rank_gray.png", value: level, onTap: _onRankTap),
],
),
],
),
),
),
],
);
}
Widget _buildStatItem({required String icon, required String value, VoidCallback? onTap}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.black26),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(padding: EdgeInsets.all(2), child: Image.asset(icon, width: 24, height: 24)),
SizedBox(width: 2),
Text(value, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.black87)),
],
),
),
);
}
_onSearchTap() {
Get.toNamed(vouchersScreen, arguments: {"enableSearch": true});
}
_onNotificationTap() {
Get.toNamed(notificationScreen);
}
_onPointTap() {
print("_onPointTap");
}
_onMyVoucherTap() {
print("_onMyVoucherTap");
}
_onRankTap() {
print("_onRankTap");
}
}
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:mypoint_flutter_app/screen/home/custom_widget/scrollable_header.dart';
import '../../../shared/router_gage.dart';
import '../../voucher/sub_widget/voucher_section_title.dart';
import '../home_tab_viewmodel.dart';
import '../models/achievement_model.dart';
import '../models/main_service_model.dart';
import 'achievement_carousel_widget.dart';
import 'banner_carousel_widget.dart';
import 'main_service_grid_widget.dart';
class HomeScreenWithHeader extends StatefulWidget {
const HomeScreenWithHeader({super.key});
@override
State<HomeScreenWithHeader> createState() => _HomeScreenWithHeaderState();
}
class _HomeScreenWithHeaderState extends State<HomeScreenWithHeader> {
String backgroundImage = 'https://images.unsplash.com/photo-1557683316-973673baf926?w=800';
final HomeTabViewModel _viewModel = Get.put(HomeTabViewModel());
bool _showHover = true;
List<MainServiceModel> _services = [];
List<AchievementModel> _achievements = [];
@override
void initState() {
super.initState();
loadMainServicesFromAsset().then((list) {
setState(() => _services = list);
});
loadMainAchievementsFromAsset().then((list) {
setState(() => _achievements = list);
});
}
// Sample data
final String userName = 'Khánh';
final int coinCount = 100;
final int messageCount = 8;
final String userRank = 'Hạng Đồng';
void _changeBackground() {
final List<String> backgrounds = [
'https://images.unsplash.com/photo-1557683316-973673baf926?w=800',
'https://images.unsplash.com/photo-1579952363873-27d3bfad9c0d?w=800',
'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800',
'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800',
'https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=800',
];
setState(() {
backgroundImage = backgrounds[(backgrounds.indexOf(backgroundImage) + 1) % backgrounds.length];
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
top: false, // Cho phép content hiển thị dưới status bar
child: CustomScrollView(
physics: BouncingScrollPhysics(), // Hiệu ứng bounce khi scroll
slivers: [
// Scrollable Header
ScrollableHeader(
backgroundImageUrl: backgroundImage,
userName: userName,
coinCount: coinCount,
messageCount: messageCount,
userRank: userRank,
onSearchTap: () {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Search tapped')));
},
onNotificationTap: () {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Notification tapped')));
},
onCoinTap: () {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Coin: $coinCount')));
},
onMessageTap: () {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Messages: $messageCount')));
},
onRankTap: () {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Rank: $userRank')));
},
),
// Content area
SliverToBoxAdapter(
child: Container(
color: Colors.white,
child: Column(
children: [
BannerCarousel(
imageUrls: [
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/F31FF2E775D7BFC940156709FB79E883/1746430303',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/1B67CFBF96BD24929EB10F1853A47651/1740708747',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/29B40B1E04EBEC8A1C1F9D13C0194A27/1735194572',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/C29872C4F95B280B880DE45BC07E7DE4/1693906872',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/F31FF2E775D7BFC940156709FB79E883/1746430303',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/F31FF2E775D7BFC940156709FB79E883/1746430303',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/F31FF2E775D7BFC940156709FB79E883/1746430303',
],
),
if (_services.isNotEmpty)
MainServiceGrid(
services: _services,
onTap: (item) {
print("Tapped: ${item.serviceName}");
},
),
if (_achievements.isNotEmpty)
HeaderSectionTitle(
title: 'Sự kiện MyPoint',
onViewAll: () {
Get.toNamed(vouchersScreen, arguments: {"isHotProduct": true});
},
),
if (_achievements.isNotEmpty)
AchievementCarousel(
items: _achievements,
onTap: (item) {
// xử lý khi nhấn vào card
},
),
// Sample content
...List.generate(20, (index) => _buildContentCard(index)),
// Bottom padding
SizedBox(height: 20),
],
),
),
),
],
),
),
);
}
Widget _buildContentCard(int index) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 8, offset: Offset(0, 2))],
),
child: Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(color: Colors.red[400]!.withOpacity(0.1), borderRadius: BorderRadius.circular(8)),
child: Icon(Icons.card_giftcard, color: Colors.red[400]),
),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Nội dung ${index + 1}',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.black87),
),
SizedBox(height: 4),
Text(
'Mô tả chi tiết cho nội dung số ${index + 1}',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
],
),
),
Icon(Icons.arrow_forward_ios, color: Colors.grey[400], size: 16),
],
),
);
}
Future<List<MainServiceModel>> loadMainServicesFromAsset() async {
final jsonStr = await rootBundle.loadString('assets/data/main_services.json');
final json = jsonDecode(jsonStr);
final List list = json['data'];
return list.map((e) => MainServiceModel.fromJson(e)).toList();
}
Future<List<AchievementModel>> loadMainAchievementsFromAsset() async {
final jsonStr = await rootBundle.loadString('assets/data/main_achievements.json');
final json = jsonDecode(jsonStr);
final List list = json['data'];
return list.map((e) => AchievementModel.fromJson(e)).toList();
}
}
import 'dart:async';
import 'package:flutter/material.dart';
class HoverView extends StatefulWidget {
final String imagePath;
final VoidCallback? onTap;
final VoidCallback? onClose;
final double size;
final Color backgroundColor;
final Color closeButtonColor;
final double countDownTime;
const HoverView({
super.key,
required this.imagePath,
this.onTap,
this.onClose,
this.size = 100.0,
this.backgroundColor = Colors.white,
this.closeButtonColor = Colors.red,
this.countDownTime = 0.0,
});
@override
State<HoverView> createState() => _HoverViewState();
}
class _HoverViewState extends State<HoverView> {
Offset _position = const Offset(0, 0);
bool _isDragging = false;
bool _showCloseButton = false;
Size _screenSize = Size.zero;
bool _isInitialized = false;
late int _remainingSeconds;
Timer? _timer;
double get _expandBottom {
if (_remainingSeconds > 0) {
return 30.0;
}
return 8.0;
}
@override
void initState() {
super.initState();
_remainingSeconds = widget.countDownTime.toInt();
if (_remainingSeconds > 0) {
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (!mounted) return;
setState(() {
_remainingSeconds--;
if (_remainingSeconds <= 0) {
_timer?.cancel();
}
});
});
}
WidgetsBinding.instance.addPostFrameCallback((_) {
_setInitialPosition();
});
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
void _setInitialPosition() {
if (!mounted) return;
final paddingBottom = MediaQuery.of(context).padding.bottom + _expandBottom;
final size = MediaQuery.of(context).size;
if (size.width > 0 && size.height > 0) {
setState(() {
_screenSize = size;
_position = Offset(_screenSize.width - widget.size - 20, _screenSize.height - widget.size - paddingBottom);
_isInitialized = true;
});
}
}
void _onPanStart(DragStartDetails details) {
setState(() {
_isDragging = true;
_showCloseButton = true;
});
}
String _formatTime(int seconds) {
if (seconds > 30 * 3600) {
final days = (seconds / 86400).ceil();
return '$days Ngày';
} else {
final h = (seconds ~/ 3600).toString().padLeft(2, '0');
final m = ((seconds % 3600) ~/ 60).toString().padLeft(2, '0');
final s = (seconds % 60).toString().padLeft(2, '0');
return '$h:$m:$s';
}
}
void _onPanUpdate(DragUpdateDetails details) {
if (!mounted || _screenSize.width <= 0 || _screenSize.height <= 0) return;
setState(() {
_position += details.delta;
// Constrain position within screen bounds
_position = Offset(
_position.dx.clamp(0, _screenSize.width - widget.size),
_position.dy.clamp(0, _screenSize.height - widget.size),
);
});
}
void _onPanEnd(DragEndDetails details) {
setState(() {
_isDragging = false;
});
_snapToNearestCorner();
}
void _snapToNearestCorner() {
final paddingBottom = MediaQuery.of(context).padding.bottom + _expandBottom;
if (!mounted || _screenSize.width <= 0 || _screenSize.height <= 0) return;
final corners = [
Offset(20, paddingBottom), // Top left
Offset(_screenSize.width - widget.size - 20, paddingBottom), // Top right
Offset(20, _screenSize.height - widget.size - paddingBottom), // Bottom left
Offset(_screenSize.width - widget.size - 20, _screenSize.height - widget.size - paddingBottom), // Bottom right
];
Offset nearestCorner = corners[0];
double minDistance = double.infinity;
for (final corner in corners) {
final distance = (_position - corner).distance;
if (distance < minDistance) {
minDistance = distance;
nearestCorner = corner;
}
}
// Animate to nearest corner using setState
setState(() {
_position = nearestCorner;
_showCloseButton = false;
});
}
void _onTap() {
if (widget.onTap != null) {
widget.onTap!();
}
}
void _onClose() {
if (widget.onClose != null) {
widget.onClose!();
}
}
@override
Widget build(BuildContext context) {
// Update screen size on build to handle orientation changes
final currentSize = MediaQuery.of(context).size;
if (_screenSize != currentSize && currentSize.width > 0 && currentSize.height > 0) {
_screenSize = currentSize;
// Adjust position if needed when screen size changes
if (_isInitialized) {
_position = Offset(
_position.dx.clamp(0, _screenSize.width - widget.size),
_position.dy.clamp(0, _screenSize.height - widget.size),
);
} else {
_setInitialPosition();
}
}
// Don't render until we have valid dimensions
if (!_isInitialized || _screenSize.width <= 0 || _screenSize.height <= 0) {
return const SizedBox.shrink();
}
return Positioned(
left: _position.dx,
top: _position.dy,
width: widget.size,
height: widget.size + (_remainingSeconds > 0 ? 24 : 0),
child: GestureDetector(
onPanStart: _onPanStart,
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
onTap: _onTap,
onLongPress: () {
setState(() {
_showCloseButton = !_showCloseButton;
});
},
child: Stack(
clipBehavior: Clip.none,
children: [
// Main container with image (no border, no pulse effect)
Column(
children: [
AnimatedScale(
scale: _isDragging ? 1.1 : 1.0,
duration: const Duration(milliseconds: 200),
child: SizedBox(
width: widget.size,
height: widget.size,
child:
widget.imagePath.startsWith('http')
? Image.network(widget.imagePath, fit: BoxFit.cover)
: Image.asset(widget.imagePath, fit: BoxFit.cover),
),
),
if (_remainingSeconds > 0)
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.6),
borderRadius: BorderRadius.circular(8),
),
child: Text(
_formatTime(_remainingSeconds),
style: const TextStyle(fontSize: 12, color: Colors.white, fontWeight: FontWeight.w500),
),
),
],
),
// Close button
if (_showCloseButton)
Positioned(
top: -8,
right: -8,
width: 24,
height: 24,
child: GestureDetector(
onTap: _onClose,
child: const Icon(Icons.close, color: Colors.transparent, size: 16),
),
),
],
),
),
);
}
}
import 'package:flutter/material.dart';
import '../../../widgets/image_loader.dart';
import '../models/main_service_model.dart';
class MainServiceGrid extends StatelessWidget {
final List<MainServiceModel> services;
final void Function(MainServiceModel)? onTap;
const MainServiceGrid({super.key, required this.services, this.onTap});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 120,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: services.length,
separatorBuilder: (_, __) => const SizedBox(width: 16),
itemBuilder: (context, index) => _buildItem(context, services[index]),
),
),
);
}
Widget _buildItem(BuildContext context, MainServiceModel item) {
return InkWell(
onTap: () => onTap?.call(item),
child: SizedBox(
width: 64,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Stack(
alignment: Alignment.topRight,
children: [
Container(
width: 64,
height: 64,
padding: const EdgeInsets.all(10),
child: item.serviceIcon != null && item.serviceIcon!.isNotEmpty
? Image.asset(item.serviceIcon!, fit: BoxFit.contain)
: loadNetworkImage(
url: item.imageUrl,
fit: BoxFit.contain,
placeholderAsset: 'assets/images/ic_logo.png',
),
),
if (item.eventDescrible != null && item.eventDescrible!.isNotEmpty)
Positioned(
top: 2,
right: 2,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Color(_parseColor(item.eventColor ?? 'EB3C4B')),
borderRadius: BorderRadius.circular(12),
),
child: Text(
item.eventDescrible!,
style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold),
),
),
)
],
),
Text(
maxLines: 2,
item.serviceName ?? '',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 13),
)
],
),
),
);
}
int _parseColor(String hexColor) {
hexColor = hexColor.replaceAll("#", "");
if (hexColor.length == 6) {
hexColor = "FF$hexColor";
}
return int.parse(hexColor, radix: 16);
}
}
import 'package:flutter/material.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../faqs/faqs_model.dart';
class NewsCarouselWidget extends StatelessWidget {
final List<PageItemModel> items;
final void Function(PageItemModel)? onTap;
const NewsCarouselWidget({super.key, required this.items, this.onTap});
@override
Widget build(BuildContext context) {
final widthItem = MediaQuery.of(context).size.width/1.6;
if (items.isEmpty) return const SizedBox.shrink();
return SizedBox(
height: widthItem*9/16 + 72,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: items.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (context, index) => _buildItem(context, items[index]),
),
);
}
Widget _buildItem(BuildContext context, PageItemModel news) {
final widthItem = MediaQuery.of(context).size.width/1.6;
return GestureDetector(
onTap: () => onTap?.call(news),
child: Container(
width: widthItem,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade200),
color: Colors.white,
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 6, offset: const Offset(0, 2))],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: const BorderRadius.only(topLeft: Radius.circular(16), topRight: Radius.circular(16)),
child: loadNetworkImage(
url: news.thumbnail ?? '',
height: widthItem * 9/16,
width: double.infinity,
fit: BoxFit.cover,
placeholderAsset: 'assets/images/bg_default_169.png',
),
),
Padding(
padding: const EdgeInsets.all(12),
child: Text(
news.title ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87),
),
),
],
),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../../widgets/custom_point_text_tag.dart';
import '../../voucher/models/product_model.dart';
class ProductGrid extends StatelessWidget {
final List<ProductModel> products;
final void Function(ProductModel)? onTap;
final double _spacing = 12;
const ProductGrid({super.key, required this.products, this.onTap});
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final widthItem = (width - _spacing * 3)/2;
return GridView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 16),
itemCount: products.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: widthItem / (widthItem * 9/16 + 94),
mainAxisSpacing: _spacing,
crossAxisSpacing: _spacing,
),
itemBuilder: (context, index) => _buildItem(context, products[index]),
);
}
Widget _buildItem(BuildContext context, ProductModel product) {
final width = MediaQuery.of(context).size.width;
final widthItem = (width - _spacing * 3)/2;
return InkWell(
onTap: () => onTap?.call(product),
borderRadius: BorderRadius.circular(12),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: loadNetworkImage(
url: product.banner?.url ?? '',
height: widthItem * 9/16,
width: double.infinity,
fit: BoxFit.cover,
placeholderAsset: "assets/images/bg_default_169.png",
),
),
Padding(
padding: const EdgeInsets.all(8),
child: Text(
product.name ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
),
const Spacer(),
Padding(
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
child: CustomPointText(point: product.amountToBePaid ?? 0, type: product.price?.method),
)
],
),
),
);
}
}
import 'package:flutter/material.dart';
class ScrollableHeader extends StatelessWidget {
final String backgroundImageUrl;
final String userName;
final int coinCount;
final int messageCount;
final String userRank;
final VoidCallback? onSearchTap;
final VoidCallback? onNotificationTap;
final VoidCallback? onCoinTap;
final VoidCallback? onMessageTap;
final VoidCallback? onRankTap;
const ScrollableHeader({
super.key,
required this.backgroundImageUrl,
required this.userName,
this.coinCount = 0,
this.messageCount = 0,
this.userRank = 'Hạng Đồng',
this.onSearchTap,
this.onNotificationTap,
this.onCoinTap,
this.onMessageTap,
this.onRankTap,
});
@override
Widget build(BuildContext context) {
return SliverAppBar(
expandedHeight: 200.0,
floating: false,
pinned: false,
elevation: 0,
backgroundColor: Colors.transparent,
flexibleSpace: FlexibleSpaceBar(
background: Stack(
children: [
// Background Image
Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
image: DecorationImage(
image: backgroundImageUrl.startsWith('http')
? NetworkImage(backgroundImageUrl)
: AssetImage(backgroundImageUrl) as ImageProvider,
fit: BoxFit.cover,
),
),
),
// Gradient overlay
Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.3),
Colors.transparent,
],
),
),
),
// MyPoint Logo
Positioned(
top: 60,
left: 20,
child: Row(
children: [
Text(
'mypoint',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
],
),
),
// Decorative circles
Positioned(
top: 40,
right: 50,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withOpacity(0.1),
),
),
),
Positioned(
top: 80,
right: 20,
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withOpacity(0.15),
),
),
),
],
),
),
bottom: PreferredSize(
preferredSize: Size.fromHeight(80),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
// Greeting and actions row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Xin chào $userName!',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
Row(
children: [
GestureDetector(
onTap: onSearchTap,
child: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[100],
shape: BoxShape.circle,
),
child: Icon(
Icons.search,
color: Colors.grey[600],
size: 20,
),
),
),
SizedBox(width: 12),
GestureDetector(
onTap: onNotificationTap,
child: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[100],
shape: BoxShape.circle,
),
child: Stack(
children: [
Icon(
Icons.notifications_outlined,
color: Colors.grey[600],
size: 20,
),
if (messageCount > 0)
Positioned(
right: 0,
top: 0,
child: Container(
padding: EdgeInsets.all(2),
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
constraints: BoxConstraints(
minWidth: 12,
minHeight: 12,
),
child: Text(
messageCount > 99 ? '99+' : messageCount.toString(),
style: TextStyle(
color: Colors.white,
fontSize: 8,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
),
],
),
),
),
],
),
],
),
SizedBox(height: 16),
// Stats row
Row(
children: [
_buildStatItem(
icon: Icons.monetization_on_outlined,
value: coinCount.toString(),
iconColor: Colors.orange,
onTap: onCoinTap,
),
SizedBox(width: 12),
_buildStatItem(
icon: Icons.mail_outline,
value: messageCount.toString(),
iconColor: Colors.blue,
onTap: onMessageTap,
),
SizedBox(width: 12),
_buildStatItem(
icon: Icons.person_outline,
value: userRank,
iconColor: Colors.green,
onTap: onRankTap,
),
],
),
],
),
),
),
),
);
}
Widget _buildStatItem({
required IconData icon,
required String value,
required Color iconColor,
VoidCallback? onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.grey[200]!),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
color: iconColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: iconColor,
size: 16,
),
),
SizedBox(width: 8),
Text(
value,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
],
),
),
);
}
}
\ No newline at end of file
// home_screen.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:game_miniapp/game_miniapp.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart';
import 'package:mypoint_flutter_app/screen/home/custom_widget/header_home.dart';
import 'package:mypoint_flutter_app/screen/home/custom_widget/product_grid_widget.dart';
import 'package:mypoint_flutter_app/screen/home/pipi_detail_screen.dart';
import 'package:mypoint_flutter_app/shared/router_gage.dart';
import '../setting/setting_screen.dart';
import '../voucher/sub_widget/voucher_section_title.dart';
import 'custom_widget/achievement_carousel_widget.dart';
import 'custom_widget/banner_carousel_widget.dart';
import 'custom_widget/hover_view.dart';
import 'custom_widget/main_service_grid_widget.dart';
import 'custom_widget/news_carousel_widget.dart';
import 'home_tab_viewmodel.dart';
import 'models/achievement_model.dart';
import 'models/main_service_model.dart';
class HomeScreen extends StatelessWidget {
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final HomeTabViewModel _viewModel = Get.put(HomeTabViewModel());
bool _showHover = true;
@override
void initState() {
super.initState();
_viewModel.getSectionLayoutHome();
}
Widget _buildSliverHeader(double heightHeader) {
return Obx(() {
final data = _viewModel.headerHomeData.value;
if (data == null) return SliverToBoxAdapter(child: SizedBox.shrink());
return SliverToBoxAdapter(
child: HomeGreetingHeader(
dataHeader: data,
heightContent: heightHeader,
),
);
});
}
@override
Widget build(BuildContext context) {
final paddingBottom = MediaQuery.of(context).padding.bottom + 20;
final width = MediaQuery.of(context).size.width;
final heightHeader = width * 86 / 375 + 112;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ElevatedButton(onPressed: () => _showMiniGame(context), child: const Text('Mini Game')),
ElevatedButton(onPressed: () => _logout(context), child: const Text('Đăng xuất')),
ElevatedButton(onPressed: () => _showSetting(context), child: const Text('Setting')),
ElevatedButton(onPressed: () => _showNotify(context), child: const Text('Notify')),
],
),
body: Stack(
children: [
NestedScrollView(
physics: AlwaysScrollableScrollPhysics(),
headerSliverBuilder:
(_, _) => [
_buildSliverHeader(heightHeader),
// SliverToBoxAdapter(
// child: Obx(() {
// if (_viewModel.headerHomeData.value == null) return SizedBox.shrink();
// return HomeGreetingHeader(
// dataHeader: _viewModel.headerHomeData.value!,
// heightContent: heightHeader);
// }),
// ),
],
body: RefreshIndicator(
onRefresh: _onRefresh,
child: Obx(() {
return ListView(
padding: EdgeInsets.only(bottom: paddingBottom),
physics: AlwaysScrollableScrollPhysics(),
children: [
BannerCarousel(
imageUrls: [
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/F31FF2E775D7BFC940156709FB79E883/1746430303',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/1B67CFBF96BD24929EB10F1853A47651/1740708747',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/29B40B1E04EBEC8A1C1F9D13C0194A27/1735194572',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/C29872C4F95B280B880DE45BC07E7DE4/1693906872',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/F31FF2E775D7BFC940156709FB79E883/1746430303',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/F31FF2E775D7BFC940156709FB79E883/1746430303',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/F31FF2E775D7BFC940156709FB79E883/1746430303',
],
),
if (_viewModel.services.value.isNotEmpty)
MainServiceGrid(
services: _viewModel.services.value,
onTap: (item) {
item.directionalScreen?.begin();
},
),
if (_viewModel.achievements.value.isNotEmpty)
HeaderSectionTitle(
title: 'Sự kiện MyPoint',
onViewAll: () {
Get.toNamed(achievementListScreen);
},
),
if (_viewModel.achievements.value.isNotEmpty)
AchievementCarousel(
items: _viewModel.achievements.value,
onTap: (item) {
item.directionScreen?.begin();
},
),
if (_viewModel.products.value.isNotEmpty)
ProductGrid(
products: _viewModel.products.value,
onTap: (product) {
Get.toNamed(voucherDetailScreen, arguments: product.id);
},
),
if (_viewModel.news.value.isNotEmpty)
HeaderSectionTitle(
title: 'MyPoint có gì hot?',
onViewAll: () {
Get.toNamed(newsListScreen);
},
),
if (_viewModel.news.value.isNotEmpty)
NewsCarouselWidget(
items: _viewModel.news.value,
onTap: (item) async {
Get.toNamed(campaignDetailScreen, arguments: {"id": item.pageId});
},
),
// ElevatedButton(onPressed: () => _showMiniGame(context), child: const Text('Mini Game')),
// ElevatedButton(onPressed: () => _logout(context), child: const Text('Đăng xuất')),
// ElevatedButton(onPressed: () => _showSetting(context), child: const Text('Setting')),
// ElevatedButton(onPressed: () => _showNotify(context), child: const Text('Notify')),
],
);
}),
),
),
if (_showHover)
Positioned.fill(
child: Obx(() {
return HoverView(
imagePath: _viewModel.hoverData.value?.icon ?? '',
onTap: _handleHoverViewTap,
onClose: _handleCloseHoverView,
backgroundColor: Colors.transparent,
size: 80,
countDownTime: _viewModel.hoverData.value?.countDownTime ?? 0.0,
);
}),
),
],
),
);
}
void _showNotify(BuildContext context) async {
Get.toNamed(notificationScreen);
void _handleHoverViewTap() {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (_) => PipiDetailScreen(),
);
}
void _handleCloseHoverView() {
setState(() {
_showHover = false;
});
}
Future<void> _onRefresh() async {
print("onRefresh");
await _viewModel.getSectionLayoutHome();
}
void _showMiniGame(BuildContext context) async {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const GameMiniAppScreen()),
);
Navigator.push(context, MaterialPageRoute(builder: (_) => const GameMiniAppScreen()));
}
void _logout(BuildContext context) async {
......@@ -51,11 +195,9 @@ class HomeScreen extends StatelessWidget {
],
),
);
if (confirm == true) {
DataPreference.instance.clearLoginToken();
_safeBackToLogin();
// Get.until((route) => route.settings.name == loginScreen);
}
}
......@@ -80,4 +222,4 @@ class HomeScreen extends StatelessWidget {
void _showSetting(BuildContext context) async {
Get.toNamed(settingScreen);
}
}
\ No newline at end of file
}
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../base/restful_api_viewmodel.dart';
import '../../preference/point/header_home_model.dart';
import '../faqs/faqs_model.dart';
import '../voucher/models/product_model.dart';
import '../voucher/models/product_type.dart';
import 'models/achievement_model.dart';
import 'models/hover_data_model.dart';
import 'models/main_section_config_model.dart';
import 'models/main_service_model.dart';
import 'models/notification_unread_model.dart';
class HomeTabViewModel extends RestfulApiViewModel {
final RxList<ProductModel> products = <ProductModel>[].obs;
final RxList<PageItemModel> news = <PageItemModel>[].obs;
final RxList<MainServiceModel> services = <MainServiceModel>[].obs;
final RxList<AchievementModel> achievements = <AchievementModel>[].obs;
var hoverData = Rxn<HoverDataModel>();
var notificationUnreadData = Rxn<NotificationUnreadData>();
var headerHomeData = Rxn<HeaderHomeModel>();
List<MainSectionConfigModel> sectionLayouts = [];
@override
void onInit() {
super.onInit();
getDynamicHeaderHome();
// getSectionLayoutHome();
// getHotProducts();
// fetchFAQItems();
// loadMainServicesFromAsset();
// loadMainAchievementsFromAsset();
// loadDataPiPiHome();
// getNotificationUnread();
}
Future<void> getSectionLayoutHome() async {
showLoading();
try {
final response = await client.getSectionLayoutHome();
if (response.data != null) {
sectionLayouts = response.data ?? [];
}
} catch (error) {
print("Error fetching section layout: $error");
} finally {
hideLoading();
}
}
Future<void> getHotProducts() async {
final body = {
"type": ProductType.voucher.value,
"size": 10,
"index": 0,
"catalog_code": "HOT",
};
try {
final result = await client.getProducts(body);
products.value = result.data ?? [];
} catch (error) {
print("Error fetching hot products: $error");
}
}
Future<void> loadDataPiPiHome() async {
try {
final result = await client.getDataPiPiHome();
hoverData.value = result.data;
} catch (error) {
print("Error fetching loadDataPiPiHome: $error");
}
}
Future<void> getDynamicHeaderHome() async {
try {
final result = await client.getDynamicHeaderHome();
headerHomeData.value = result.data;
} catch (error) {
print("Error fetching getDynamicHeaderHome: $error");
}
}
Future<void> getNotificationUnread() async {
try {
final result = await client.getNotificationUnread();
notificationUnreadData.value = result.data;
} catch (error) {
print("Error fetching hot products: $error");
}
}
Future<void> fetchFAQItems() async {
showLoading();
client.websiteFolderGetPageList({"folder_uri": "TIN-TUC", "limit": 20}).then((value) {
hideLoading();
news.value = value.data?.items ?? [];
});
}
Future<void> loadMainServicesFromAsset() async {
final jsonStr = await rootBundle.loadString('assets/data/main_services.json');
final json = jsonDecode(jsonStr);
final List list = json['data'];
services.value = list.map((e) => MainServiceModel.fromJson(e)).toList();
}
Future<void> loadMainAchievementsFromAsset() async {
final jsonStr = await rootBundle.loadString('assets/data/main_achievements.json');
final json = jsonDecode(jsonStr);
final List list = json['data'];
achievements.value = list.map((e) => AchievementModel.fromJson(e)).toList();
}
}
\ No newline at end of file
import 'package:get/get.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:mypoint_flutter_app/directional/directional_screen.dart';
part 'achievement_model.g.dart';
@JsonSerializable()
class AchievementModel {
final String? id;
@JsonKey(name: 'achievement_name')
final String? achievementName;
@JsonKey(name: 'achievement_icon_url')
final String? achievementIconUrl;
@JsonKey(name: 'apply_for_group')
final String? applyForGroup;
@JsonKey(name: 'click_action_type')
final String? clickActionType;
@JsonKey(name: 'click_action_param')
final String? clickActionParam;
final List<AchievementImageModel>? images;
const AchievementModel({
this.id,
this.achievementName,
this.achievementIconUrl,
this.applyForGroup,
this.clickActionType,
this.clickActionParam,
this.images,
});
DirectionalScreen? get directionScreen {
return DirectionalScreen.build(
clickActionType: clickActionType,
clickActionParam: clickActionParam,
);
}
String? get urlBackground {
final coverImage = images?.firstWhereOrNull(
(image) => image.imageType == 'COVER',
);
final url = coverImage?.imageUrl ?? images?.firstOrNull?.imageUrl;
print("urlBackground: $url");
return url;
}
factory AchievementModel.fromJson(Map<String, dynamic> json) =>
_$AchievementModelFromJson(json);
Map<String, dynamic> toJson() => _$AchievementModelToJson(this);
}
@JsonSerializable()
class AchievementImageModel {
final String? id;
@JsonKey(name: 'image_type')
final String? imageType;
@JsonKey(name: 'image_url')
final String? imageUrl;
const AchievementImageModel({
this.id,
this.imageType,
this.imageUrl,
});
factory AchievementImageModel.fromJson(Map<String, dynamic> json) =>
_$AchievementImageModelFromJson(json);
Map<String, dynamic> toJson() => _$AchievementImageModelToJson(this);
}
class AchievementListResponse {
final List<AchievementModel>? achievements;
const AchievementListResponse({this.achievements});
factory AchievementListResponse.fromJson(Map<String, dynamic> json) {
return AchievementListResponse(
achievements: (json['achievements'] as List?)
?.map((e) => AchievementModel.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
Map<String, dynamic> toJson() {
return {
'achievements': achievements?.map((e) => e.toJson()).toList(),
};
}
}
\ No newline at end of file
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'achievement_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AchievementModel _$AchievementModelFromJson(Map<String, dynamic> json) =>
AchievementModel(
id: json['id'] as String?,
achievementName: json['achievement_name'] as String?,
achievementIconUrl: json['achievement_icon_url'] as String?,
applyForGroup: json['apply_for_group'] as String?,
clickActionType: json['click_action_type'] as String?,
clickActionParam: json['click_action_param'] as String?,
images:
(json['images'] as List<dynamic>?)
?.map(
(e) =>
AchievementImageModel.fromJson(e as Map<String, dynamic>),
)
.toList(),
);
Map<String, dynamic> _$AchievementModelToJson(AchievementModel instance) =>
<String, dynamic>{
'id': instance.id,
'achievement_name': instance.achievementName,
'achievement_icon_url': instance.achievementIconUrl,
'apply_for_group': instance.applyForGroup,
'click_action_type': instance.clickActionType,
'click_action_param': instance.clickActionParam,
'images': instance.images,
};
AchievementImageModel _$AchievementImageModelFromJson(
Map<String, dynamic> json,
) => AchievementImageModel(
id: json['id'] as String?,
imageType: json['image_type'] as String?,
imageUrl: json['image_url'] as String?,
);
Map<String, dynamic> _$AchievementImageModelToJson(
AchievementImageModel instance,
) => <String, dynamic>{
'id': instance.id,
'image_type': instance.imageType,
'image_url': instance.imageUrl,
};
import 'package:json_annotation/json_annotation.dart';
part 'brand_category_model.g.dart'; // Nếu dùng build_runner
@JsonSerializable()
class BrandCategoryModel {
final String? id;
final String? subscribed;
@JsonKey(name: 'category_code')
final String? categoryCode;
@JsonKey(name: 'category_name')
final String? categoryName;
@JsonKey(name: 'image_url')
final String? imageUrl;
const BrandCategoryModel({
this.id,
this.subscribed,
this.categoryCode,
this.categoryName,
this.imageUrl,
});
factory BrandCategoryModel.fromJson(Map<String, dynamic> json) => _$BrandCategoryModelFromJson(json);
Map<String, dynamic> toJson() => _$BrandCategoryModelToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'brand_category_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
BrandCategoryModel _$BrandCategoryModelFromJson(Map<String, dynamic> json) =>
BrandCategoryModel(
id: json['id'] as String?,
subscribed: json['subscribed'] as String?,
categoryCode: json['category_code'] as String?,
categoryName: json['category_name'] as String?,
imageUrl: json['image_url'] as String?,
);
Map<String, dynamic> _$BrandCategoryModelToJson(BrandCategoryModel instance) =>
<String, dynamic>{
'id': instance.id,
'subscribed': instance.subscribed,
'category_code': instance.categoryCode,
'category_name': instance.categoryName,
'image_url': instance.imageUrl,
};
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