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
e7dd4bc3
Commit
e7dd4bc3
authored
Mar 06, 2025
by
DatHV
Browse files
update page detail
parent
8ec716d3
Changes
19
Show whitespace changes
Inline
Side-by-side
assets/images/bg_header_campain_default.png
0 → 100644
View file @
e7dd4bc3
191 KB
lib/configs/api_paths.dart
View file @
e7dd4bc3
class
APIPaths
{
static
const
String
baseUrl
=
"https://api.
sandbox.
mypoint.com.vn/8854/gup2start/rest"
;
static
const
String
baseUrl
=
"https://api.mypoint.com.vn/8854/gup2start/rest"
;
static
const
String
checkUpdate
=
"/version-management-service/api/v1.0/check-customer-software-update"
;
static
const
String
getOnboardingInfo
=
"/resource/api/v2.0/intro-screen"
;
static
const
String
checkPhoneNumber
=
"/user/api/v2.0/account/users/checkPhoneNumber"
;
...
...
@@ -8,4 +8,5 @@ class APIPaths {
static
const
String
retryOtpWithAction
=
"/iam/v2/authentication/otp/retry"
;
static
const
String
signup
=
"/user/api/v2.0/signup"
;
static
const
String
otpCreateNew
=
"/otpCreateNew/1.0.0"
;
static
const
String
websitePageGetDetail
=
"/websitePageGetDetail/1.0.0"
;
}
lib/extensions/string_extension.dart
View file @
e7dd4bc3
import
'dart:convert'
;
import
'package:crypto/crypto.dart'
;
import
'package:flutter/material.dart'
;
extension
PhoneValidator
on
String
{
bool
isPhoneValid
()
{
...
...
@@ -9,8 +10,23 @@ extension PhoneValidator on String {
extension
StringConvert
on
String
{
String
toSha256
()
{
var
bytes1
=
utf8
.
encode
(
this
);
// data being hashed
var
bytes1
=
utf8
.
encode
(
this
);
var
digest1
=
sha256
.
convert
(
bytes1
);
return
digest1
.
toString
();
}
}
/// Hàm parse hex -> Color
Color
parseHexColor
(
String
hexString
,
{
Color
fallbackColor
=
Colors
.
grey
})
{
try
{
if
(
hexString
.
startsWith
(
'#'
))
{
hexString
=
hexString
.
replaceFirst
(
'#'
,
''
);
}
if
(
hexString
.
length
==
6
)
{
hexString
=
'ff
$hexString
'
;
}
return
Color
(
int
.
parse
(
hexString
,
radix:
16
));
}
catch
(
e
)
{
return
fallbackColor
;
}
}
lib/networking/restful_api_request.dart
View file @
e7dd4bc3
...
...
@@ -9,6 +9,7 @@ import '../model/update_response_object.dart';
import
'../screen/onboarding/model/check_phone_response_model.dart'
;
import
'../screen/onboarding/model/onboarding_info_model.dart'
;
import
'../screen/otp/model/otp_verify_response_model.dart'
;
import
'../screen/pageDetail/model/campaign_detail_model.dart'
;
import
'../screen/splash/splash_screen_viewmodel.dart'
;
import
'model_maker.dart'
;
...
...
@@ -86,4 +87,14 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
(
data
)
=>
EmptyCodable
.
fromJson
(
data
as
Json
),
);
}
Future
<
BaseResponseModel
<
CampaignDetailResponseModel
>>
websitePageGetDetail
(
String
id
)
async
{
final
body
=
{
"website_page_id"
:
"18756"
,
"access_token"
:
""
,};
return
requestNormal
(
APIPaths
.
websitePageGetDetail
,
Method
.
POST
,
body
,
(
data
)
=>
CampaignDetailResponseModel
.
fromJson
(
data
as
Json
),
);
}
}
lib/screen/biometric/biometric_screen.dart
0 → 100644
View file @
e7dd4bc3
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'package:local_auth/local_auth.dart'
;
import
'biometric_viewmodel.dart'
;
class
BiometricAuthScreen
extends
StatefulWidget
{
const
BiometricAuthScreen
({
super
.
key
});
@override
State
<
BiometricAuthScreen
>
createState
()
=>
_BiometricAuthScreenState
();
}
class
_BiometricAuthScreenState
extends
State
<
BiometricAuthScreen
>
{
final
controller
=
Get
.
put
(
BiometricViewModel
());
@override
Widget
build
(
BuildContext
context
)
{
return
Scaffold
(
appBar:
AppBar
(
automaticallyImplyLeading:
false
,
leading:
null
,
title:
Obx
(()
{
String
title
=
controller
.
biometricType
.
value
==
BiometricType
.
face
?
"Face ID"
:
"Touch ID"
;
return
Text
(
title
);
}),
backgroundColor:
Colors
.
white
,
foregroundColor:
Colors
.
black
,
elevation:
0
,
),
body:
Obx
(()
{
if
(!
controller
.
isAvailable
.
value
)
{
return
const
Center
(
child:
Text
(
"Thiết bị không hỗ trợ sinh trắc học."
));
}
String
title
=
controller
.
biometricType
.
value
==
BiometricType
.
face
?
"Kích hoạt xác thực Face ID"
:
"Kích hoạt xác thực vân tay"
;
String
description
=
controller
.
biometricType
.
value
==
BiometricType
.
face
?
"Kích hoạt xác thực Face ID để đăng nhập nhanh không cần mật khẩu.
\n
Bạn có muốn thực hiện không?"
:
"Kích hoạt xác thực vân tay để đăng nhập nhanh không cần mật khẩu.
\n
Bạn có muốn thực hiện không?"
;
IconData
icon
=
controller
.
biometricType
.
value
==
BiometricType
.
face
?
Icons
.
face
:
Icons
.
fingerprint
;
return
Center
(
child:
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
20
),
child:
Column
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
Icon
(
icon
,
size:
80
,
color:
Colors
.
black54
),
const
SizedBox
(
height:
20
),
Text
(
title
,
style:
const
TextStyle
(
fontSize:
24
,
fontWeight:
FontWeight
.
bold
)),
const
SizedBox
(
height:
10
),
Text
(
description
,
textAlign:
TextAlign
.
center
,
style:
const
TextStyle
(
fontSize:
16
,
color:
Colors
.
black54
)),
const
SizedBox
(
height:
80
),
/// Nút kích hoạt
Obx
(()
=>
SizedBox
(
width:
double
.
infinity
,
child:
ElevatedButton
(
onPressed:
controller
.
isAuthenticating
.
value
?
null
:
controller
.
authenticate
,
style:
ElevatedButton
.
styleFrom
(
padding:
const
EdgeInsets
.
symmetric
(
vertical:
15
),
backgroundColor:
Colors
.
redAccent
,
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
10
),
),
),
child:
controller
.
isAuthenticating
.
value
?
const
CircularProgressIndicator
(
color:
Colors
.
white
)
:
const
Text
(
"Kích hoạt"
,
style:
TextStyle
(
color:
Colors
.
white
,
fontSize:
18
)),
),
)),
const
SizedBox
(
height:
10
),
/// Nút để sau
TextButton
(
onPressed:
()
=>
Get
.
back
(),
child:
const
Text
(
"Để sau"
,
style:
TextStyle
(
fontSize:
16
,
color:
Colors
.
black54
)),
),
],
),
),
);
}),
);
}
}
lib/screen/biometric/biometric_viewmodel.dart
0 → 100644
View file @
e7dd4bc3
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'package:local_auth/local_auth.dart'
;
class
BiometricViewModel
extends
GetxController
{
final
LocalAuthentication
_localAuth
=
LocalAuthentication
();
var
biometricType
=
Rxn
<
BiometricType
>();
// Loại sinh trắc học (Face ID / Touch ID)
var
isAvailable
=
false
.
obs
;
// Kiểm tra thiết bị có hỗ trợ sinh trắc học không
var
isAuthenticating
=
false
.
obs
;
// Trạng thái xác thực
@override
void
onInit
()
{
super
.
onInit
();
checkBiometricType
();
}
/// Kiểm tra loại sinh trắc học có thể sử dụng
Future
<
void
>
checkBiometricType
()
async
{
try
{
bool
canCheckBiometrics
=
await
_localAuth
.
canCheckBiometrics
;
List
<
BiometricType
>
availableBiometrics
=
await
_localAuth
.
getAvailableBiometrics
();
isAvailable
.
value
=
canCheckBiometrics
;
if
(
availableBiometrics
.
contains
(
BiometricType
.
face
))
{
biometricType
.
value
=
BiometricType
.
face
;
}
else
if
(
availableBiometrics
.
contains
(
BiometricType
.
fingerprint
))
{
biometricType
.
value
=
BiometricType
.
fingerprint
;
}
}
catch
(
e
)
{
print
(
"Lỗi kiểm tra sinh trắc học:
$e
"
);
}
}
/// Xác thực sinh trắc học
Future
<
void
>
authenticate
()
async
{
isAuthenticating
.
value
=
true
;
try
{
bool
authenticated
=
await
_localAuth
.
authenticate
(
localizedReason:
"Xác thực để kích hoạt đăng nhập nhanh"
,
options:
const
AuthenticationOptions
(
biometricOnly:
true
,
stickyAuth:
true
,
),
);
if
(
authenticated
)
{
Get
.
snackbar
(
"Thành công"
,
"Xác thực sinh trắc học thành công!"
,
backgroundColor:
Colors
.
green
,
colorText:
Colors
.
white
);
}
else
{
Get
.
snackbar
(
"Thất bại"
,
"Xác thực không thành công!"
,
backgroundColor:
Colors
.
red
,
colorText:
Colors
.
white
);
}
}
catch
(
e
)
{
print
(
"Lỗi xác thực:
$e
"
);
}
isAuthenticating
.
value
=
false
;
}
}
lib/screen/create_pass/create_pass_screen.dart
View file @
e7dd4bc3
...
...
@@ -20,7 +20,7 @@ class CreatePasswordScreen extends StatelessWidget {
return
Scaffold
(
appBar:
AppBar
(
centerTitle:
true
,
leading:
CustomBackButton
(
onPressed:
()
=>
{
Get
.
off
(()
=>
OnboardingScreen
())}
),
leading:
CustomBackButton
(),
),
body:
SafeArea
(
child:
Stack
(
...
...
lib/screen/create_pass/create_pass_viewmodel.dart
View file @
e7dd4bc3
...
...
@@ -38,8 +38,9 @@ class CreatePasswordViewModel extends GetxController {
Future
<
void
>
onSubmit
()
async
{
if
(!
isButtonEnabled
.
value
)
return
;
try
{
final
success
=
await
repository
.
signup
(
newPassword
.
value
);
if
(
success
)
{
final
response
=
await
repository
.
signup
(
newPassword
.
value
);
// errorMessage.value = success
if
(
response
.
isSuccess
)
{
errorMessage
.
value
=
""
;
// TODO: Điều hướng sang màn hình tiếp theo
// e.g. Get.offAllNamed("/home");
...
...
lib/screen/create_pass/signup_create_password_repository.dart
View file @
e7dd4bc3
import
'dart:async'
;
import
'package:flutter/material.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_request.dart'
;
import
'../../base/base_response_model.dart'
;
import
'../../base/restful_api_viewmodel.dart'
;
import
'../splash/splash_screen_viewmodel.dart'
;
abstract
class
ICreatePasswordRepository
{
late
String
phoneNumber
;
Future
<
bool
?>
createPassword
(
String
newPassword
);
Future
<
bool
>
signup
(
String
password
);
Future
<
BaseResponseModel
<
EmptyCodable
>
>
signup
(
String
password
);
}
class
SignUpCreatePasswordRepository
extends
RestfulApiViewModel
implements
ICreatePasswordRepository
{
...
...
@@ -16,11 +18,11 @@ class SignUpCreatePasswordRepository extends RestfulApiViewModel implements ICre
SignUpCreatePasswordRepository
(
this
.
phoneNumber
);
@override
Future
<
bool
>
signup
(
String
password
)
async
{
Future
<
BaseResponseModel
<
EmptyCodable
>
>
signup
(
String
password
)
async
{
showLoading
();
return
client
.
signup
(
phoneNumber
,
password
).
then
((
value
)
{
hideLoading
();
return
value
.
isSuccess
;
return
value
;
});
}
...
...
lib/screen/login/login_viewmodel.dart
View file @
e7dd4bc3
...
...
@@ -66,9 +66,6 @@ class LoginViewModel extends RestfulApiViewModel {
}
void
onForgotPassPressed
()
{
client
.
getOnboardingInfo
().
then
((
value
)
{
info
.
value
=
value
;
});
}
/// Xác thực đăng nhập bằng sinh trắc
...
...
lib/screen/onboarding/onboarding_screen.dart
View file @
e7dd4bc3
import
'package:flutter/material.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter_widget_from_html/flutter_widget_from_html.dart'
;
// Hiển thị HTML
import
'package:flutter_widget_from_html/flutter_widget_from_html.dart'
;
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/base/base_response_model.dart'
;
import
'../../base/base_screen.dart'
;
import
'../../base/basic_state.dart'
;
import
'../../configs/constants.dart'
;
import
'../../resouce/base_color.dart'
;
import
'../biometric/biometric_screen.dart'
;
import
'../create_pass/create_pass_screen.dart'
;
import
'../create_pass/signup_create_password_repository.dart'
;
import
'../login/login_screen.dart'
;
import
'../otp/otp_screen.dart'
;
import
'../otp/verify_otp_repository.dart'
;
import
'../pageDetail/campaign_detail_screen.dart'
;
import
'model/check_phone_response_model.dart'
;
import
'onboarding_viewmodel.dart'
;
...
...
@@ -56,6 +58,8 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
}
void
_handleResponseCheckPhoneNumber
(
CheckPhoneResponseModel
?
response
)
{
Get
.
to
(
CampaignDetailScreen
());
return
;
if
(
response
==
null
)
return
;
if
(
response
.
requireRecaptcha
==
true
)
{
// show Captcha
...
...
lib/screen/pageDetail/campaign_detail_screen.dart
0 → 100644
View file @
e7dd4bc3
import
'package:flutter/material.dart'
;
import
'package:flutter_widget_from_html/flutter_widget_from_html.dart'
;
import
'package:get/get.dart'
;
import
'../../extensions/string_extension.dart'
;
// tuỳ dự án
import
'../../resouce/base_color.dart'
;
import
'../../widgets/network_image_with_aspect_ratio.dart'
;
// widget custom
import
'campaign_detail_viewmodel.dart'
;
import
'model/campaign_detail_item_model.dart'
;
import
'model/campaign_detail_model.dart'
;
import
'model/media_type_item_campaign.dart'
;
class
CampaignDetailScreen
extends
StatefulWidget
{
const
CampaignDetailScreen
({
super
.
key
});
@override
State
<
CampaignDetailScreen
>
createState
()
=>
_CampaignDetailScreenState
();
}
class
_CampaignDetailScreenState
extends
State
<
CampaignDetailScreen
>
{
final
CampaignDetailViewModel
_viewModel
=
Get
.
put
(
CampaignDetailViewModel
());
@override
void
initState
()
{
super
.
initState
();
_viewModel
.
fetchCampaignDetail
();
}
@override
Widget
build
(
BuildContext
context
)
{
return
Scaffold
(
backgroundColor:
BaseColor
.
second200
,
// Không dùng AppBar mặc định
body:
Obx
(()
{
CampaignDetailModel
?
pageDetail
=
_viewModel
.
campaignDetail
.
value
.
data
?.
pageDetail
;
if
(
pageDetail
==
null
)
{
return
const
Center
(
child:
CircularProgressIndicator
());
}
// Lấy các giá trị
final
thumbnail
=
pageDetail
.
thumbnail
??
""
;
final
publishDate
=
pageDetail
.
publishDate
??
""
;
final
title
=
pageDetail
.
title
??
""
;
final
List
<
CampaignDetailItemModel
>
items
=
pageDetail
.
items
??
[];
final
buttonOn
=
pageDetail
.
buttonOn
??
"0"
;
final
buttonColor
=
pageDetail
.
buttonColor
??
"#d9d9d9"
;
final
buttonName
=
pageDetail
.
buttonName
??
""
;
final
buttonTextColor
=
pageDetail
.
buttonTextColor
??
"#FFFFFF"
;
return
Stack
(
children:
[
SingleChildScrollView
(
child:
Column
(
children:
[
if
(
thumbnail
.
isNotEmpty
)
NetworkImageWithAspectRatio
(
imageUrl:
thumbnail
,
placeholder:
Container
(
height:
200
,
color:
Colors
.
grey
.
shade200
,
child:
const
Center
(
child:
CircularProgressIndicator
()),
),
errorWidget:
Image
.
asset
(
"assets/bg_header_campain_default.png"
,
fit:
BoxFit
.
cover
),
)
else
Image
.
asset
(
"assets/bg_header_campain_default.png"
,
fit:
BoxFit
.
cover
),
Transform
.
translate
(
offset:
const
Offset
(
0
,
-
32
),
child:
Container
(
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
16
),
padding:
EdgeInsets
.
all
(
16
),
decoration:
BoxDecoration
(
color:
Colors
.
white
,
border:
Border
.
all
(
color:
BaseColor
.
second200
),
borderRadius:
BorderRadius
.
circular
(
12
),
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
stretch
,
children:
[
Text
(
publishDate
,
style:
const
TextStyle
(
color:
Colors
.
grey
,
fontSize:
14
)),
const
SizedBox
(
height:
8
),
Text
(
title
,
style:
const
TextStyle
(
fontSize:
20
,
fontWeight:
FontWeight
.
bold
)),
const
SizedBox
(
height:
16
),
_buildItems
(
items
),
const
SizedBox
(
height:
24
),
// 3) Nút, nếu có
if
(
buttonOn
==
"1"
)
ElevatedButton
(
onPressed:
()
{
// Xử lý khi bấm nút
},
style:
ElevatedButton
.
styleFrom
(
backgroundColor:
parseHexColor
(
buttonColor
),
minimumSize:
const
Size
.
fromHeight
(
48
),
),
child:
Text
(
buttonName
,
style:
TextStyle
(
color:
parseHexColor
(
buttonTextColor
),
fontWeight:
FontWeight
.
bold
),
),
),
],
),
),
),
],
),
),
Positioned
(
top:
MediaQuery
.
of
(
context
).
padding
.
top
+
8
,
left:
8
,
child:
IconButton
(
icon:
const
Icon
(
Icons
.
arrow_back_ios
,
color:
Colors
.
white
),
onPressed:
()
=>
Get
.
back
(),
),
),
if
(
buttonOn
==
"1"
)
_bottomButton
(
pageDetail
),
],
);
}),
);
}
Widget
_bottomButton
(
CampaignDetailModel
?
pageDetail
)
{
final
buttonColor
=
pageDetail
?.
buttonColor
??
"#d9d9d9"
;
final
buttonName
=
pageDetail
?.
buttonName
??
""
;
final
buttonTextColor
=
pageDetail
?.
buttonTextColor
??
"#FFFFFF"
;
return
Positioned
(
left:
16
,
right:
16
,
bottom:
MediaQuery
.
of
(
context
).
padding
.
bottom
+
16
,
child:
ElevatedButton
(
onPressed:
()
{
// Xử lý khi bấm nút
},
style:
ElevatedButton
.
styleFrom
(
backgroundColor:
parseHexColor
(
buttonColor
),
minimumSize:
const
Size
.
fromHeight
(
48
),
),
child:
Text
(
buttonName
,
style:
TextStyle
(
color:
parseHexColor
(
buttonTextColor
),
fontWeight:
FontWeight
.
bold
)),
),
);
}
Widget
_buildItems
(
List
<
CampaignDetailItemModel
>
items
)
{
List
<
Widget
>
widgets
=
[];
for
(
var
item
in
items
)
{
final
mediaType
=
item
.
mediaType
?.
value
??
""
;
//toLowerCase() ?? "";
if
(
mediaType
==
MediaTypeItemCampaign
.
image
.
name
)
{
if
(
item
.
contentText
!=
null
&&
item
.
contentText
!.
isNotEmpty
)
{
widgets
.
add
(
Padding
(
padding:
const
EdgeInsets
.
only
(
bottom:
16
),
child:
NetworkImageWithAspectRatio
(
imageUrl:
item
.
contentText
!,
placeholder:
Container
(
height:
200
,
color:
Colors
.
grey
.
shade200
,
child:
const
Center
(
child:
CircularProgressIndicator
()),
),
errorWidget:
Image
.
asset
(
"assets/bg_header_campain_default.png"
,
fit:
BoxFit
.
cover
),
),
),
);
}
}
else
if
(
mediaType
==
MediaTypeItemCampaign
.
text
.
name
)
{
if
(
item
.
contentText
!=
null
&&
item
.
contentText
!.
isNotEmpty
)
{
widgets
.
add
(
Padding
(
padding:
const
EdgeInsets
.
only
(
bottom:
16
),
child:
HtmlWidget
(
item
.
contentText
!)));
}
}
}
return
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
stretch
,
children:
widgets
);
}
}
lib/screen/pageDetail/campaign_detail_viewmodel.dart
0 → 100644
View file @
e7dd4bc3
import
'package:get/get.dart'
;
import
'package:flutter/material.dart'
;
import
'package:mypoint_flutter_app/configs/constants.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_request.dart'
;
import
'../../base/base_response_model.dart'
;
import
'../../base/restful_api_viewmodel.dart'
;
import
'model/campaign_detail_model.dart'
;
class
CampaignDetailViewModel
extends
RestfulApiViewModel
{
var
campaignDetail
=
BaseResponseModel
<
CampaignDetailResponseModel
>().
obs
;
var
isLoading
=
false
.
obs
;
var
errorMessage
=
""
.
obs
;
void
fetchCampaignDetail
()
{
showLoading
();
isLoading
(
true
);
client
.
websitePageGetDetail
(
""
).
then
((
value
)
{
campaignDetail
.
value
=
value
;
if
(!
value
.
isSuccess
)
{
errorMessage
.
value
=
value
.
errorMessage
??
Constants
.
commonError
;
}
hideLoading
();
isLoading
(
false
);
});
}
}
lib/screen/pageDetail/model/campaign_detail_item_model.dart
0 → 100644
View file @
e7dd4bc3
import
'package:json_annotation/json_annotation.dart'
;
import
'media_type_item_campaign.dart'
;
part
'campaign_detail_item_model.g.dart'
;
@JsonSerializable
()
class
CampaignDetailItemModel
{
@JsonKey
(
name:
"content_caption"
)
String
?
contentCaption
;
@JsonKey
(
name:
"content_text"
)
String
?
contentText
;
@JsonKey
(
name:
"media_type"
,
fromJson:
MediaTypeItemCampaign
.
fromString
,
toJson:
_mediaTypeToJson
)
MediaTypeItemCampaign
?
mediaType
;
CampaignDetailItemModel
({
this
.
contentCaption
,
this
.
contentText
,
this
.
mediaType
,
});
factory
CampaignDetailItemModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$CampaignDetailItemModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$CampaignDetailItemModelToJson
(
this
);
/// 🎯 Helper để convert Enum sang String khi serialize JSON
static
String
?
_mediaTypeToJson
(
MediaTypeItemCampaign
?
type
)
=>
type
?.
toJson
();
}
lib/screen/pageDetail/model/campaign_detail_item_model.g.dart
0 → 100644
View file @
e7dd4bc3
// GENERATED CODE - DO NOT MODIFY BY HAND
part of
'campaign_detail_item_model.dart'
;
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CampaignDetailItemModel
_$CampaignDetailItemModelFromJson
(
Map
<
String
,
dynamic
>
json
,
)
=>
CampaignDetailItemModel
(
contentCaption:
json
[
'content_caption'
]
as
String
?,
contentText:
json
[
'content_text'
]
as
String
?,
mediaType:
MediaTypeItemCampaign
.
fromString
(
json
[
'media_type'
]
as
String
?),
);
Map
<
String
,
dynamic
>
_$CampaignDetailItemModelToJson
(
CampaignDetailItemModel
instance
,
)
=>
<
String
,
dynamic
>{
'content_caption'
:
instance
.
contentCaption
,
'content_text'
:
instance
.
contentText
,
'media_type'
:
CampaignDetailItemModel
.
_mediaTypeToJson
(
instance
.
mediaType
),
};
lib/screen/pageDetail/model/campaign_detail_model.dart
0 → 100644
View file @
e7dd4bc3
import
'package:json_annotation/json_annotation.dart'
;
import
'campaign_detail_item_model.dart'
;
part
'campaign_detail_model.g.dart'
;
@JsonSerializable
()
class
CampaignDetailModel
{
final
String
?
title
;
@JsonKey
(
name:
"publish_at_date"
)
final
String
?
publishDate
;
final
String
?
thumbnail
;
@JsonKey
(
name:
"button_on"
)
final
String
?
buttonOn
;
@JsonKey
(
name:
"button_color"
)
final
String
?
buttonColor
;
@JsonKey
(
name:
"button_name"
)
final
String
?
buttonName
;
@JsonKey
(
name:
"button_text_color"
)
final
String
?
buttonTextColor
;
final
List
<
CampaignDetailItemModel
>?
items
;
CampaignDetailModel
({
this
.
title
,
this
.
publishDate
,
this
.
thumbnail
,
this
.
buttonOn
,
this
.
buttonColor
,
this
.
buttonName
,
this
.
buttonTextColor
,
this
.
items
,
});
factory
CampaignDetailModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$CampaignDetailModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$CampaignDetailModelToJson
(
this
);
}
@JsonSerializable
()
class
CampaignDetailResponseModel
{
@JsonKey
(
name:
"page_detail"
)
CampaignDetailModel
?
pageDetail
;
CampaignDetailResponseModel
({
this
.
pageDetail
,
});
factory
CampaignDetailResponseModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$CampaignDetailResponseModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$CampaignDetailResponseModelToJson
(
this
);
}
\ No newline at end of file
lib/screen/pageDetail/model/campaign_detail_model.g.dart
0 → 100644
View file @
e7dd4bc3
// GENERATED CODE - DO NOT MODIFY BY HAND
part of
'campaign_detail_model.dart'
;
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CampaignDetailModel
_$CampaignDetailModelFromJson
(
Map
<
String
,
dynamic
>
json
)
=>
CampaignDetailModel
(
title:
json
[
'title'
]
as
String
?,
publishDate:
json
[
'publish_at_date'
]
as
String
?,
thumbnail:
json
[
'thumbnail'
]
as
String
?,
buttonOn:
json
[
'button_on'
]
as
String
?,
buttonColor:
json
[
'button_color'
]
as
String
?,
buttonName:
json
[
'button_name'
]
as
String
?,
buttonTextColor:
json
[
'button_text_color'
]
as
String
?,
items:
(
json
[
'items'
]
as
List
<
dynamic
>?)
?.
map
(
(
e
)
=>
CampaignDetailItemModel
.
fromJson
(
e
as
Map
<
String
,
dynamic
>),
)
.
toList
(),
);
Map
<
String
,
dynamic
>
_$CampaignDetailModelToJson
(
CampaignDetailModel
instance
,
)
=>
<
String
,
dynamic
>{
'title'
:
instance
.
title
,
'publish_at_date'
:
instance
.
publishDate
,
'thumbnail'
:
instance
.
thumbnail
,
'button_on'
:
instance
.
buttonOn
,
'button_color'
:
instance
.
buttonColor
,
'button_name'
:
instance
.
buttonName
,
'button_text_color'
:
instance
.
buttonTextColor
,
'items'
:
instance
.
items
,
};
CampaignDetailResponseModel
_$CampaignDetailResponseModelFromJson
(
Map
<
String
,
dynamic
>
json
,
)
=>
CampaignDetailResponseModel
(
pageDetail:
json
[
'page_detail'
]
==
null
?
null
:
CampaignDetailModel
.
fromJson
(
json
[
'page_detail'
]
as
Map
<
String
,
dynamic
>,
),
);
Map
<
String
,
dynamic
>
_$CampaignDetailResponseModelToJson
(
CampaignDetailResponseModel
instance
,
)
=>
<
String
,
dynamic
>{
'page_detail'
:
instance
.
pageDetail
};
lib/screen/pageDetail/model/media_type_item_campaign.dart
0 → 100644
View file @
e7dd4bc3
import
'package:get/get.dart'
;
import
'package:json_annotation/json_annotation.dart'
;
/// 🎯 Enum ánh xạ với String
enum
MediaTypeItemCampaign
{
image
(
"image"
),
text
(
"text"
);
final
String
value
;
const
MediaTypeItemCampaign
(
this
.
value
);
/// 🎯 Chuyển từ String sang Enum
static
MediaTypeItemCampaign
?
fromString
(
String
?
value
)
{
return
MediaTypeItemCampaign
.
values
.
firstWhereOrNull
((
e
)
=>
e
.
value
==
value
);
}
/// 🎯 Chuyển từ Enum sang String
String
toJson
()
=>
value
;
}
\ No newline at end of file
lib/widgets/network_image_with_aspect_ratio.dart
0 → 100644
View file @
e7dd4bc3
import
'package:flutter/material.dart'
;
class
NetworkImageWithAspectRatio
extends
StatefulWidget
{
final
String
imageUrl
;
final
Widget
?
placeholder
;
final
Widget
?
errorWidget
;
const
NetworkImageWithAspectRatio
({
super
.
key
,
required
this
.
imageUrl
,
this
.
placeholder
,
this
.
errorWidget
,
});
@override
State
<
NetworkImageWithAspectRatio
>
createState
()
=>
_NetworkImageWithAspectRatioState
();
}
class
_NetworkImageWithAspectRatioState
extends
State
<
NetworkImageWithAspectRatio
>
{
double
?
_aspectRatio
;
bool
_hasError
=
false
;
@override
void
initState
()
{
super
.
initState
();
_loadImageInfo
();
}
void
_loadImageInfo
()
{
final
image
=
NetworkImage
(
widget
.
imageUrl
);
image
.
resolve
(
const
ImageConfiguration
()).
addListener
(
ImageStreamListener
(
(
ImageInfo
info
,
bool
synchronousCall
)
{
final
width
=
info
.
image
.
width
;
final
height
=
info
.
image
.
height
;
setState
(()
{
_aspectRatio
=
width
/
height
;
_hasError
=
false
;
});
},
onError:
(
dynamic
error
,
stackTrace
)
{
setState
(()
{
_hasError
=
true
;
});
},
),
);
}
@override
Widget
build
(
BuildContext
context
)
{
// Nếu load lỗi => hiển thị errorWidget nếu có
if
(
_hasError
)
{
return
widget
.
errorWidget
??
Container
(
color:
Colors
.
grey
.
shade200
,
alignment:
Alignment
.
center
,
child:
const
Text
(
"Load ảnh lỗi"
),
);
}
// Nếu chưa có _aspectRatio => hiển thị placeholder (progress, v.v.)
if
(
_aspectRatio
==
null
)
{
return
widget
.
placeholder
??
Container
(
color:
Colors
.
grey
.
shade200
,
height:
200
,
alignment:
Alignment
.
center
,
child:
const
CircularProgressIndicator
(),
);
}
// Có aspectRatio => hiển thị ảnh với AspectRatio
return
AspectRatio
(
aspectRatio:
_aspectRatio
!,
child:
Image
.
network
(
widget
.
imageUrl
,
fit:
BoxFit
.
cover
,
// Nếu ảnh load xong aspect ratio nhưng khi vẽ vẫn lỗi => fallback
errorBuilder:
(
context
,
error
,
stackTrace
)
{
return
widget
.
errorWidget
??
Container
(
color:
Colors
.
grey
.
shade200
,
alignment:
Alignment
.
center
,
child:
const
Text
(
"Load ảnh lỗi"
),
);
},
),
);
}
}
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