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
a030a6e7
Commit
a030a6e7
authored
Jan 15, 2026
by
DatHV
Browse files
update fix bug, selected avatar image
parent
682ab1de
Changes
35
Show whitespace changes
Inline
Side-by-side
lib/features/daily_checkin/daily_checkin_viewmodel.dart
View file @
a030a6e7
import
'package:get/get_rx/src/rx_types/rx_types.dart'
;
import
'package:mypoint_flutter_app/core/network/restful_api_client_all_request.dart'
;
import
'../../core/network/restful_api_viewmodel.dart'
;
import
'../../shared/preferences/point/point_manager.dart'
;
import
'daily_checkin_models.dart'
;
class
DailyCheckInViewModel
extends
RestfulApiViewModel
{
...
...
@@ -8,8 +9,10 @@ class DailyCheckInViewModel extends RestfulApiViewModel {
final
Rxn
<
SubmitCheckInData
>
submitData
=
Rxn
<
SubmitCheckInData
>();
void
Function
(
String
message
,
bool
onBack
)?
onShowAlertError
;
void
Function
(
SubmitCheckInData
?
data
)?
submitDataResponse
;
bool
_userClickDailyCheckIn
=
false
;
bool
get
todayIsChecked
{
if
(
_userClickDailyCheckIn
)
return
true
;
final
counter
=
checkInData
.
value
?.
dailyCounter
;
final
items
=
counter
?.
values
??
[];
return
(
items
.
firstOrNull
?.
counterValue
??
''
)
==
'1'
;
...
...
@@ -37,6 +40,8 @@ class DailyCheckInViewModel extends RestfulApiViewModel {
await
callApi
<
SubmitCheckInData
>(
request:
()
=>
client
.
submitCheckIn
(),
onSuccess:
(
data
,
_
)
{
UserPointManager
().
fetchUserPoint
();
_userClickDailyCheckIn
=
true
;
submitData
.
value
=
data
;
submitDataResponse
?.
call
(
data
);
_rewardOpportunityGetList
();
// Refresh data after successful check-in
...
...
lib/features/data_network_service/data_network_service_screen.dart
View file @
a030a6e7
...
...
@@ -258,7 +258,7 @@ class _DataNetworkServiceScreenState extends BaseState<DataNetworkServiceScreen>
selectedBrand:
_viewModel
.
selectedBrand
.
value
,
onSelected:
(
brand
)
{
Navigator
.
pop
(
context
);
if
(
brand
.
id
!
=
_viewModel
.
selectedBrand
.
value
?.
id
)
return
;
if
(
brand
.
id
=
=
_viewModel
.
selectedBrand
.
value
?.
id
)
return
;
_viewModel
.
selectedProduct
.
value
=
null
;
_viewModel
.
selectedBrand
.
value
=
brand
;
_viewModel
.
getTelcoDetail
();
...
...
lib/features/home/custom_widget/header_home_widget.dart
View file @
a030a6e7
...
...
@@ -113,7 +113,12 @@ class HomeGreetingHeader extends StatelessWidget {
],
),
const
SizedBox
(
height:
2
),
Row
(
Align
(
alignment:
Alignment
.
centerLeft
,
child:
SingleChildScrollView
(
scrollDirection:
Axis
.
horizontal
,
physics:
const
BouncingScrollPhysics
(),
child:
Row
(
mainAxisAlignment:
MainAxisAlignment
.
start
,
children:
[
Obx
(()
{
...
...
@@ -134,6 +139,8 @@ class HomeGreetingHeader extends StatelessWidget {
_buildStatItem
(
icon:
"assets/images/ic_rank_gray.png"
,
value:
level
,
onTap:
_onRankTap
),
],
),
),
),
],
),
),
...
...
lib/features/otp/otp_screen.dart
View file @
a030a6e7
...
...
@@ -19,12 +19,14 @@ class OtpScreen extends BaseScreen {
class
_OtpScreenState
extends
BaseState
<
OtpScreen
>
with
BasicState
{
final
TextEditingController
_pinController
=
TextEditingController
();
late
final
OtpViewModel
_otpVM
;
@override
void
initState
()
{
super
.
initState
();
final
OtpViewModel
otpVM
=
Get
.
put
(
OtpViewModel
(
widget
.
repository
));
ever
(
otpVM
.
errorMessage
,
(
value
)
{
Get
.
delete
<
OtpViewModel
>(
force:
true
);
_otpVM
=
Get
.
put
(
OtpViewModel
(
widget
.
repository
));
ever
(
_otpVM
.
errorMessage
,
(
value
)
{
if
(
value
.
toString
().
isNotEmpty
)
{
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
showAlertError
(
content:
value
);
...
...
@@ -35,7 +37,6 @@ class _OtpScreenState extends BaseState<OtpScreen> with BasicState {
@override
Widget
createBody
()
{
final
otpVM
=
Get
.
put
(
OtpViewModel
(
widget
.
repository
));
return
Scaffold
(
appBar:
CustomNavigationBar
(
title:
''
,
...
...
@@ -53,13 +54,13 @@ class _OtpScreenState extends BaseState<OtpScreen> with BasicState {
children:
[
const
Text
(
"Nhập mã xác thực OTP"
,
style:
TextStyle
(
fontSize:
32
,
fontWeight:
FontWeight
.
bold
)),
const
SizedBox
(
height:
12
),
_buildWelcomeText
(
otpVM
),
_buildWelcomeText
(
_
otpVM
),
const
SizedBox
(
height:
32
),
_buildPinCodeFields
(
otpVM
),
_buildPinCodeFields
(
_
otpVM
),
const
SizedBox
(
height:
16
),
_buildErrorText
(
otpVM
),
_buildErrorText
(
_
otpVM
),
const
SizedBox
(
height:
16
),
_buildResendOtp
(
otpVM
),
_buildResendOtp
(
_
otpVM
),
],
),
),
...
...
lib/features/otp/otp_viewmodel.dart
View file @
a030a6e7
...
...
@@ -72,7 +72,10 @@ class OtpViewModel extends GetxController {
if
(
response
.
isSuccess
)
{
errorMessage
.
value
=
""
;
}
else
{
errorMessage
.
value
=
response
.
errorMessage
??
""
;
errorMessage
.
value
=
response
.
errorMessage
??
response
.
message
??
Constants
.
commonError
;
}
}
catch
(
e
)
{
// Bắt lỗi do repository throw
...
...
lib/features/personal/personal_edit_item_model.dart
View file @
a030a6e7
...
...
@@ -5,6 +5,18 @@ import 'package:mypoint_flutter_app/features/personal/personal_gender.dart';
import
'../../app/config/callbacks.dart'
;
import
'../location_address/location_address_viewmodel.dart'
;
class
UpdateImageResponseModel
{
final
String
?
imageId
;
UpdateImageResponseModel
({
this
.
imageId
});
factory
UpdateImageResponseModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
UpdateImageResponseModel
(
imageId:
json
[
'image_id'
],
);
}
}
enum
SectionPersonalEditType
{
name
,
nickname
,
...
...
@@ -56,8 +68,8 @@ class PersonalEditDataModel {
"address_province_code"
:
province
?.
code
??
""
,
"identification_number"
:
identificationNumber
??
""
,
"email"
:
email
??
""
,
"avatar"
:
""
,
"avatar_2"
:
""
,
"avatar"
:
avatar
??
""
,
"avatar_2"
:
avatar
??
""
,
};
}
...
...
lib/features/personal/personal_edit_screen.dart
View file @
a030a6e7
import
'dart:convert'
;
import
'dart:io'
;
import
'dart:typed_data'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'package:image_picker/image_picker.dart'
;
import
'package:flutter_image_compress/flutter_image_compress.dart'
;
import
'package:mypoint_flutter_app/shared/preferences/data_preference.dart'
;
import
'package:path_provider/path_provider.dart'
;
import
'package:permission_handler/permission_handler.dart'
;
import
'package:mypoint_flutter_app/features/personal/personal_edit_item_model.dart'
;
import
'package:mypoint_flutter_app/features/personal/personal_edit_viewmodel.dart'
;
import
'package:mypoint_flutter_app/features/personal/personal_gender.dart'
;
import
'package:mypoint_flutter_app/features/personal/widgets/avatar_picker_sheet.dart'
;
import
'package:mypoint_flutter_app/shared/widgets/image_loader.dart'
;
import
'package:mypoint_flutter_app/core/services/web/web_helper.dart'
;
import
'../../core/services/web/web_info_data.dart'
;
import
'../../shared/widgets/base_view/base_screen.dart'
;
import
'../../shared/widgets/base_view/basic_state.dart'
;
...
...
@@ -108,39 +119,214 @@ class _PersonalEditScreenState extends BaseState<PersonalEditScreen> with BasicS
}
Widget
_buildAvatarItem
()
{
final
avatar
=
WebData
.
getAvatar
();
final
avatar
=
viewModel
.
editDataModel
.
value
?.
avatar
;
final
fallbackAvatar
=
WebData
.
getAvatar
();
return
Center
(
child:
GestureDetector
(
onTap:
()
{
_showAvatarPicker
();
},
child:
Stack
(
alignment:
Alignment
.
bottomRight
,
children:
[
ClipOval
(
child:
loadNetwork
Image
(
url:
a
vatar
,
child:
_buildAvatar
Image
(
avatar
?.
isNotEmpty
==
true
?
avatar
:
fallbackA
vatar
,
width:
100
,
height:
100
,
fit:
BoxFit
.
cover
,
placeholderAsset:
"assets/images/bg_default_11.png"
)
),
Positioned
(
bottom:
4
,
right:
4
,
child:
GestureDetector
(
onTap:
()
{
debugPrint
(
"Change avatar tapped"
);
},
child:
Container
(
padding:
const
EdgeInsets
.
all
(
4
),
decoration:
const
BoxDecoration
(
color:
Colors
.
red
,
shape:
BoxShape
.
circle
),
child:
const
Icon
(
Icons
.
camera_alt
,
color:
Colors
.
white
,
size:
18
),
),
),
),
],
),
),
);
}
Widget
_buildAvatarImage
(
String
?
avatar
,
{
double
?
width
,
double
?
height
,
String
placeholderAsset
=
"assets/images/bg_default_11.png"
,
})
{
if
(
avatar
==
null
||
avatar
.
isEmpty
)
{
return
Image
.
asset
(
placeholderAsset
,
fit:
BoxFit
.
cover
,
width:
width
,
height:
height
,
);
}
if
(
avatar
.
contains
(
"avatar-"
))
{
return
Image
.
asset
(
"assets/images/
$avatar
"
,
fit:
BoxFit
.
cover
,
width:
width
,
height:
height
,
);
}
if
(
avatar
.
startsWith
(
"data:image"
))
{
final
bytes
=
_decodeBase64Image
(
avatar
);
if
(
bytes
!=
null
)
{
return
Image
.
memory
(
bytes
,
fit:
BoxFit
.
cover
,
width:
width
,
height:
height
,
);
}
return
Image
.
asset
(
placeholderAsset
,
fit:
BoxFit
.
cover
,
width:
width
,
height:
height
,
);
}
return
loadNetworkImage
(
url:
avatar
,
width:
width
,
height:
height
,
fit:
BoxFit
.
cover
,
placeholderAsset:
placeholderAsset
,
);
}
Uint8List
?
_decodeBase64Image
(
String
value
)
{
try
{
final
data
=
value
.
split
(
','
).
last
;
return
base64Decode
(
data
);
}
catch
(
_
)
{
return
null
;
}
}
void
_showAvatarPicker
()
{
print
(
"_showAvatarPicker
${viewModel.editDataModel.value?.avatar}
"
);
AvatarPickerSheet
.
show
(
context:
context
,
onCameraTap:
()
{
_pickImage
(
"camera"
);
},
onGalleryTap:
()
{
_pickImage
(
"gallery"
);
},
onAvatarSelected:
(
avatarPath
)
{
viewModel
.
editDataModel
.
value
?.
avatar
=
avatarPath
;
viewModel
.
editDataModel
.
refresh
();
},
selectedAvatar:
viewModel
.
editDataModel
.
value
?.
avatar
,
);
}
Future
<
void
>
_pickImage
(
String
type
)
async
{
print
(
"_pickImage _pickImage
$type
"
);
if
(
kIsWeb
)
{
final
result
=
await
webOpenPickerImage
(
type
);
final
imageValue
=
_extractImageValue
(
result
);
final
normalized
=
_normalizeImageValue
(
imageValue
);
if
(
normalized
==
null
||
normalized
.
isEmpty
)
{
return
;
}
viewModel
.
editDataModel
.
value
?.
avatar
=
normalized
;
viewModel
.
editDataModel
.
refresh
();
return
;
}
final
granted
=
await
_requestNativePermission
(
type
);
if
(!
granted
)
{
debugPrint
(
"🚫 Permission denied for
$type
"
);
return
;
}
final
picker
=
ImagePicker
();
final
source
=
type
==
"camera"
?
ImageSource
.
camera
:
ImageSource
.
gallery
;
final
picked
=
await
picker
.
pickImage
(
source
:
source
);
if
(
picked
==
null
)
{
return
;
}
final
uploadPath
=
await
_ensureUploadPng
(
picked
.
path
);
print
(
"_pickImage type:
$type
, path:
$uploadPath
"
);
await
viewModel
.
uploadAvatarAndSet
(
uploadPath
);
}
Future
<
String
>
_ensureUploadPng
(
String
path
)
async
{
try
{
final
bytes
=
await
File
(
path
).
readAsBytes
();
final
png
=
await
FlutterImageCompress
.
compressWithList
(
bytes
,
format:
CompressFormat
.
png
,
quality:
30
,
minWidth:
200
,
minHeight:
200
,
);
if
(
png
.
isEmpty
)
{
return
path
;
}
final
dir
=
await
getTemporaryDirectory
();
final
fileName
=
"avatar_
${DateTime.now().millisecondsSinceEpoch}
.png"
;
final
outFile
=
File
(
"
${dir.path}
/
$fileName
"
);
await
outFile
.
writeAsBytes
(
png
,
flush:
true
);
return
outFile
.
path
;
}
catch
(
_
)
{
return
path
;
}
}
Future
<
bool
>
_requestNativePermission
(
String
type
)
async
{
if
(
type
==
"camera"
)
{
final
status
=
await
Permission
.
camera
.
request
();
return
status
.
isGranted
;
}
final
photosStatus
=
await
Permission
.
photos
.
request
();
if
(
photosStatus
.
isGranted
)
{
return
true
;
}
final
storageStatus
=
await
Permission
.
storage
.
request
();
return
storageStatus
.
isGranted
;
}
String
?
_extractImageValue
(
dynamic
result
)
{
if
(
result
is
String
)
{
return
result
;
}
if
(
result
is
Map
)
{
final
data
=
result
[
"data"
]
??
result
[
"image"
]
??
result
;
if
(
data
is
String
)
{
return
data
;
}
if
(
data
is
Map
)
{
final
base64
=
data
[
"base64"
];
if
(
base64
is
String
&&
base64
.
isNotEmpty
)
{
return
base64
;
}
final
path
=
data
[
"path"
];
if
(
path
is
String
&&
path
.
isNotEmpty
)
{
return
path
;
}
}
}
return
null
;
}
String
?
_normalizeImageValue
(
String
?
value
)
{
if
(
value
==
null
||
value
.
isEmpty
)
return
null
;
if
(
value
.
startsWith
(
"data:image"
)
||
value
.
startsWith
(
"http"
)
||
value
.
startsWith
(
"assets/"
))
{
return
value
;
}
if
(
value
.
startsWith
(
"/"
)
||
value
.
startsWith
(
"file:"
))
{
return
value
;
}
return
"data:image/jpeg;base64,
$value
"
;
}
Future
<
void
>
_onTapItemChangeValue
(
PersonalEditItemModel
item
)
async
{
if
(
item
.
sectionType
==
SectionPersonalEditType
.
province
||
item
.
sectionType
==
SectionPersonalEditType
.
district
)
{
viewModel
.
navigateToLocationScreen
(
item
);
...
...
lib/features/personal/personal_edit_viewmodel.dart
View file @
a030a6e7
...
...
@@ -60,6 +60,7 @@ class PersonalEditViewModel extends RestfulApiViewModel {
address:
address
,
province:
province
.
value
,
district:
district
.
value
,
avatar:
profile
.
workerSite
?.
avatar2
,
);
isValidate
.
value
=
validate
();
}
...
...
@@ -89,6 +90,32 @@ class PersonalEditViewModel extends RestfulApiViewModel {
break
;
}
}
Future
<
void
>
uploadAvatarAndSet
(
String
imagePath
)
async
{
final
feedbackId
=
DataPreference
.
instance
.
profile
?.
workerSite
?.
id
??
""
;
if
(
feedbackId
.
isEmpty
)
{
editDataModel
.
value
?.
avatar
=
imagePath
;
editDataModel
.
refresh
();
return
;
}
showLoading
();
try
{
final
res
=
await
client
.
uploadImage
(
imagePath
,
feedbackId
);
hideLoading
();
final
imageId
=
res
.
data
?.
imageId
;
print
(
"uploadAvatarAndSet imageId
${imageId}
"
);
if
(
res
.
isSuccess
&&
imageId
!=
null
&&
imageId
.
isNotEmpty
)
{
editDataModel
.
value
?.
avatar
=
imageId
;
editDataModel
.
refresh
();
}
else
{
onShowAlertError
?.
call
(
res
.
errorMessage
??
Constants
.
commonError
);
}
}
catch
(
_
)
{
hideLoading
();
onShowAlertError
?.
call
(
Constants
.
commonError
);
}
}
Future
<
void
>
updateProfile
()
async
{
showLoading
();
try
{
...
...
lib/features/personal/personal_screen.dart
View file @
a030a6e7
...
...
@@ -36,10 +36,15 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState, Po
void
initState
()
{
super
.
initState
();
_appInfoFuture
=
Future
.
wait
([
AppInfoHelper
.
version
,
AppInfoHelper
.
buildNumber
]);
_pointManager
.
fetchUserPoint
();
runPopupCheck
(
DirectionalScreenName
.
personal
);
}
@override
void
onRouteDidAppear
()
{
super
.
onRouteDidAppear
();
_pointManager
.
fetchUserPoint
();
}
@override
Widget
createBody
()
{
return
Scaffold
(
...
...
@@ -78,7 +83,6 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState, Po
final
level
=
DataPreference
.
instance
.
rankName
??
"Hạng Đồng"
;
final
email
=
DataPreference
.
instance
.
profile
?.
workerSite
?.
email
??
""
;
final
topWebPadding
=
Constants
.
extendTopPaddingNavigation
;
final
avatar
=
WebData
.
getAvatar
();
return
Container
(
decoration:
BoxDecoration
(
image:
DecorationImage
(
image:
NetworkImage
(
data
.
background
??
""
),
fit:
BoxFit
.
cover
)),
child:
SafeArea
(
...
...
@@ -102,13 +106,7 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState, Po
shape:
BoxShape
.
circle
,
border:
Border
.
all
(
color:
Colors
.
white
,
width:
2
),
),
child:
ClipOval
(
child:
loadNetworkImage
(
url:
avatar
,
fit:
BoxFit
.
cover
,
placeholderAsset:
"assets/images/ic_logo.png"
)
),
child:
ClipOval
(
child:
_buildAvatarWidget
()),
),
const
SizedBox
(
width:
8
),
Expanded
(
...
...
@@ -215,6 +213,30 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState, Po
);
}
Widget
_buildAvatarWidget
()
{
final
avatar
=
WebData
.
getAvatar
();
final
localAvatar
=
DataPreference
.
instance
.
profile
?.
workerSite
?.
avatar2
??
""
;
if
(
localAvatar
.
isEmpty
)
{
return
loadNetworkImage
(
url:
avatar
,
fit:
BoxFit
.
cover
,
placeholderAsset:
"assets/images/ic_logo.png"
,
);
}
final
assetsAvatar
=
"assets/images/
$localAvatar
"
;
return
Image
.
asset
(
assetsAvatar
,
fit:
BoxFit
.
cover
,
errorBuilder:
(
context
,
error
,
stackTrace
)
{
return
loadNetworkImage
(
url:
avatar
,
fit:
BoxFit
.
cover
,
placeholderAsset:
"assets/images/ic_logo.png"
,
);
},
);
}
Widget
_buildMenuItems
()
{
var
menuItems
=
[
{
...
...
@@ -252,8 +274,7 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState, Po
menuItems
.
insertAll
(
2
,
[
{
'icon'
:
Icons
.
qr_code_2
,
'title'
:
'QR Code'
,
'type'
:
'APP_SCREEN_QR_CODE'
,},
{
'icon'
:
Icons
.
border_right
,
'title'
:
'Hoá đơn điện'
,
'type'
:
'APP_SCREEN_LIST_PAYMENT_OF_ELECTRIC'
,},
]
);
]);
}
return
Container
(
color:
Colors
.
white
,
...
...
lib/features/personal/widgets/avatar_picker_sheet.dart
0 → 100644
View file @
a030a6e7
import
'package:flutter/material.dart'
;
import
'package:mypoint_flutter_app/core/services/web/web_helper.dart'
;
class
AvatarPickerSheet
{
static
const
List
<
String
>
localAvatars
=
[
"avatar-1.png"
,
"avatar-2.png"
,
"avatar-3.png"
,
"avatar-4.png"
,
"avatar-5.png"
,
"avatar-6.png"
,
];
static
String
_avatarAssetPath
(
String
filename
)
=>
"assets/images/
$filename
"
;
static
void
show
({
required
BuildContext
context
,
required
VoidCallback
onCameraTap
,
required
VoidCallback
onGalleryTap
,
required
ValueChanged
<
String
>
onAvatarSelected
,
String
?
selectedAvatar
,
})
{
showModalBottomSheet
(
context:
context
,
backgroundColor:
Colors
.
white
,
isScrollControlled:
true
,
shape:
const
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
vertical
(
top:
Radius
.
circular
(
16
))),
builder:
(
_
)
{
return
SafeArea
(
top:
false
,
child:
Padding
(
padding:
const
EdgeInsets
.
fromLTRB
(
24
,
12
,
24
,
24
),
child:
Column
(
mainAxisSize:
MainAxisSize
.
min
,
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Stack
(
alignment:
Alignment
.
center
,
children:
[
const
Center
(
child:
Text
(
"Ảnh đại diện"
,
style:
TextStyle
(
fontSize:
18
,
fontWeight:
FontWeight
.
bold
),
),
),
Align
(
alignment:
Alignment
.
centerRight
,
child:
GestureDetector
(
onTap:
()
=>
Navigator
.
of
(
context
).
pop
(),
child:
const
Icon
(
Icons
.
close
,
size:
20
),
),
),
],
),
const
SizedBox
(
height:
36
),
Row
(
children:
[
_OptionButton
(
icon:
Icons
.
camera_alt
,
label:
"Chụp ảnh"
,
onTap:
()
{
Navigator
.
of
(
context
).
pop
();
_requestImagePermission
(
"camera"
);
onCameraTap
();
},
),
const
SizedBox
(
width:
24
),
_OptionButton
(
icon:
Icons
.
photo_library
,
label:
"Thư viện"
,
onTap:
()
{
Navigator
.
of
(
context
).
pop
();
_requestImagePermission
(
"gallery"
);
onGalleryTap
();
},
),
],
),
const
SizedBox
(
height:
24
),
const
Text
(
"Thư viện biểu cảm"
,
style:
TextStyle
(
fontSize:
14
,
fontWeight:
FontWeight
.
w600
)),
const
SizedBox
(
height:
8
),
LayoutBuilder
(
builder:
(
context
,
constraints
)
{
const
crossAxisCount
=
3
;
const
spacing
=
36.0
;
final
itemSize
=
(
constraints
.
maxWidth
-
(
crossAxisCount
-
1
)
*
spacing
)
/
crossAxisCount
;
return
GridView
.
builder
(
shrinkWrap:
true
,
physics:
const
NeverScrollableScrollPhysics
(),
itemCount:
localAvatars
.
length
,
gridDelegate:
const
SliverGridDelegateWithFixedCrossAxisCount
(
crossAxisCount:
crossAxisCount
,
crossAxisSpacing:
spacing
,
mainAxisSpacing:
spacing
,
childAspectRatio:
1
,
),
itemBuilder:
(
context
,
index
)
{
final
avatarFile
=
localAvatars
[
index
];
final
avatarPath
=
_avatarAssetPath
(
avatarFile
);
final
isSelected
=
avatarFile
==
selectedAvatar
;
return
GestureDetector
(
onTap:
()
{
Navigator
.
of
(
context
).
pop
();
onAvatarSelected
(
avatarFile
);
},
child:
Stack
(
alignment:
Alignment
.
center
,
children:
[
ClipOval
(
child:
Image
.
asset
(
avatarPath
,
width:
itemSize
,
height:
itemSize
,
fit:
BoxFit
.
cover
,
),
),
if
(
isSelected
)
Container
(
width:
itemSize
,
height:
itemSize
,
decoration:
BoxDecoration
(
color:
Colors
.
black
.
withOpacity
(
0.35
),
shape:
BoxShape
.
circle
,
),
),
if
(
isSelected
)
const
Icon
(
Icons
.
check
,
color:
Colors
.
white
,
size:
30
),
],
),
);
},
);
},
),
const
SizedBox
(
height:
8
),
],
),
),
);
},
);
}
static
void
showCameraOptions
({
required
BuildContext
context
,
required
VoidCallback
onCamera
,
required
VoidCallback
onGallery
,
})
{
showModalBottomSheet
(
context:
context
,
backgroundColor:
Colors
.
white
,
shape:
const
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
vertical
(
top:
Radius
.
circular
(
16
))),
builder:
(
_
)
{
return
SafeArea
(
top:
false
,
child:
Column
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
ListTile
(
leading:
const
Icon
(
Icons
.
camera_alt
),
title:
const
Text
(
"Chụp ảnh"
),
onTap:
()
{
Navigator
.
of
(
context
).
pop
();
_requestImagePermission
(
"camera"
);
onCamera
();
},
),
ListTile
(
leading:
const
Icon
(
Icons
.
photo_library
),
title:
const
Text
(
"Chọn từ thư viện"
),
onTap:
()
{
Navigator
.
of
(
context
).
pop
();
_requestImagePermission
(
"gallery"
);
onGallery
();
},
),
const
SizedBox
(
height:
12
),
],
),
);
},
);
}
static
Future
<
bool
>
_requestImagePermission
(
String
type
)
async
{
final
result
=
await
webPermissionsRequest
(
type
);
final
granted
=
_isPermissionGranted
(
result
);
if
(!
granted
)
{
debugPrint
(
"🚫 Permission denied:
$type
"
);
}
return
granted
;
}
static
bool
_isPermissionGranted
(
dynamic
result
)
{
if
(
result
==
null
)
{
return
true
;
}
if
(
result
is
bool
)
{
return
result
;
}
if
(
result
is
String
)
{
final
normalized
=
result
.
toLowerCase
();
return
normalized
==
'granted'
||
normalized
==
'allow'
||
normalized
==
'allowed'
||
normalized
==
'true'
||
normalized
==
'1'
;
}
if
(
result
is
Map
)
{
final
status
=
result
[
'status'
]
??
result
[
'state'
]
??
result
[
'result'
];
if
(
status
is
String
)
{
final
normalized
=
status
.
toLowerCase
();
if
(
normalized
==
'granted'
||
normalized
==
'allow'
||
normalized
==
'allowed'
)
{
return
true
;
}
if
(
normalized
==
'denied'
||
normalized
==
'blocked'
)
{
return
false
;
}
}
final
granted
=
result
[
'granted'
]
??
result
[
'allow'
]
??
result
[
'allowed'
];
if
(
granted
is
bool
)
{
return
granted
;
}
if
(
granted
is
String
)
{
final
normalized
=
granted
.
toLowerCase
();
return
normalized
==
'true'
||
normalized
==
'1'
||
normalized
==
'granted'
;
}
}
return
true
;
}
}
class
_OptionButton
extends
StatelessWidget
{
const
_OptionButton
({
required
this
.
icon
,
required
this
.
label
,
required
this
.
onTap
,
});
final
IconData
icon
;
final
String
label
;
final
VoidCallback
onTap
;
@override
Widget
build
(
BuildContext
context
)
{
return
GestureDetector
(
onTap:
onTap
,
child:
Column
(
children:
[
Text
(
label
,
style:
const
TextStyle
(
fontSize:
14
,
fontWeight:
FontWeight
.
w600
)),
const
SizedBox
(
height:
8
),
Container
(
width:
64
,
height:
64
,
decoration:
BoxDecoration
(
color:
Colors
.
grey
.
shade200
,
shape:
BoxShape
.
circle
),
child:
Icon
(
icon
,
color:
Colors
.
grey
,
size:
24
),
),
],
),
);
}
}
lib/features/topup/topup_screen.dart
View file @
a030a6e7
...
...
@@ -110,11 +110,9 @@ class _PhoneTopUpScreenState extends BaseState<PhoneTopUpScreen> with BasicState
selectedBrand:
_viewModel
.
selectedBrand
.
value
,
onSelected:
(
brand
)
{
Navigator
.
pop
(
context
);
print
(
"BrandSelectShe 2222 2 et
${brand.name}
"
);
if
(
brand
.
id
!=
_viewModel
.
selectedBrand
.
value
?.
id
)
return
;
if
(
brand
.
id
==
_viewModel
.
selectedBrand
.
value
?.
id
)
return
;
_viewModel
.
selectedProduct
.
value
=
null
;
_viewModel
.
selectedBrand
.
value
=
brand
;
print
(
"BrandSelectSheet
${brand.name}
"
);
_viewModel
.
getTelcoDetail
();
},
),
...
...
lib/features/topup/topup_viewmodel.dart
View file @
a030a6e7
...
...
@@ -85,9 +85,9 @@ class TopUpViewModel extends RestfulApiViewModel {
}
Future
<
void
>
getTelcoDetail
({
String
?
selected
})
async
{
final
code
=
selectedBrand
.
value
?.
code
;
final
id
=
selectedBrand
.
value
?.
id
;
if
(
code
==
null
||
id
==
null
)
return
;
final
code
=
(
id
??
0
).
toString
();
if
(
id
==
null
)
return
;
void
makeSelected
(
List
<
ProductModel
>
list
)
{
bool
didSelect
=
false
;
if
(
selected
!=
null
&&
selected
.
isNotEmpty
)
{
...
...
lib/shared/widgets/custom_navigation_bar.dart
View file @
a030a6e7
...
...
@@ -55,7 +55,7 @@ class CustomNavigationBar extends StatelessWidget implements PreferredSizeWidget
}
final
double
totalTopPadding
=
statusBarHeight
+
extraWebPadding
;
final
bool
isHttp
=
bgImage
.
startsWith
(
'http://'
)
||
bgImage
.
startsWith
(
'https://'
);
final
paddingTitle
=
(
leftButtons
.
isNotEmpty
||
rightButtons
.
isNotEmpty
)
?
48
.0
:
16.0
;
//
cách 2 đầu
final
paddingTitle
=
(
leftButtons
.
isNotEmpty
||
rightButtons
.
isNotEmpty
)
?
72
.0
:
16.0
;
//
tránh đè lên nút 2 bên
return
Container
(
height:
totalTopPadding
+
kToolbarHeight
,
decoration:
BoxDecoration
(
color:
bgImage
.
isEmpty
?
Colors
.
white
:
null
),
...
...
@@ -70,6 +70,8 @@ class CustomNavigationBar extends StatelessWidget implements PreferredSizeWidget
bottom:
false
,
child:
Padding
(
padding:
EdgeInsets
.
only
(
top:
extraWebPadding
),
child:
SizedBox
(
height:
kToolbarHeight
,
child:
Stack
(
alignment:
Alignment
.
center
,
children:
[
...
...
@@ -81,23 +83,35 @@ class CustomNavigationBar extends StatelessWidget implements PreferredSizeWidget
minFontSize:
12
,
stepGranularity:
0.1
,
overflow:
TextOverflow
.
visible
,
// giữ nguyên như bạn đang dùng
textAlign:
TextAlign
.
center
,
style:
const
TextStyle
(
fontSize:
18
,
// 👈 cỡ tối đa mong muốn
fontSize:
18
,
fontWeight:
FontWeight
.
w800
,
color:
Colors
.
white
,
),
),
),
if
(
leftButtons
.
isNotEmpty
)
Positioned
(
left:
12
,
top:
Constants
.
extendTopPaddingNavigationButton
,
child:
Row
(
mainAxisSize:
MainAxisSize
.
min
,
children:
leftButtons
)),
Align
(
alignment:
Alignment
.
centerLeft
,
child:
Padding
(
padding:
const
EdgeInsets
.
only
(
left:
12
),
child:
Row
(
mainAxisSize:
MainAxisSize
.
min
,
children:
leftButtons
),
),
),
if
(
rightButtons
.
isNotEmpty
)
Positioned
(
right:
8
,
top:
Constants
.
extendTopPaddingNavigationButton
,
child:
Row
(
mainAxisSize:
MainAxisSize
.
min
,
children:
rightButtons
)),
Align
(
alignment:
Alignment
.
centerRight
,
child:
Padding
(
padding:
const
EdgeInsets
.
only
(
right:
8
),
child:
Row
(
mainAxisSize:
MainAxisSize
.
min
,
children:
rightButtons
),
),
),
],
),
),
),
),
],
),
);
...
...
lib/shared/widgets/custom_search_navigation_bar.dart
View file @
a030a6e7
...
...
@@ -25,8 +25,11 @@ class CustomSearchNavigationBar extends StatefulWidget implements PreferredSizeW
@override
Size
get
preferredSize
{
final
dispatcher
=
WidgetsBinding
.
instance
.
platformDispatcher
;
final
view
=
dispatcher
.
implicitView
??
(
dispatcher
.
views
.
isNotEmpty
?
dispatcher
.
views
.
first
:
null
);
double
paddingTop
=
view
!=
null
?
MediaQueryData
.
fromView
(
view
).
padding
.
top
:
0.0
;
final
view
=
dispatcher
.
implicitView
??
(
dispatcher
.
views
.
isNotEmpty
?
dispatcher
.
views
.
first
:
null
);
double
paddingTop
=
view
!=
null
?
MediaQueryData
.
fromView
(
view
).
padding
.
top
:
0.0
;
if
(
paddingTop
==
0
&&
kIsWeb
)
{
paddingTop
=
Constants
.
extendTopPaddingNavigation
;
}
...
...
@@ -74,12 +77,18 @@ class _CustomSearchNavigationBarState extends State<CustomSearchNavigationBar> {
children:
[
if
(
bgImage
.
isNotEmpty
)
isHttp
?
loadNetworkImage
(
url:
bgImage
,
fit:
BoxFit
.
cover
,
placeholderAsset:
_defaultBgImage
)
?
loadNetworkImage
(
url:
bgImage
,
fit:
BoxFit
.
cover
,
placeholderAsset:
_defaultBgImage
,
)
:
Image
.
asset
(
_defaultBgImage
,
fit:
BoxFit
.
cover
),
SafeArea
(
bottom:
false
,
child:
Padding
(
padding:
EdgeInsets
.
only
(
top:
extraWebPadding
),
child:
SizedBox
(
height:
kToolbarHeight
,
child:
Stack
(
alignment:
Alignment
.
center
,
children:
[
...
...
@@ -89,7 +98,10 @@ class _CustomSearchNavigationBarState extends State<CustomSearchNavigationBar> {
child:
Container
(
height:
36
,
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
8
),
decoration:
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
circular
(
12
)),
decoration:
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
circular
(
12
),
),
child:
Row
(
children:
[
const
Icon
(
Icons
.
search
,
size:
20
),
...
...
@@ -122,13 +134,30 @@ class _CustomSearchNavigationBarState extends State<CustomSearchNavigationBar> {
),
),
),
if
(
widget
.
showBackButton
)
Positioned
(
left:
12
,
top:
Constants
.
extendTopPaddingNavigationButton
,
child:
CustomBackButton
()),
if
(
widget
.
showBackButton
)
Align
(
alignment:
Alignment
.
centerLeft
,
child:
Padding
(
padding:
const
EdgeInsets
.
only
(
left:
12
),
child:
CustomBackButton
(),
),
),
if
(
widget
.
rightButtons
.
isNotEmpty
)
Positioned
(
right:
12
,
top:
Constants
.
extendTopPaddingNavigationButton
,
child:
Row
(
mainAxisSize:
MainAxisSize
.
min
,
children:
widget
.
rightButtons
)),
Align
(
alignment:
Alignment
.
centerRight
,
child:
Padding
(
padding:
const
EdgeInsets
.
only
(
right:
12
),
child:
Row
(
mainAxisSize:
MainAxisSize
.
min
,
children:
widget
.
rightButtons
,
),
),
),
],
),
),
),
),
],
),
);
...
...
pubspec.yaml
View file @
a030a6e7
...
...
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version
:
1.21.13+20251231
01
version
:
2.01.01+20260108
01
environment
:
sdk
:
^3.7.0
...
...
@@ -54,12 +54,15 @@ dependencies:
dotted_border
:
^3.1.0
flutter_contacts
:
^1.1.6
permission_handler
:
^12.0.1
image_picker
:
^1.1.2
share_plus
:
^12.0.0
file_saver
:
^0.3.1
flutter_branch_sdk
:
^8.0.1
month_picker_dialog
:
marquee
:
^2.2.3
image_gallery_saver
:
^2.0.3
flutter_image_compress
:
^2.4.0
path_provider
:
^2.1.5
fl_chart
:
^1.1.0
mobile_scanner
:
^7.0.1
pointycastle
:
^3.9.1
...
...
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