Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
Hoàng Văn Đạt
mypoint_flutter_app
Commits
f0334970
Commit
f0334970
authored
Oct 31, 2025
by
DatHV
Browse files
update flash sale
parent
b93b2948
Changes
31
Hide whitespace changes
Inline
Side-by-side
lib/configs/api_paths.dart
View file @
f0334970
...
...
@@ -96,6 +96,8 @@ class APIPaths {//sandbox
static
const
String
getCampaignMissions
=
"/campaign/api/v3.0/%@/missions"
;
static
const
String
getCampaignReward
=
"/campaign/api/v3.0/%@/reward"
;
static
const
String
submitCampaignMission
=
"/campaign/api/v3.0/%@/mission/%@/submit"
;
static
const
String
getFlashSaleGroup
=
"/campaign/api/v3.0/flash_sale/group/%@"
;
static
const
String
getFlashSaleDetail
=
"/campaign/api/v3.0/flash_sale/detail/%@"
;
static
const
String
getQuizCampaign
=
"/quiz/api/v1.0/quiz/%@/"
;
static
const
String
quizSubmitCampaign
=
"/quiz/api/v1.0/quiz/%@/submit/"
;
static
const
String
getCurrentDevice
=
"/user/api/v2.0/devices/current"
;
...
...
@@ -119,4 +121,4 @@ class APIPaths {//sandbox
static
const
String
myProductMarkAsUsed
=
"/myProductMarkAsUsed/1.0.0"
;
static
const
String
myProductMarkAsNotUsedYet
=
"/myProductMarkAsNotUsedYet/1.0.0"
;
static
const
String
submitCampaignViewVoucherComplete
=
"/campaign/api/v3.0/view-voucher/complete"
;
}
\ No newline at end of file
}
lib/core/app_initializer.dart
View file @
f0334970
...
...
@@ -77,9 +77,15 @@ class AppInitializer {
static
Future
<
void
>
_configureWebSdkHeader
()
async
{
try
{
final
response
=
await
webConfigUIApp
({
'mode'
:
'mini'
,
'iconNavigationColor'
:
'#000000'
,
'navigationColor'
:
'#FFFFFF'
,
'iconNavigationPosision'
:
'right'
,
'headerTitle'
:
'MyPoint'
,
'headerColor'
:
'#E71D28'
,
'headerTextColor'
:
'#ffffff'
,
// 'headerSubTitle': 'Tích điểm - đổi quà nhanh chóng',
// 'headerColor': '#ffffff',
// 'headerTextColor': '#000000',
// 'headerIcon': 'https://cdn.mypoint.vn/app_assets/mypoint_icon.png',
});
if
(
response
!=
null
&&
kDebugMode
)
{
print
(
'🧭 x-app-sdk header configured:
$response
'
);
...
...
lib/directional/directional_action_type.dart
View file @
f0334970
...
...
@@ -101,29 +101,6 @@ enum DirectionalScreenName {
linkMBPAccount
,
}
extension
DirectionalScreenRouterExtension
on
DirectionalScreenName
{
String
get
router
{
switch
(
this
)
{
case
DirectionalScreenName
.
setting
:
return
settingScreen
;
case
DirectionalScreenName
.
notifications
:
return
notificationScreen
;
case
DirectionalScreenName
.
home
:
return
mainScreen
;
case
DirectionalScreenName
.
productOwnVoucher
:
return
voucherDetailScreen
;
case
DirectionalScreenName
.
customerSupport
:
return
supportScreen
;
case
DirectionalScreenName
.
pointHunting
:
return
achievementListScreen
;
case
DirectionalScreenName
.
orderMenu
:
return
orderMenuScreen
;
default
:
return
''
;
}
}
}
extension
DirectionalScreenNameExtension
on
DirectionalScreenName
{
String
get
rawValue
{
switch
(
this
)
{
...
...
lib/directional/directional_screen.dart
View file @
f0334970
...
...
@@ -6,6 +6,7 @@ import 'package:mypoint_flutter_app/extensions/string_extension.dart';
import
'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart'
;
import
'package:mypoint_flutter_app/preference/data_preference.dart'
;
import
'package:mypoint_flutter_app/widgets/alert/popup_data_model.dart'
;
import
'package:permission_handler/permission_handler.dart'
;
import
'package:url_launcher/url_launcher.dart'
;
import
'package:uuid/uuid.dart'
;
import
'../configs/constants.dart'
;
...
...
@@ -34,12 +35,11 @@ class DirectionalScreen {
DirectionalScreen
.
_
({
this
.
clickActionType
,
this
.
clickActionParam
,
this
.
popup
});
factory
DirectionalScreen
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
DirectionalScreen
.
_
(
clickActionType:
json
[
'click_action_type'
]
as
String
?,
clickActionParam:
json
[
'click_action_param'
]
as
String
?,
popup:
json
[
'popup'
]
!=
null
?
PopupDataModel
.
fromJson
(
json
[
'popup'
]
as
Map
<
String
,
dynamic
>)
:
null
,
clickActionType:
json
[
Defines
.
actionType
]
as
String
?,
clickActionParam:
json
[
Defines
.
actionParams
]
as
String
?,
);
Map
<
String
,
dynamic
>
toJson
()
=>
{
'click_
action
_t
ype
'
:
clickActionType
,
'click_
action
_p
aram
'
:
clickActionParam
};
Map
<
String
,
dynamic
>
toJson
()
=>
{
Defines
.
action
T
ype
:
clickActionType
,
Defines
.
action
P
aram
s
:
clickActionParam
};
static
DirectionalScreen
?
build
({
String
?
clickActionType
,
String
?
clickActionParam
})
{
if
((
clickActionType
??
""
).
isEmpty
)
return
null
;
...
...
@@ -63,6 +63,10 @@ class DirectionalScreen {
return
false
;
}
switch
(
type
)
{
case
DirectionalScreenName
.
flashSale
:
if
((
clickActionParam
??
''
).
isEmpty
)
return
false
;
Get
.
toNamed
(
flashSaleScreen
,
arguments:
{
"groupId"
:
clickActionParam
});
return
true
;
case
DirectionalScreenName
.
brand
:
if
((
clickActionParam
??
''
).
isEmpty
)
return
false
;
Get
.
toNamed
(
affiliateBrandDetailScreen
,
arguments:
{
"brandId"
:
clickActionParam
});
...
...
@@ -77,19 +81,9 @@ class DirectionalScreen {
final
storeUrl
=
'https://itunes.apple.com/app/id
${Constants.appStoreId}
?action=write-review'
;
openStringUrlExternally
(
storeUrl
);
return
true
;
// case DirectionalScreenName.historyInvitedFriend:
// case DirectionalScreenName.screenAddInvitationCode:
// // TODO: Lịch sử mời bạn – cần màn tương ứng
// return false;
case
DirectionalScreenName
.
rateStorePopup
:
_requestAppReview
();
return
false
;
// return false;
// case DirectionalScreenName.shoppingOnline:
// case DirectionalScreenName.partnerRedirect:
// return false;
// case DirectionalScreenName.brandOffline:
// return false;
case
DirectionalScreenName
.
pipiScreen
:
Get
.
bottomSheet
(
const
PipiDetailScreen
(),
isScrollControlled:
true
,
backgroundColor:
Colors
.
transparent
);
return
true
;
...
...
@@ -158,9 +152,7 @@ class DirectionalScreen {
final
body
=
Uri
.
encodeComponent
(
contentDecoded
);
// iOS: &body=..., Android: ?body=...
final
isIOS
=
defaultTargetPlatform
==
TargetPlatform
.
iOS
;
final
urlStr
=
isIOS
?
'sms:
$phone
&body=
$body
'
:
'sms:
$phone
?body=
$body
'
;
final
urlStr
=
isIOS
?
'sms:
$phone
&body=
$body
'
:
'sms:
$phone
?body=
$body
'
;
final
uri
=
Uri
.
parse
(
urlStr
);
print
(
'Mở SMS:
$uri
phone=
$phone
, content=
$content
'
);
_openUrlExternally
(
uri
);
...
...
@@ -252,7 +244,7 @@ class DirectionalScreen {
case
DirectionalScreenName
.
dailyCheckin
||
DirectionalScreenName
.
dailyCheckinScreen
:
Get
.
toNamed
(
dailyCheckInScreen
);
return
true
;
case
DirectionalScreenName
.
favorite
:
case
DirectionalScreenName
.
favorite
||
DirectionalScreenName
.
productVoucherLike
:
Get
.
toNamed
(
vouchersScreen
,
arguments:
{
"favorite"
:
true
});
return
true
;
case
DirectionalScreenName
.
transactionHistories
:
...
...
@@ -315,12 +307,69 @@ class DirectionalScreen {
if
((
clickActionParam
??
''
).
isEmpty
)
return
false
;
_handleLinkMBPAccount
();
return
true
;
case
DirectionalScreenName
.
notifications
:
Get
.
toNamed
(
notificationScreen
);
return
true
;
case
DirectionalScreenName
.
notification
:
if
((
clickActionParam
??
''
).
isEmpty
)
{
final
title
=
(
extraData
?[
'title'
]
as
String
?).
orEmpty
;
final
body
=
(
extraData
?[
'body'
]
as
String
?).
orEmpty
;
if
(
title
.
isNotEmpty
&&
body
.
isNotEmpty
)
{
Get
.
toNamed
(
notificationDetailScreen
,
arguments:
{
"title"
:
title
,
"body"
:
body
});
return
true
;
}
return
false
;
}
Get
.
toNamed
(
notificationDetailScreen
,
arguments:
{
"notificationId"
:
clickActionParam
});
return
true
;
case
DirectionalScreenName
.
detailTrafficService
:
if
((
clickActionParam
??
''
).
isEmpty
)
return
false
;
Get
.
toNamed
(
trafficServiceDetailScreen
,
arguments:
{
"serviceId"
:
clickActionParam
});
return
true
;
case
DirectionalScreenName
.
appScreen
:
if
((
clickActionParam
??
''
).
isEmpty
)
return
false
;
final
direction
=
DirectionalScreen
.
build
(
clickActionType:
clickActionParam
);
direction
?.
begin
();
return
true
;
case
DirectionalScreenName
.
transaction
:
if
((
clickActionParam
??
''
).
isEmpty
)
return
false
;
Get
.
toNamed
(
transactionHistoryDetailScreen
,
arguments:
{
"orderId"
:
clickActionParam
});
return
true
;
case
DirectionalScreenName
.
applicationSetting
:
openAppSettings
();
return
true
;
case
DirectionalScreenName
.
cashBackPointPartnerDetail
:
if
((
clickActionParam
??
''
).
isEmpty
)
return
false
;
Get
.
toNamed
(
affiliateBrandDetailScreen
,
arguments:
{
"brandId"
:
clickActionParam
});
return
true
;
case
DirectionalScreenName
.
viewGift
||
DirectionalScreenName
.
feedback
||
DirectionalScreenName
.
ranking
||
DirectionalScreenName
.
inputReferralCode
||
DirectionalScreenName
.
shoppingOnline
||
DirectionalScreenName
.
partnerRedirect
||
DirectionalScreenName
.
brandOffline
||
DirectionalScreenName
.
customerTransferPoint
||
DirectionalScreenName
.
home
||
DirectionalScreenName
.
brandList
||
DirectionalScreenName
.
brandLike
||
DirectionalScreenName
.
register
||
DirectionalScreenName
.
walkingCampaign
||
DirectionalScreenName
.
gameWorldCup2022
||
DirectionalScreenName
.
vietlott
||
DirectionalScreenName
.
unknown
:
_logUnsupported
(
type
);
return
false
;
default
:
print
(
"Không nhận diện được action type:
$clickActionType
"
);
return
false
;
}
}
void
_logUnsupported
(
DirectionalScreenName
type
)
{
print
(
"⚠️ Chưa hỗ trợ điều hướng action type:
${type.rawValue}
"
);
}
void
_handleLinkMBPAccount
()
{
final
phone
=
clickActionParam
.
orEmpty
;
if
(
phone
.
isEmpty
)
return
;
...
...
@@ -357,6 +406,13 @@ class DirectionalScreen {
await
DataPreference
.
instance
.
clearData
();
Get
.
offAllNamed
(
loginScreen
,
arguments:
{
"phone"
:
phone
,
'password'
:
password
});
}
Future
<
void
>
openSystemSettings
()
async
{
final
opened
=
await
openAppSettings
();
if
(!
opened
)
{
debugPrint
(
'⚠️ Không mở được trang cài đặt hệ thống'
);
}
}
}
Future
<
bool
>
forceOpen
({
required
Uri
url
,
LaunchMode
mode
=
LaunchMode
.
platformDefault
})
async
{
...
...
@@ -405,10 +461,7 @@ Future<bool> _safeOpenUrl(Uri url, {LaunchMode preferred = LaunchMode.platformDe
final
ok
=
await
launchUrl
(
url
,
mode:
mode
,
webViewConfiguration:
const
WebViewConfiguration
(
enableJavaScript:
true
,
headers:
<
String
,
String
>{},
),
webViewConfiguration:
const
WebViewConfiguration
(
enableJavaScript:
true
,
headers:
<
String
,
String
>{}),
);
if
(
ok
)
return
true
;
}
catch
(
_
)
{
...
...
lib/extensions/num_extension.dart
View file @
f0334970
...
...
@@ -28,7 +28,7 @@ extension NumExtension on num {
}
enum
CurrencyUnit
{
vnd
(
'
đ'
),
vnd
(
'đ'
),
usd
(
' USD'
),
eur
(
' EUR'
),
none
(
' '
),
...
...
lib/firebase/push_notification.dart
View file @
f0334970
...
...
@@ -74,16 +74,22 @@ class PushNotification {
if
(
kDebugMode
)
print
(
'From info - name:
$name
, param:
$param
'
);
}
DirectionalScreen
?
screen
;
if
(
name
!=
null
||
param
!=
null
)
{
if
(
kDebugMode
)
print
(
'Building DirectionalScreen with name:
$name
, param:
$param
'
);
return
DirectionalScreen
.
build
(
clickActionType:
name
,
clickActionParam:
param
);
screen
=
DirectionalScreen
.
build
(
clickActionType:
name
,
clickActionParam:
param
);
}
if
(
kDebugMode
)
{
print
(
'No action data found, using default notification screen with ID:
$id
'
);
print
(
'Title:
$title
, Body:
$body
'
);
}
return
DirectionalScreen
.
buildByName
(
name:
DirectionalScreenName
.
notification
,
clickActionParam:
id
);
screen
??=
DirectionalScreen
.
buildByName
(
name:
DirectionalScreenName
.
notification
,
clickActionParam:
id
);
screen
?.
extraData
=
{
'title'
:
title
,
'body'
:
body
,
};
return
screen
;
// TODO handel title + body
}
...
...
lib/networking/restful_api_client_all_request.dart
View file @
f0334970
...
...
@@ -36,6 +36,8 @@ import '../screen/device_manager/device_manager_model.dart';
import
'../screen/electric_payment/models/customer_contract_object_model.dart'
;
import
'../screen/electric_payment/models/electric_payment_response_model.dart'
;
import
'../screen/faqs/faqs_model.dart'
;
import
'../screen/flash_sale/models/flash_sale_category_model.dart'
;
import
'../screen/flash_sale/models/flash_sale_detail_response.dart'
;
import
'../screen/game/models/game_bundle_item_model.dart'
;
import
'../screen/history_point_cashback/models/history_point_cashback_model.dart'
;
import
'../screen/home/models/achievement_model.dart'
;
...
...
@@ -886,6 +888,51 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient {
});
}
Future
<
BaseResponseModel
<
List
<
FlashSaleCategoryModel
>>>
getFlashSaleCategories
(
String
groupId
)
async
{
final
path
=
APIPaths
.
getFlashSaleGroup
.
replaceFirst
(
'%@'
,
groupId
);
return
requestNormal
(
path
,
Method
.
GET
,
const
{},
(
data
)
{
if
(
data
is
List
)
{
return
data
.
whereType
<
Map
<
String
,
dynamic
>>().
map
(
FlashSaleCategoryModel
.
fromJson
).
toList
();
}
if
(
data
is
Map
<
String
,
dynamic
>)
{
final
categories
=
data
[
'categories'
]
??
data
[
'data'
]
??
data
[
'items'
];
if
(
categories
is
List
)
{
return
categories
.
whereType
<
Map
<
String
,
dynamic
>>().
map
(
FlashSaleCategoryModel
.
fromJson
).
toList
();
}
if
(
data
.
containsKey
(
'_id'
))
{
return
[
FlashSaleCategoryModel
.
fromJson
(
Map
<
String
,
dynamic
>.
from
(
data
))];
}
}
return
<
FlashSaleCategoryModel
>[];
});
}
Future
<
BaseResponseModel
<
FlashSaleDetailResponse
>>
getFlashSaleDetail
({
required
String
groupId
,
int
index
=
0
,
int
size
=
10
,
int
?
categoryId
,
})
async
{
final
path
=
APIPaths
.
getFlashSaleDetail
.
replaceFirst
(
'%@'
,
groupId
);
final
params
=
<
String
,
dynamic
>{
'index'
:
index
,
'size'
:
size
,
if
(
categoryId
!=
null
)
'category_id'
:
categoryId
,
};
return
requestNormal
(
path
,
Method
.
GET
,
params
,
(
data
)
{
if
(
data
is
Map
<
String
,
dynamic
>)
{
return
FlashSaleDetailResponse
.
fromJson
(
data
);
}
if
(
data
is
List
&&
data
.
isNotEmpty
)
{
final
first
=
data
.
first
;
if
(
first
is
Map
<
String
,
dynamic
>)
{
return
FlashSaleDetailResponse
.
fromJson
(
first
);
}
}
return
FlashSaleDetailResponse
(
products:
<
ProductModel
>[]);
});
}
Future
<
BaseResponseModel
<
DeviceItemModel
>>
getCurrentDevice
()
async
{
return
requestNormal
(
APIPaths
.
getCurrentDevice
,
Method
.
GET
,
{},
(
data
)
{
return
DeviceItemModel
.
fromJson
(
data
as
Json
);
...
...
@@ -1079,4 +1126,4 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient {
return
SubmitViewVoucherCompletedResponse
.
fromJson
(
data
as
Json
);
});
}
}
\ No newline at end of file
}
lib/screen/flash_sale/flash_sale_screen.dart
0 → 100644
View file @
f0334970
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/base/base_screen.dart'
;
import
'package:mypoint_flutter_app/base/basic_state.dart'
;
import
'package:mypoint_flutter_app/extensions/num_extension.dart'
;
import
'package:mypoint_flutter_app/screen/flash_sale/flash_sale_viewmodel.dart'
;
import
'package:mypoint_flutter_app/screen/flash_sale/models/flash_sale_category_model.dart'
;
import
'package:mypoint_flutter_app/screen/voucher/models/product_model.dart'
;
import
'package:mypoint_flutter_app/widgets/custom_empty_widget.dart'
;
import
'package:mypoint_flutter_app/widgets/image_loader.dart'
;
import
'../../shared/router_gage.dart'
;
import
'../../widgets/custom_navigation_bar.dart'
;
class
FlashSaleScreen
extends
BaseScreen
{
const
FlashSaleScreen
({
super
.
key
});
@override
State
<
StatefulWidget
>
createState
()
=>
_FlashSaleScreenState
();
}
class
_FlashSaleScreenState
extends
BaseState
<
FlashSaleScreen
>
with
BasicState
{
late
final
FlashSaleViewModel
_viewModel
;
@override
void
initState
()
{
super
.
initState
();
final
args
=
Get
.
arguments
;
final
dynamic
rawId
=
args
is
Map
?
(
args
[
'groupId'
]
??
args
[
'id'
])
:
args
;
final
String
groupId
=
(
rawId
==
null
||
rawId
.
toString
().
isEmpty
)
?
'294'
:
rawId
.
toString
();
_viewModel
=
Get
.
put
(
FlashSaleViewModel
(
groupId:
groupId
));
_viewModel
.
onShowAlertError
??=
(
message
)
=>
showAlertError
(
content:
message
);
}
@override
Widget
createBody
()
{
return
Scaffold
(
backgroundColor:
Colors
.
grey
[
200
],
appBar:
const
CustomNavigationBar
(
title:
'Flash Sale'
),
body:
NotificationListener
<
ScrollNotification
>(
onNotification:
(
notification
)
{
if
(
notification
is
ScrollUpdateNotification
&&
notification
.
metrics
.
axis
==
Axis
.
vertical
&&
notification
.
metrics
.
pixels
>=
notification
.
metrics
.
maxScrollExtent
-
200
)
{
_viewModel
.
loadMore
();
}
return
false
;
},
child:
RefreshIndicator
(
onRefresh:
_viewModel
.
refresh
,
child:
CustomScrollView
(
physics:
const
AlwaysScrollableScrollPhysics
(),
slivers:
[
SliverToBoxAdapter
(
child:
Obx
(()
=>
_buildCategorySection
())),
SliverToBoxAdapter
(
child:
Obx
(()
=>
_buildCountdownSection
())),
Obx
(()
=>
_buildProductSliver
()),
],
),
),
),
);
}
Widget
_buildCategorySection
()
{
final
categories
=
_viewModel
.
categories
;
if
(
categories
.
isEmpty
)
return
const
SizedBox
.
shrink
();
final
selectedId
=
_viewModel
.
selectedCategoryId
.
value
??
FlashSaleViewModel
.
allCategory
.
id
;
return
Container
(
color:
Colors
.
white
,
height:
72
,
child:
ListView
.
separated
(
scrollDirection:
Axis
.
horizontal
,
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
8
),
itemCount:
categories
.
length
,
separatorBuilder:
(
_
,
_
)
=>
const
SizedBox
(
width:
8
),
itemBuilder:
(
_
,
index
)
{
final
category
=
categories
[
index
];
final
bool
isSelected
=
selectedId
==
category
.
id
;
return
_buildCategoryChip
(
category
,
isSelected
);
},
),
);
}
Widget
_buildCategoryChip
(
FlashSaleCategoryModel
category
,
bool
isSelected
)
{
final
Color
accent
=
isSelected
?
const
Color
(
0xFFE53935
)
:
const
Color
(
0xFFE0E0E0
);
final
Color
textColor
=
isSelected
?
const
Color
(
0xFFE53935
)
:
Colors
.
black87
;
return
GestureDetector
(
onTap:
()
=>
_viewModel
.
onCategorySelected
(
category
),
child:
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
8
),
decoration:
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
circular
(
8
),
border:
Border
.
all
(
color:
accent
,
width:
1
),
),
child:
Center
(
child:
Text
(
category
.
name
??
''
,
style:
TextStyle
(
color:
textColor
,
fontWeight:
FontWeight
.
w600
)),
),
),
);
}
Widget
_buildCountdownSection
()
{
final
sale
=
_viewModel
.
products
.
firstOrNull
?.
previewFlashSale
;
if
(
sale
==
null
)
return
const
SizedBox
.
shrink
();
final
duration
=
_viewModel
.
remaining
.
value
;
final
bool
isCounting
=
duration
.
inSeconds
>
0
;
final
label
=
(
sale
.
desTime
??
'Kết thúc trong'
).
toUpperCase
();
return
Container
(
padding:
const
EdgeInsets
.
only
(
left:
16
,
right:
16
,
top:
12
,
bottom:
0
),
child:
Row
(
children:
[
const
Spacer
(),
Text
(
label
,
style:
const
TextStyle
(
fontSize:
14
,
fontWeight:
FontWeight
.
w700
,
color:
Colors
.
black54
)),
const
SizedBox
(
width:
12
),
_buildTimeChip
(
_formatTwoDigits
(
duration
.
inHours
)),
const
SizedBox
(
width:
4
),
const
Text
(
':'
,
style:
TextStyle
(
fontWeight:
FontWeight
.
bold
)),
const
SizedBox
(
width:
4
),
_buildTimeChip
(
_formatTwoDigits
(
duration
.
inMinutes
.
remainder
(
60
))),
const
SizedBox
(
width:
4
),
const
Text
(
':'
,
style:
TextStyle
(
fontWeight:
FontWeight
.
bold
)),
const
SizedBox
(
width:
4
),
_buildTimeChip
(
_formatTwoDigits
(
duration
.
inSeconds
.
remainder
(
60
))),
if
(!
isCounting
)
const
Text
(
'Đã kết thúc'
,
style:
TextStyle
(
fontSize:
12
,
color:
Colors
.
redAccent
,
fontWeight:
FontWeight
.
w600
),
),
],
),
);
}
Widget
_buildTimeChip
(
String
value
)
{
return
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
6
,
vertical:
4
),
decoration:
BoxDecoration
(
color:
const
Color
(
0xFF212121
),
borderRadius:
BorderRadius
.
circular
(
6
)),
child:
Text
(
value
,
style:
const
TextStyle
(
color:
Colors
.
white
,
fontWeight:
FontWeight
.
bold
)),
);
}
Widget
_buildProductSliver
()
{
final
products
=
_viewModel
.
products
;
if
(
products
.
isEmpty
)
{
return
const
SliverFillRemaining
(
hasScrollBody:
false
,
child:
EmptyWidget
());
}
final
double
screenWidth
=
MediaQuery
.
of
(
context
).
size
.
width
;
final
double
itemWidth
=
(
screenWidth
-
36
)
/
2
;
final
bool
showProgress
=
products
.
firstOrNull
?.
isShowProsessSoldItem
??
false
;
// 16 space top-bottom content
// 24 height price
// 48 height name
final
double
itemHeight
=
itemWidth
*
9
/
16
+
16
+
24
+
48
+
(
showProgress
?
24
:
0
);
return
SliverPadding
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
12
),
sliver:
SliverGrid
(
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount
(
crossAxisCount:
2
,
mainAxisSpacing:
12
,
crossAxisSpacing:
12
,
childAspectRatio:
itemWidth
/
itemHeight
,
),
delegate:
SliverChildBuilderDelegate
(
(
context
,
index
)
=>
_buildProductCard
(
products
[
index
]),
childCount:
products
.
length
,
),
),
);
}
Widget
_buildProductCard
(
ProductModel
product
)
{
final
percent
=
product
.
percentDiscount
;
final
bool
showProgress
=
product
.
isShowProsessSoldItem
;
return
GestureDetector
(
onTap:
()
=>
Get
.
toNamed
(
voucherDetailScreen
,
arguments:
{
"productId"
:
product
.
id
}),
child:
Container
(
decoration:
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
circular
(
16
),
boxShadow:
[
BoxShadow
(
color:
Colors
.
black
.
withOpacity
(
0.04
),
blurRadius:
8
,
offset:
const
Offset
(
0
,
4
))],
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
ClipRRect
(
borderRadius:
const
BorderRadius
.
vertical
(
top:
Radius
.
circular
(
16
)),
child:
AspectRatio
(
aspectRatio:
16
/
9
,
child:
Stack
(
fit:
StackFit
.
expand
,
children:
[
loadNetworkImage
(
url:
product
.
banner
?.
url
??
''
,
fit:
BoxFit
.
cover
,
placeholderAsset:
'assets/images/bg_default_169.png'
,
),
if
(
percent
!=
null
)
Positioned
(
right:
8
,
bottom:
8
,
child:
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
8
,
vertical:
4
),
decoration:
BoxDecoration
(
color:
const
Color
(
0xFFE53935
),
borderRadius:
BorderRadius
.
circular
(
20
),
),
child:
Text
(
'-
$percent
%'
,
style:
const
TextStyle
(
color:
Colors
.
white
,
fontWeight:
FontWeight
.
bold
,
fontSize:
12
),
),
),
),
],
),
),
),
Expanded
(
child:
Padding
(
padding:
const
EdgeInsets
.
all
(
8
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Row
(
children:
[
Image
.
asset
(
'assets/images/ic_hot_flash_sale.png'
,
width:
20
,
height:
20
),
const
SizedBox
(
width:
4
),
Text
(
product
.
amountToBePaid
?.
money
(
CurrencyUnit
.
vnd
)
??
''
,
style:
const
TextStyle
(
fontSize:
16
,
fontWeight:
FontWeight
.
w700
,
color:
Colors
.
black87
),
),
if
((
product
.
price
?.
value
??
0
)
>
0
)
Padding
(
padding:
const
EdgeInsets
.
only
(
left:
4
),
child:
Text
(
'
${product.price?.value?.money(CurrencyUnit.noneSpace)}
đ'
,
style:
const
TextStyle
(
fontSize:
14
,
color:
Colors
.
grey
,
decoration:
TextDecoration
.
lineThrough
,
),
),
),
],
),
const
SizedBox
(
height:
6
),
Text
(
product
.
name
??
''
,
maxLines:
2
,
overflow:
TextOverflow
.
ellipsis
,
style:
const
TextStyle
(
fontSize:
14
,
fontWeight:
FontWeight
.
w600
),
),
if
(
showProgress
)
...[
const
SizedBox
(
height:
6
),
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
SizedBox
(
width:
100
,
child:
ClipRRect
(
borderRadius:
BorderRadius
.
circular
(
4
),
child:
LinearProgressIndicator
(
value:
product
.
progress
,
minHeight:
6
,
backgroundColor:
Colors
.
grey
.
shade300
,
valueColor:
AlwaysStoppedAnimation
<
Color
>(
product
.
inStock
?
Colors
.
orange
:
Colors
.
red
),
),
),
),
const
SizedBox
(
width:
2
),
Text
(
product
.
inStock
?
'Đã bán
${product.previewFlashSale?.fsQuantitySold ?? 0}
'
:
'Đã bán hết'
,
style:
TextStyle
(
fontSize:
12
,
color:
product
.
inStock
?
Colors
.
black
:
Colors
.
grey
),
),
],
),
],
],
),
),
),
],
),
),
);
}
String
_formatTwoDigits
(
int
value
)
=>
value
.
toString
().
padLeft
(
2
,
'0'
);
}
lib/screen/flash_sale/flash_sale_viewmodel.dart
0 → 100644
View file @
f0334970
import
'dart:async'
;
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_viewmodel.dart'
;
import
'package:mypoint_flutter_app/screen/flash_sale/models/flash_sale_category_model.dart'
;
import
'package:mypoint_flutter_app/screen/flash_sale/models/flash_sale_detail_response.dart'
;
import
'package:mypoint_flutter_app/screen/voucher/models/product_model.dart'
;
class
FlashSaleViewModel
extends
RestfulApiViewModel
{
final
String
groupId
;
FlashSaleViewModel
({
required
this
.
groupId
});
static
const
FlashSaleCategoryModel
allCategory
=
FlashSaleCategoryModel
(
id:
-
1
,
name:
'Tất cả'
);
final
RxList
<
FlashSaleCategoryModel
>
categories
=
<
FlashSaleCategoryModel
>[].
obs
;
final
RxnInt
selectedCategoryId
=
RxnInt
();
final
RxList
<
ProductModel
>
products
=
<
ProductModel
>[].
obs
;
final
Rx
<
FlashSaleDetailResponse
?>
flashSale
=
Rx
<
FlashSaleDetailResponse
?>(
null
);
final
RxBool
hasMore
=
true
.
obs
;
final
Rx
<
Duration
>
remaining
=
const
Duration
().
obs
;
void
Function
(
String
message
)?
onShowAlertError
;
Timer
?
_timer
;
int
_offset
=
0
;
final
int
_pageSize
=
20
;
@override
void
onInit
()
{
super
.
onInit
();
_loadInitialData
();
}
Future
<
void
>
_loadInitialData
({
bool
withLoading
=
true
})
async
{
if
(
withLoading
)
showLoading
();
await
fetchCategories
();
await
loadProducts
(
reset:
true
);
if
(
withLoading
)
hideLoading
();
}
Future
<
void
>
fetchCategories
()
async
{
await
callApi
<
List
<
FlashSaleCategoryModel
>>(
withLoading:
false
,
request:
()
=>
client
.
getFlashSaleCategories
(
groupId
),
onSuccess:
(
data
,
_
)
{
final
extended
=
<
FlashSaleCategoryModel
>[
allCategory
,
...
data
];
categories
.
assignAll
(
extended
);
selectedCategoryId
.
value
??=
allCategory
.
id
;
},
onFailure:
(
_
,
_
,
_
)
{
if
(
categories
.
isEmpty
)
{
categories
.
assignAll
(
const
[
allCategory
]);
selectedCategoryId
.
value
=
allCategory
.
id
;
}
},
);
}
Future
<
void
>
loadProducts
({
bool
reset
=
false
})
async
{
if
(
reset
)
{
_offset
=
0
;
hasMore
.
value
=
true
;
}
else
{
_offset
=
products
.
length
;
}
final
categoryId
=
selectedCategoryId
.
value
;
final
int
?
categoryParam
=
(
categoryId
!=
null
&&
categoryId
!=
allCategory
.
id
)
?
categoryId
:
null
;
await
callApi
<
FlashSaleDetailResponse
>(
withLoading:
false
,
request:
()
=>
client
.
getFlashSaleDetail
(
groupId:
groupId
,
index:
_offset
,
size:
_pageSize
,
categoryId:
categoryParam
),
onSuccess:
(
data
,
_
)
{
final
fetched
=
data
.
products
??
<
ProductModel
>[];
if
(
reset
)
{
products
.
assignAll
(
fetched
);
}
else
{
products
.
addAll
(
fetched
);
}
hasMore
.
value
=
fetched
.
length
>=
_pageSize
;
flashSale
.
value
=
data
;
_restartTimer
();
},
onFailure:
(
message
,
_
,
_
)
{
if
(
reset
)
{
products
.
clear
();
}
hasMore
.
value
=
false
;
onShowAlertError
?.
call
(
message
);
},
);
}
Future
<
void
>
refresh
()
async
{
await
loadProducts
(
reset:
true
);
}
void
onCategorySelected
(
FlashSaleCategoryModel
category
)
{
if
(
selectedCategoryId
.
value
==
category
.
id
)
return
;
selectedCategoryId
.
value
=
category
.
id
;
loadProducts
(
reset:
true
);
}
void
loadMore
()
{
if
(!
hasMore
.
value
)
return
;
loadProducts
(
reset:
false
);
}
void
_restartTimer
()
{
final
info
=
products
.
firstOrNull
?.
previewFlashSale
;
_timer
?.
cancel
();
final
target
=
info
;
final
int
seconds
=
target
?.
countdownLocal
?.
inSeconds
??
target
?.
countdownSecond
??
0
;
if
(
seconds
<=
0
)
{
remaining
.
value
=
Duration
.
zero
;
return
;
}
remaining
.
value
=
Duration
(
seconds:
seconds
);
_timer
=
Timer
.
periodic
(
const
Duration
(
seconds:
1
),
(
timer
)
{
final
current
=
remaining
.
value
;
if
(
current
.
inSeconds
<=
1
)
{
remaining
.
value
=
Duration
.
zero
;
timer
.
cancel
();
_loadInitialData
(
withLoading:
false
);
return
;
}
remaining
.
value
=
Duration
(
seconds:
current
.
inSeconds
-
1
);
});
}
@override
void
onClose
()
{
_timer
?.
cancel
();
super
.
onClose
();
}
}
lib/screen/flash_sale/models/flash_sale_category_model.dart
0 → 100644
View file @
f0334970
import
'package:json_annotation/json_annotation.dart'
;
part
'flash_sale_category_model.g.dart'
;
@JsonSerializable
()
class
FlashSaleCategoryModel
{
@JsonKey
(
name:
'_id'
)
final
int
id
;
final
String
?
name
;
@JsonKey
(
name:
'code'
)
final
String
?
code
;
const
FlashSaleCategoryModel
({
required
this
.
id
,
this
.
name
,
this
.
code
});
factory
FlashSaleCategoryModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$FlashSaleCategoryModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$FlashSaleCategoryModelToJson
(
this
);
}
lib/screen/flash_sale/models/flash_sale_category_model.g.dart
0 → 100644
View file @
f0334970
// GENERATED CODE - DO NOT MODIFY BY HAND
part of
'flash_sale_category_model.dart'
;
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
FlashSaleCategoryModel
_$FlashSaleCategoryModelFromJson
(
Map
<
String
,
dynamic
>
json
,
)
=>
FlashSaleCategoryModel
(
id:
(
json
[
'_id'
]
as
num
).
toInt
(),
name:
json
[
'name'
]
as
String
?,
code:
json
[
'code'
]
as
String
?,
);
Map
<
String
,
dynamic
>
_$FlashSaleCategoryModelToJson
(
FlashSaleCategoryModel
instance
,
)
=>
<
String
,
dynamic
>{
'_id'
:
instance
.
id
,
'name'
:
instance
.
name
,
'code'
:
instance
.
code
,
};
lib/screen/flash_sale/models/flash_sale_detail_response.dart
0 → 100644
View file @
f0334970
import
'package:json_annotation/json_annotation.dart'
;
import
'package:mypoint_flutter_app/screen/flash_sale/models/preview_flash_sale_model.dart'
;
import
'package:mypoint_flutter_app/screen/voucher/models/product_model.dart'
;
part
'flash_sale_detail_response.g.dart'
;
@JsonSerializable
()
class
FlashSaleDetailResponse
{
final
int
?
id
;
@JsonKey
(
name:
'flash_sale'
)
final
PreviewFlashSale
?
flashSale
;
final
List
<
ProductModel
>?
products
;
final
String
?
name
;
@JsonKey
(
name:
'countdown_second'
)
final
int
?
countdownSecond
;
@JsonKey
(
name:
'header_img'
)
final
String
?
headerImg
;
@JsonKey
(
name:
'start_time'
)
final
String
?
startTime
;
@JsonKey
(
name:
'end_time'
)
final
String
?
endTime
;
@JsonKey
(
name:
'is_reward'
)
final
bool
?
isReward
;
FlashSaleDetailResponse
({
this
.
id
,
this
.
flashSale
,
this
.
products
,
this
.
name
,
this
.
countdownSecond
,
this
.
headerImg
,
this
.
startTime
,
this
.
endTime
,
this
.
isReward
,
});
factory
FlashSaleDetailResponse
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$FlashSaleDetailResponseFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$FlashSaleDetailResponseToJson
(
this
);
}
lib/screen/flash_sale/models/flash_sale_detail_response.g.dart
0 → 100644
View file @
f0334970
// GENERATED CODE - DO NOT MODIFY BY HAND
part of
'flash_sale_detail_response.dart'
;
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
FlashSaleDetailResponse
_$FlashSaleDetailResponseFromJson
(
Map
<
String
,
dynamic
>
json
,
)
=>
FlashSaleDetailResponse
(
id:
(
json
[
'id'
]
as
num
?)?.
toInt
(),
flashSale:
json
[
'flash_sale'
]
==
null
?
null
:
PreviewFlashSale
.
fromJson
(
json
[
'flash_sale'
]
as
Map
<
String
,
dynamic
>,
),
products:
(
json
[
'products'
]
as
List
<
dynamic
>?)
?.
map
((
e
)
=>
ProductModel
.
fromJson
(
e
as
Map
<
String
,
dynamic
>))
.
toList
(),
name:
json
[
'name'
]
as
String
?,
countdownSecond:
(
json
[
'countdown_second'
]
as
num
?)?.
toInt
(),
headerImg:
json
[
'header_img'
]
as
String
?,
startTime:
json
[
'start_time'
]
as
String
?,
endTime:
json
[
'end_time'
]
as
String
?,
isReward:
json
[
'is_reward'
]
as
bool
?,
);
Map
<
String
,
dynamic
>
_$FlashSaleDetailResponseToJson
(
FlashSaleDetailResponse
instance
,
)
=>
<
String
,
dynamic
>{
'id'
:
instance
.
id
,
'flash_sale'
:
instance
.
flashSale
,
'products'
:
instance
.
products
,
'name'
:
instance
.
name
,
'countdown_second'
:
instance
.
countdownSecond
,
'header_img'
:
instance
.
headerImg
,
'start_time'
:
instance
.
startTime
,
'end_time'
:
instance
.
endTime
,
'is_reward'
:
instance
.
isReward
,
};
lib/screen/flash_sale/preview_flash_sale_model.dart
→
lib/screen/flash_sale/
models/
preview_flash_sale_model.dart
View file @
f0334970
...
...
@@ -2,6 +2,7 @@ import 'package:json_annotation/json_annotation.dart';
import
'package:intl/intl.dart'
;
part
'preview_flash_sale_model.g.dart'
;
@JsonSerializable
()
class
PreviewFlashSale
{
final
int
?
id
;
...
...
@@ -126,7 +127,7 @@ class PreviewFlashSale {
DateTime
?
_parseDate
(
String
?
dateStr
)
{
if
(
dateStr
==
null
)
return
null
;
try
{
return
DateFormat
(
"yyyy-MM-dd
'T'
HH:mm:ss"
).
parse
(
dateStr
);
return
DateFormat
(
"yyyy-MM-dd
HH:mm:ss"
).
parse
(
dateStr
);
}
catch
(
_
)
{
return
null
;
}
...
...
lib/screen/flash_sale/preview_flash_sale_model.g.dart
→
lib/screen/flash_sale/
models/
preview_flash_sale_model.g.dart
View file @
f0334970
File moved
lib/screen/home/custom_widget/brand_grid_widget.dart
View file @
f0334970
...
...
@@ -12,7 +12,7 @@ class BrandGridWidget extends StatelessWidget {
const
BrandGridWidget
({
super
.
key
,
required
this
.
brands
,
this
.
sectionConfig
,
this
.
onTap
});
_handleTapRightButton
()
{
void
_handleTapRightButton
()
{
sectionConfig
?.
buttonViewAll
?.
directionalScreen
?.
begin
();
}
...
...
lib/screen/home/custom_widget/flash_sale_carousel_widget.dart
View file @
f0334970
import
'package:flutter/material.dart'
;
import
'package:mypoint_flutter_app/extensions/num_extension.dart'
;
import
'package:mypoint_flutter_app/widgets/image_loader.dart'
;
import
'../../voucher/models/product_model.dart'
;
import
'../../voucher/sub_widget/voucher_section_title.dart'
;
import
'../models/main_section_config_model.dart'
;
...
...
@@ -9,15 +10,22 @@ class FlashSaleCarouselWidget extends StatefulWidget {
final
List
<
ProductModel
>
products
;
final
MainSectionConfigModel
?
sectionConfig
;
final
void
Function
(
ProductModel
)?
onTap
;
final
VoidCallback
?
onCountdownFinished
;
const
FlashSaleCarouselWidget
({
super
.
key
,
required
this
.
products
,
this
.
sectionConfig
,
this
.
onTap
});
const
FlashSaleCarouselWidget
({
super
.
key
,
required
this
.
products
,
this
.
sectionConfig
,
this
.
onTap
,
this
.
onCountdownFinished
,
});
@override
State
<
FlashSaleCarouselWidget
>
createState
()
=>
_FlashSaleCarouselWidgetState
();
}
class
_FlashSaleCarouselWidgetState
extends
State
<
FlashSaleCarouselWidget
>
{
_handleTapRightButton
()
{
void
_handleTapRightButton
()
{
widget
.
sectionConfig
?.
buttonViewAll
?.
directionalScreen
?.
begin
();
}
...
...
@@ -25,13 +33,15 @@ class _FlashSaleCarouselWidgetState extends State<FlashSaleCarouselWidget> {
Widget
build
(
BuildContext
context
)
{
final
widthItem
=
MediaQuery
.
of
(
context
).
size
.
width
/
2.5
;
final
heightItem
=
widthItem
*
9
/
16
+
(
widget
.
products
.
firstOrNull
?.
extendSpaceFlashSaleItem
??
130
);
final
flashSale
=
widget
.
products
.
firstOrNull
?.
previewFlashSale
;
if
(
widget
.
products
.
isEmpty
)
return
const
SizedBox
.
shrink
();
return
Column
(
children:
[
if
(
widget
.
sectionConfig
?.
flashSale
!=
null
)
if
(
flashSale
!=
null
)
FlashSaleHeader
(
flashSale:
widget
.
sectionConfig
?.
flashSale
,
flashSale:
flashSale
,
onViewAll:
widget
.
sectionConfig
?.
buttonViewAll
?.
directionalScreen
!=
null
?
_handleTapRightButton
:
null
,
onCountdownFinished:
widget
.
onCountdownFinished
,
),
const
SizedBox
(
height:
8
),
ConstrainedBox
(
...
...
@@ -41,7 +51,7 @@ class _FlashSaleCarouselWidgetState extends State<FlashSaleCarouselWidget> {
scrollDirection:
Axis
.
horizontal
,
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
),
itemCount:
widget
.
products
.
length
,
separatorBuilder:
(
_
,
_
_
)
=>
const
SizedBox
(
width:
12
),
separatorBuilder:
(
_
,
_
)
=>
const
SizedBox
(
width:
12
),
itemBuilder:
(
context
,
index
)
=>
_buildFlashSaleGridItem
(
widget
.
products
[
index
]),
),
),
...
...
@@ -69,11 +79,12 @@ class _FlashSaleCarouselWidgetState extends State<FlashSaleCarouselWidget> {
children:
[
ClipRRect
(
borderRadius:
BorderRadius
.
only
(
topLeft:
Radius
.
circular
(
8
),
topRight:
Radius
.
circular
(
8
)),
child:
Image
.
n
etwork
(
product
.
banner
?.
url
??
''
,
child:
loadN
etwork
Image
(
url:
product
.
banner
?.
url
??
''
,
width:
double
.
infinity
,
height:
widthItem
*
9
/
16
,
fit:
BoxFit
.
cover
,
placeholderAsset:
'assets/images/bg_default_169.png'
,
),
),
if
(
product
.
percentDiscount
!=
null
)
...
...
lib/screen/home/custom_widget/flash_sale_header_widget.dart
View file @
f0334970
import
'dart:async'
;
import
'package:flutter/material.dart'
;
import
'../../flash_sale/preview_flash_sale_model.dart'
;
import
'../../flash_sale/
models/
preview_flash_sale_model.dart'
;
class
FlashSaleHeader
extends
StatefulWidget
{
final
PreviewFlashSale
?
flashSale
;
final
VoidCallback
?
onViewAll
;
final
VoidCallback
?
onCountdownFinished
;
const
FlashSaleHeader
({
super
.
key
,
required
this
.
flashSale
,
this
.
onViewAll
,
});
const
FlashSaleHeader
({
super
.
key
,
required
this
.
flashSale
,
this
.
onViewAll
,
this
.
onCountdownFinished
});
@override
State
<
FlashSaleHeader
>
createState
()
=>
_FlashSaleHeaderState
();
...
...
@@ -19,19 +16,29 @@ class FlashSaleHeader extends StatefulWidget {
class
_FlashSaleHeaderState
extends
State
<
FlashSaleHeader
>
{
late
Duration
_remaining
;
Timer
?
_timer
;
bool
_didNotify
=
false
;
@override
void
initState
()
{
super
.
initState
();
_remaining
=
widget
.
flashSale
?.
countdownLocal
??
Duration
(
seconds:
10000
0
);
_remaining
=
widget
.
flashSale
?.
countdownLocal
??
Duration
(
seconds:
0
);
_startTimer
();
}
void
_startTimer
()
{
_timer
?.
cancel
();
_timer
=
Timer
.
periodic
(
const
Duration
(
seconds:
1
),
(
_
)
{
if
(
_remaining
.
inSeconds
<=
0
)
{
_timer
?.
cancel
();
if
(
_remaining
.
inSeconds
<=
0
)
return
;
_timer
=
Timer
.
periodic
(
const
Duration
(
seconds:
1
),
(
timer
)
{
if
(!
mounted
)
{
timer
.
cancel
();
return
;
}
if
(
_remaining
.
inSeconds
<=
1
)
{
setState
(()
{
_remaining
=
Duration
.
zero
;
});
timer
.
cancel
();
_notifyCountdownFinished
();
}
else
{
setState
(()
{
_remaining
-=
const
Duration
(
seconds:
1
);
...
...
@@ -40,20 +47,10 @@ class _FlashSaleHeaderState extends State<FlashSaleHeader> {
});
}
String
_formatTime
(
int
value
)
=>
value
.
toString
().
padLeft
(
2
,
'0'
);
Widget
_buildTimeBox
(
String
text
)
{
return
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
3
,
vertical:
2
),
decoration:
BoxDecoration
(
color:
Colors
.
red
.
shade400
,
borderRadius:
BorderRadius
.
circular
(
4
),
),
child:
Text
(
text
,
style:
const
TextStyle
(
color:
Colors
.
white
,
fontWeight:
FontWeight
.
bold
),
),
);
void
_notifyCountdownFinished
()
{
if
(
_didNotify
)
return
;
_didNotify
=
true
;
widget
.
onCountdownFinished
?.
call
();
}
@override
...
...
@@ -64,38 +61,61 @@ class _FlashSaleHeaderState extends State<FlashSaleHeader> {
@override
Widget
build
(
BuildContext
context
)
{
final
hours
=
_formatTime
(
_remaining
.
inHours
);
final
minutes
=
_formatTime
(
_remaining
.
inMinutes
.
remainder
(
60
));
final
seconds
=
_formatTime
(
_remaining
.
inSeconds
.
remainder
(
60
));
return
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
12
),
child:
Row
(
children:
[
Image
.
asset
(
"assets/images/ic_flash_sale.png"
,
height:
24
,
fit:
BoxFit
.
cover
,
),
Image
.
asset
(
"assets/images/ic_flash_sale.png"
,
height:
24
,
fit:
BoxFit
.
cover
),
const
SizedBox
(
width:
6
),
Text
(
widget
.
flashSale
?.
desTime
??
""
,
style:
TextStyle
(
fontSize:
14
)),
const
SizedBox
(
width:
4
),
_buildTimeBox
(
hours
),
const
SizedBox
(
width:
2
),
const
Text
(
":"
,
style:
TextStyle
(
fontWeight:
FontWeight
.
bold
)),
const
SizedBox
(
width:
2
),
_buildTimeBox
(
minutes
),
const
SizedBox
(
width:
2
),
const
Text
(
":"
,
style:
TextStyle
(
fontWeight:
FontWeight
.
bold
)),
const
SizedBox
(
width:
2
),
_buildTimeBox
(
seconds
),
const
Spacer
(),
Expanded
(
child:
_buildCountdownSection
(
_remaining
)),
const
SizedBox
(
width:
8
),
if
(
widget
.
onViewAll
!=
null
)
GestureDetector
(
onTap:
widget
.
onViewAll
,
child:
Text
(
'Xem tất cả'
,
style:
TextStyle
(
color:
Colors
.
blue
[
700
],
fontWeight:
FontWeight
.
bold
),
),
child:
Text
(
'Xem tất cả'
,
style:
TextStyle
(
color:
Colors
.
blue
[
700
],
fontWeight:
FontWeight
.
bold
)),
),
],
),
);
}
Widget
_buildCountdownSection
(
Duration
duration
)
{
final
bool
isCounting
=
duration
.
inSeconds
>
0
;
final
label
=
(
widget
.
flashSale
?.
desTime
??
'Kết thúc trong'
);
return
Row
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
Flexible
(
child:
FittedBox
(
fit:
BoxFit
.
scaleDown
,
alignment:
Alignment
.
centerLeft
,
child:
Text
(
label
,
style:
const
TextStyle
(
fontSize:
14
,
fontWeight:
FontWeight
.
w700
,
color:
Colors
.
black54
),
),
),
),
const
SizedBox
(
width:
4
),
_buildTimeChip
(
_formatTwoDigits
(
duration
.
inHours
)),
const
SizedBox
(
width:
2
),
const
Text
(
':'
,
style:
TextStyle
(
fontWeight:
FontWeight
.
bold
)),
const
SizedBox
(
width:
2
),
_buildTimeChip
(
_formatTwoDigits
(
duration
.
inMinutes
.
remainder
(
60
))),
const
SizedBox
(
width:
2
),
const
Text
(
':'
,
style:
TextStyle
(
fontWeight:
FontWeight
.
bold
)),
const
SizedBox
(
width:
2
),
_buildTimeChip
(
_formatTwoDigits
(
duration
.
inSeconds
.
remainder
(
60
))),
],
);
}
Widget
_buildTimeChip
(
String
value
)
{
return
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
4
,
vertical:
4
),
decoration:
BoxDecoration
(
color:
Colors
.
black
,
borderRadius:
BorderRadius
.
circular
(
6
)),
child:
Text
(
value
,
style:
const
TextStyle
(
color:
Colors
.
white
,
fontWeight:
FontWeight
.
bold
)),
);
}
String
_formatTwoDigits
(
int
value
)
=>
value
.
toString
().
padLeft
(
2
,
'0'
);
}
lib/screen/home/home_screen.dart
View file @
f0334970
...
...
@@ -34,6 +34,7 @@ class _HomeScreenState extends State<HomeScreen> with PopupOnInit {
final
HomeTabViewModel
_viewModel
=
Get
.
put
(
HomeTabViewModel
());
final
_headerHomeVM
=
Get
.
find
<
HeaderHomeViewModel
>();
final
RxBool
_showHover
=
true
.
obs
;
bool
_isRefreshingFlashSale
=
false
;
@override
void
initState
()
{
...
...
@@ -43,6 +44,16 @@ class _HomeScreenState extends State<HomeScreen> with PopupOnInit {
runPopupCheck
(
DirectionalScreenName
.
home
);
}
Future
<
void
>
_onFlashSaleCountdownFinished
()
async
{
if
(
_isRefreshingFlashSale
)
return
;
_isRefreshingFlashSale
=
true
;
try
{
await
_viewModel
.
refreshFlashSale
();
}
finally
{
_isRefreshingFlashSale
=
false
;
}
}
Widget
_buildSliverHeader
(
double
heightHeader
)
{
return
Obx
(()
{
final
notifyUnreadData
=
_headerHomeVM
.
notificationUnreadData
.
value
;
...
...
@@ -57,10 +68,7 @@ class _HomeScreenState extends State<HomeScreen> with PopupOnInit {
}
List
<
Widget
>
_buildSectionContent
()
{
final
sections
=
_viewModel
.
sectionLayouts
.
map
(
_buildSection
)
.
whereType
<
Widget
>()
.
toList
();
final
sections
=
_viewModel
.
sectionLayouts
.
map
(
_buildSection
).
whereType
<
Widget
>().
toList
();
if
(
sections
.
isEmpty
)
{
return
const
[
Padding
(
padding:
EdgeInsets
.
symmetric
(
vertical:
40
),
child:
EmptyWidget
())];
}
...
...
@@ -123,6 +131,7 @@ class _HomeScreenState extends State<HomeScreen> with PopupOnInit {
products:
products
,
sectionConfig:
_viewModel
.
getMainSectionConfigModel
(
HeaderSectionType
.
flashSale
),
onTap:
(
product
)
=>
Get
.
toNamed
(
voucherDetailScreen
,
arguments:
product
.
id
),
onCountdownFinished:
_onFlashSaleCountdownFinished
,
);
case
HeaderSectionType
.
brand
:
if
(
_viewModel
.
brands
.
isEmpty
)
return
null
;
...
...
lib/screen/home/home_tab_viewmodel.dart
View file @
f0334970
...
...
@@ -58,8 +58,7 @@ class HomeTabViewModel extends RestfulApiViewModel {
await
callApi
<
List
<
MainSectionConfigModel
>>(
request:
()
=>
client
.
getSectionLayoutHome
(),
onSuccess:
(
data
,
_
)
=>
_resolveSectionLayouts
(
data
),
onFailure:
(
message
,
response
,
error
)
async
=>
_resolveSectionLayouts
(
await
_loadSectionLayoutHomeFromCache
()),
onFailure:
(
message
,
response
,
error
)
async
=>
_resolveSectionLayouts
(
await
_loadSectionLayoutHomeFromCache
()),
withLoading:
showLoading
,
);
}
...
...
@@ -147,6 +146,12 @@ class HomeTabViewModel extends RestfulApiViewModel {
flashSaleData
.
value
=
res
.
data
;
}
Future
<
void
>
refreshFlashSale
()
async
{
final
section
=
getMainSectionConfigModel
(
HeaderSectionType
.
flashSale
);
if
(
section
==
null
)
return
;
await
_loadFlashSale
(
section
);
}
Future
<
void
>
_loadBrands
(
MainSectionConfigModel
section
)
async
{
final
res
=
await
client
.
fetchList
<
BrandModel
>(
section
.
apiList
??
''
,
...
...
Prev
1
2
Next
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment