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
560b49dd
Commit
560b49dd
authored
Apr 28, 2025
by
DatHV
Browse files
update voucher detail
parent
00fbcfd0
Changes
13
Hide whitespace changes
Inline
Side-by-side
lib/configs/api_paths.dart
View file @
560b49dd
...
...
@@ -28,4 +28,5 @@ class APIPaths {
static
const
String
getProducts
=
"/product/api/v2.0/products"
;
static
const
String
getSearchProducts
=
"/product/api/v2.0/products/search"
;
static
const
String
getProductDetail
=
"/product/api/v2.0/products/%@"
;
static
const
String
getProductStores
=
"/product/api/v2.0/product/stores"
;
}
\ No newline at end of file
lib/extensions/datetime_extensions.dart
0 → 100644
View file @
560b49dd
import
'package:intl/intl.dart'
;
extension
DateTimeFormatExtension
on
DateTime
{
/// Convert DateTime → String theo định dạng truyền vào
String
toFormattedString
({
String
format
=
"dd/MM/yyyy"
})
{
final
formatter
=
DateFormat
(
format
);
return
formatter
.
format
(
this
);
}
}
\ No newline at end of file
lib/extensions/string_extension.dart
View file @
560b49dd
...
...
@@ -2,7 +2,6 @@ import 'dart:convert';
import
'package:crypto/crypto.dart'
;
import
'package:flutter/material.dart'
;
import
'package:intl/intl.dart'
as
intl
;
import
'date_format.dart'
;
extension
PhoneValidator
on
String
{
bool
isPhoneValid
()
{
...
...
@@ -34,13 +33,13 @@ Color parseHexColor(String hexString, {Color fallbackColor = Colors.grey}) {
}
extension
StringDateExtension
on
String
{
DateTime
?
toDate
(
{
required
String
format
}
)
{
DateTime
?
toDate
()
{
if
(
trim
().
isEmpty
)
return
null
;
try
{
return
intl
.
DateFormat
(
format
).
parseStrict
(
this
);
return
DateTime
.
parse
(
this
);
// 🚀 Dùng Dart core parse luôn đúng
}
catch
(
e
)
{
print
(
'❌ Date parse failed for "
$this
"
with format "
$format
"
:
$e
'
);
print
(
'❌ Date parse failed for "
$this
":
$e
'
);
return
null
;
}
}
}
\ No newline at end of file
}
lib/networking/restful_api_request.dart
View file @
560b49dd
...
...
@@ -20,6 +20,7 @@ import '../screen/otp/model/otp_verify_response_model.dart';
import
'../screen/pageDetail/model/campaign_detail_model.dart'
;
import
'../screen/pageDetail/model/detail_page_rule_type.dart'
;
import
'../screen/splash/splash_screen_viewmodel.dart'
;
import
'../screen/voucher/models/product_store_model.dart'
;
import
'../screen/voucher/models/search_product_response_model.dart'
;
import
'model_maker.dart'
;
...
...
@@ -287,4 +288,12 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
(
data
)
=>
ProductModel
.
fromJson
(
data
as
Json
),
);
}
}
Future
<
BaseResponseModel
<
List
<
ProductStoreModel
>>>
getProductStores
(
int
id
)
async
{
final
body
=
{
"product_id"
:
id
,
"size"
:
20
,
"index"
:
0
};
return
requestNormal
(
APIPaths
.
getProductStores
,
Method
.
GET
,
body
,
(
data
)
{
final
list
=
data
as
List
<
dynamic
>;
return
list
.
map
((
e
)
=>
ProductStoreModel
.
fromJson
(
e
)).
toList
();
});
}
}
\ No newline at end of file
lib/screen/voucher/detail/store_list_section.dart
0 → 100644
View file @
560b49dd
import
'package:flutter/material.dart'
;
import
'../models/product_store_model.dart'
;
class
StoreListSection
extends
StatelessWidget
{
final
List
<
ProductStoreModel
>
stores
;
const
StoreListSection
({
Key
?
key
,
required
this
.
stores
})
:
super
(
key:
key
);
@override
Widget
build
(
BuildContext
context
)
{
if
(
stores
.
isEmpty
)
{
return
const
SizedBox
.
shrink
();
}
return
Container
(
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
8
),
padding:
const
EdgeInsets
.
all
(
16
),
decoration:
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
circular
(
16
),
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
const
Text
(
'Địa điểm áp dụng:'
,
style:
TextStyle
(
fontWeight:
FontWeight
.
bold
,
fontSize:
16
),
),
const
SizedBox
(
height:
12
),
...
stores
.
map
((
store
)
=>
_buildStoreItem
(
store
)).
toList
(),
],
),
);
}
Widget
_buildStoreItem
(
ProductStoreModel
store
)
{
return
Padding
(
padding:
const
EdgeInsets
.
only
(
bottom:
12
),
child:
Row
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
// Logo Brand
ClipOval
(
child:
Image
.
network
(
""
,
width:
20
,
height:
20
,
fit:
BoxFit
.
cover
,
errorBuilder:
(
_
,
__
,
___
)
=>
Image
.
asset
(
'assets/images/sample.png'
,
width:
20
,
height:
20
),
),
),
const
SizedBox
(
width:
8
),
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
store
.
name
??
''
,
style:
const
TextStyle
(
fontWeight:
FontWeight
.
w600
),
),
const
SizedBox
(
height:
4
),
Row
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
const
Icon
(
Icons
.
location_on_outlined
,
size:
18
,
color:
Colors
.
grey
),
const
SizedBox
(
width:
4
),
Expanded
(
child:
Text
(
store
.
address
??
''
,
style:
const
TextStyle
(
color:
Colors
.
grey
,
fontSize:
13
),
maxLines:
2
,
overflow:
TextOverflow
.
ellipsis
,
),
),
],
),
],
),
),
],
),
);
}
}
lib/screen/voucher/detail/voucher_detail_screen.dart
View file @
560b49dd
import
'dart:math'
;
import
'package:flutter/material.dart'
;
import
'package:flutter_widget_from_html/flutter_widget_from_html.dart'
;
import
'package:mypoint_flutter_app/extensions/date_format.dart'
;
import
'package:mypoint_flutter_app/extensions/num_extension.dart'
;
import
'package:mypoint_flutter_app/extensions/string_extension.dart'
;
import
'package:mypoint_flutter_app/screen/voucher/detail/store_list_section.dart'
;
import
'../../../resouce/base_color.dart'
;
import
'../../../widgets/back_button.dart'
;
import
'../../../widgets/custom_empty_widget.dart'
;
import
'../../../widgets/custom_price_tag.dart'
;
import
'../../../widgets/dashed_line.dart'
;
import
'../../../widgets/image_loader.dart'
;
import
'../../../widgets/measure_size.dart'
;
import
'../models/product_model.dart'
;
import
'voucher_detail_viewmodel.dart'
;
import
'package:get/get.dart'
;
...
...
@@ -18,6 +23,8 @@ class VoucherDetailScreen extends StatefulWidget {
class
_VoucherDetailScreenState
extends
State
<
VoucherDetailScreen
>
{
late
final
int
productId
;
late
final
VoucherDetailViewModel
_viewModel
;
final
GlobalKey
_infoKey
=
GlobalKey
();
double
_infoHeight
=
0
;
@override
void
initState
()
{
...
...
@@ -29,7 +36,6 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
productId
=
args
[
'productId'
];
}
_viewModel
=
Get
.
put
(
VoucherDetailViewModel
(
productId:
productId
));
_viewModel
.
getProductDetail
();
}
@override
...
...
@@ -41,113 +47,151 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
}
final
product
=
_viewModel
.
product
.
value
;
if
(
product
==
null
)
{
return
const
Center
(
child:
Text
(
"Không tìm thấy sản phẩm"
));
return
const
Center
(
child:
EmptyWidget
(
));
}
return
Stack
(
children:
[
SingleChildScrollView
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
_buildHeader
(
product
),
_buildInfo
(
product
),
_buildDetailBlock
(
product
),
_buildConditionBlock
(
product
),
],
),
),
SafeArea
(
child:
Padding
(
padding:
const
EdgeInsets
.
all
(
8
),
child:
CustomBackButton
(),
Container
(
color:
Colors
.
grey
.
shade100
,
child:
SingleChildScrollView
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
_buildHeaderWithInfo
(
product
),
SizedBox
(
height:
max
(
_infoHeight
-
36
,
0
)),
_buildDetailBlock
(
product
),
_buildConditionBlock
(
product
),
SizedBox
(
height:
8
),
StoreListSection
(
stores:
_viewModel
.
stores
),
],
),
),
),
SafeArea
(
child:
Padding
(
padding:
const
EdgeInsets
.
all
(
8
),
child:
CustomBackButton
())),
],
);
}),
);
}
Widget
_buildHeader
(
ProductModel
product
)
{
return
AspectRatio
(
aspectRatio:
16
/
9
,
child:
Image
.
network
(
product
.
banner
?.
url
??
''
,
fit:
BoxFit
.
cover
,
errorBuilder:
(
_
,
__
,
___
)
=>
Image
.
asset
(
'assets/images/sample.png'
,
fit:
BoxFit
.
cover
),
),
Widget
_buildHeaderWithInfo
(
ProductModel
product
)
{
final
double
screenWidth
=
MediaQuery
.
of
(
context
).
size
.
width
;
final
double
imageHeight
=
screenWidth
/
(
16
/
9
);
return
Stack
(
clipBehavior:
Clip
.
none
,
children:
[
loadNetworkImage
(
url:
product
.
banner
?.
url
,
fit:
BoxFit
.
cover
,
height:
imageHeight
,
width:
double
.
infinity
,
placeholderAsset:
'assets/images/sample.png'
,
),
Positioned
(
left:
16
,
right:
16
,
child:
MeasureSize
(
onChange:
(
size
)
{
if
(
_infoHeight
!=
size
.
height
)
{
setState
(()
{
_infoHeight
=
size
.
height
;
});
}
},
child:
Transform
.
translate
(
offset:
Offset
(
0
,
imageHeight
-
36
),
// ✅ Lấn lên trên ảnh 36px
child:
_buildInfo
(
product
),
),
),
),
],
);
}
Widget
_buildInfo
(
ProductModel
product
)
{
Widget
_buildInfo
(
ProductModel
product
,
{
Key
?
key
}
)
{
return
Container
(
key:
key
,
padding:
const
EdgeInsets
.
all
(
16
),
decoration:
const
BoxDecoration
(
decoration:
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
vertical
(
top:
Radius
.
circular
(
16
)),
borderRadius:
BorderRadius
.
circular
(
16
),
boxShadow:
[
BoxShadow
(
color:
Colors
.
black
.
withOpacity
(
0.05
),
blurRadius:
8
,
offset:
const
Offset
(
0
,
4
))],
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
crossAxisAlignment:
CrossAxisAlignment
.
center
,
children:
[
Text
(
product
.
content
?.
name
??
''
,
style:
const
TextStyle
(
fontSize:
2
0
,
fontWeight:
FontWeight
.
bold
)),
Text
(
product
.
content
?.
name
??
''
,
style:
const
TextStyle
(
fontSize:
2
4
,
fontWeight:
FontWeight
.
bold
)),
const
SizedBox
(
height:
8
),
Row
(
children:
[
const
Text
(
'Hạn dùng: '
,
style:
TextStyle
(
color:
Colors
.
grey
)),
Text
(
product
.
expire
??
""
,
style:
const
TextStyle
(
color:
Colors
.
red
)),
],
),
if
(!(
product
.
inStock
??
true
))
Container
(
margin:
const
EdgeInsets
.
only
(
top:
8
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
,
vertical:
4
),
decoration:
BoxDecoration
(
color:
Colors
.
grey
.
shade300
,
borderRadius:
BorderRadius
.
circular
(
8
)),
child:
const
Text
(
'Tạm hết'
,
style:
TextStyle
(
color:
Colors
.
red
)),
),
const
Divider
(
height:
24
),
_buildExpireAndStock
(
product
),
const
SizedBox
(
height:
16
),
DashedLine
(
color:
Colors
.
grey
.
shade400
,
dashWidth:
3
,
dashSpacing:
3
,
height:
1
),
const
Divider
(
height:
16
),
Row
(
children:
[
CircleAvatar
(
radius:
16
,
backgroundImage:
NetworkImage
(
product
.
brand
?.
logo
??
''
),
radius:
12
,
backgroundColor:
Colors
.
transparent
,
child:
ClipOval
(
child:
loadNetworkImage
(
url:
product
.
brand
?.
logo
??
""
,
width:
24
,
height:
24
,
fit:
BoxFit
.
cover
,
placeholderAsset:
'assets/images/ic_logo.png'
,
),
),
),
const
SizedBox
(
width:
8
),
Expanded
(
child:
Text
(
product
.
brand
?.
name
??
''
,
style:
const
TextStyle
(
fontSize:
14
)),
),
_buildPointTag
(
product
),
Expanded
(
child:
Text
(
product
.
brand
?.
name
??
''
,
style:
const
TextStyle
(
fontSize:
14
))),
PriceTagWidget
(
point:
product
.
amountToBePaid
??
0
),
],
)
)
,
],
),
);
}
Widget
_buildPointTag
(
ProductModel
product
)
{
final
priceText
=
product
.
amountToBePaid
?.
money
()
??
""
;
final
isFree
=
(
product
.
price
?.
value
??
0
)
==
0
;
return
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
8
,
vertical:
2
),
decoration:
BoxDecoration
(
color:
isFree
?
Colors
.
orange
.
shade50
:
Colors
.
red
.
shade100
,
borderRadius:
BorderRadius
.
circular
(
12
),
),
child:
Row
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
Image
.
asset
(
'assets/images/ic_point.png'
,
width:
20
,
height:
20
),
const
SizedBox
(
width:
4
),
Widget
_buildExpireAndStock
(
ProductModel
product
)
{
final
bool
isOutOfStock
=
!(
product
.
inStock
??
true
);
final
bool
hasExpire
=
product
.
expire
.
isNotEmpty
;
return
Row
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
crossAxisAlignment:
CrossAxisAlignment
.
center
,
children:
[
if
(
hasExpire
)
Text
(
priceText
,
style:
TextStyle
(
'Hạn dùng: '
,
style:
const
TextStyle
(
color:
Colors
.
grey
,
fontWeight:
FontWeight
.
bold
,
fontSize:
12
,
color:
isFree
?
Colors
.
orange
:
Colors
.
red
,
fontWeight:
FontWeight
.
w500
,
),
),
],
),
if
(
hasExpire
)
Text
(
product
.
expire
,
style:
const
TextStyle
(
color:
Colors
.
red
,
fontWeight:
FontWeight
.
bold
,
fontSize:
12
,
),
),
if
(
isOutOfStock
)
Container
(
margin:
const
EdgeInsets
.
only
(
left:
8
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
,
vertical:
4
),
decoration:
BoxDecoration
(
color:
Colors
.
grey
,
borderRadius:
BorderRadius
.
circular
(
4
),
),
child:
const
Text
(
'Tạm hết'
,
style:
TextStyle
(
color:
Colors
.
white
,
fontSize:
12
,
fontWeight:
FontWeight
.
bold
),
),
),
],
);
}
...
...
@@ -161,8 +205,10 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
Widget
_buildTextBlock
(
String
title
,
String
?
content
)
{
if
(
content
==
null
||
content
.
isEmpty
)
return
const
SizedBox
();
return
Padding
(
padding:
const
EdgeInsets
.
fromLTRB
(
16
,
12
,
16
,
0
),
return
Container
(
padding:
const
EdgeInsets
.
all
(
16
),
margin:
const
EdgeInsets
.
only
(
top:
16
,
left:
16
,
right:
16
,
bottom:
0
),
decoration:
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
circular
(
16
)),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
...
...
lib/screen/voucher/detail/voucher_detail_viewmodel.dart
View file @
560b49dd
...
...
@@ -2,21 +2,23 @@ import 'package:get/get.dart';
import
'package:mypoint_flutter_app/networking/restful_api_request.dart'
;
import
'../../../base/restful_api_viewmodel.dart'
;
import
'../models/product_model.dart'
;
import
'../models/product_store_model.dart'
;
class
VoucherDetailViewModel
extends
RestfulApiViewModel
{
final
int
productId
;
VoucherDetailViewModel
({
required
this
.
productId
});
var
stores
=
RxList
<
ProductStoreModel
>();
var
product
=
Rxn
<
ProductModel
>();
var
isLoading
=
false
.
obs
;
@override
void
onInit
()
{
super
.
onInit
();
getProductDetail
();
_getProductDetail
();
_getProductStores
();
}
Future
<
void
>
getProductDetail
()
async
{
Future
<
void
>
_
getProductDetail
()
async
{
if
(
isLoading
.
value
)
return
;
try
{
isLoading
.
value
=
true
;
...
...
@@ -29,4 +31,13 @@ class VoucherDetailViewModel extends RestfulApiViewModel {
}
}
Future
<
void
>
_getProductStores
()
async
{
try
{
final
response
=
await
client
.
getProductStores
(
productId
);
stores
.
value
=
response
.
data
??
[];
}
catch
(
error
)
{
print
(
"Error fetching product detail:
$error
"
);
}
finally
{
}
}
}
lib/screen/voucher/models/product_model.dart
View file @
560b49dd
import
'package:json_annotation/json_annotation.dart'
;
import
'package:mypoint_flutter_app/extensions/date_format.dart'
;
import
'package:mypoint_flutter_app/extensions/datetime_extensions.dart'
;
import
'package:mypoint_flutter_app/extensions/string_extension.dart'
;
import
'package:mypoint_flutter_app/screen/voucher/models/product_brand_model.dart'
;
import
'package:mypoint_flutter_app/screen/voucher/models/product_content_model.dart'
;
import
'package:mypoint_flutter_app/screen/voucher/models/product_customer_info_model.dart'
;
...
...
@@ -49,8 +52,9 @@ class ProductModel {
return
content
?.
name
;
}
String
?
get
expire
{
return
isMyProduct
?
itemModel
?.
expireTime
:
expireTime
;
String
get
expire
{
final
ex
=
(
isMyProduct
?
itemModel
?.
expireTime
:
expireTime
)
??
""
;
return
ex
.
toDate
()?.
toFormattedString
()
??
""
;
}
int
?
get
amountToBePaid
{
...
...
lib/screen/voucher/models/product_store_model.dart
0 → 100644
View file @
560b49dd
import
'package:json_annotation/json_annotation.dart'
;
part
'product_store_model.g.dart'
;
@JsonSerializable
()
class
ProductStoreModel
{
final
int
?
id
;
final
String
?
code
;
final
String
?
name
;
final
String
?
phone
;
final
String
?
email
;
final
String
?
fax
;
final
String
?
address
;
final
double
?
longitude
;
final
double
?
latitude
;
final
double
?
distance
;
@JsonKey
(
name:
'district_id'
)
final
String
?
districtId
;
@JsonKey
(
name:
'district_name'
)
final
String
?
districtName
;
@JsonKey
(
name:
'city_id'
)
final
String
?
cityId
;
@JsonKey
(
name:
'city_name'
)
final
String
?
cityName
;
ProductStoreModel
({
this
.
id
,
this
.
code
,
this
.
name
,
this
.
phone
,
this
.
email
,
this
.
fax
,
this
.
address
,
this
.
longitude
,
this
.
latitude
,
this
.
distance
,
this
.
districtId
,
this
.
districtName
,
this
.
cityId
,
this
.
cityName
,
});
factory
ProductStoreModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$ProductStoreModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$ProductStoreModelToJson
(
this
);
String
get
displayDistance
{
final
doubleValue
=
distance
??
0
;
if
(
doubleValue
==
0
)
{
return
""
;
}
return
"
${doubleValue.toStringAsFixed(1)}
km"
;
}
}
\ No newline at end of file
lib/screen/voucher/models/product_store_model.g.dart
0 → 100644
View file @
560b49dd
// GENERATED CODE - DO NOT MODIFY BY HAND
part of
'product_store_model.dart'
;
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ProductStoreModel
_$ProductStoreModelFromJson
(
Map
<
String
,
dynamic
>
json
)
=>
ProductStoreModel
(
id:
(
json
[
'id'
]
as
num
?)?.
toInt
(),
code:
json
[
'code'
]
as
String
?,
name:
json
[
'name'
]
as
String
?,
phone:
json
[
'phone'
]
as
String
?,
email:
json
[
'email'
]
as
String
?,
fax:
json
[
'fax'
]
as
String
?,
address:
json
[
'address'
]
as
String
?,
longitude:
(
json
[
'longitude'
]
as
num
?)?.
toDouble
(),
latitude:
(
json
[
'latitude'
]
as
num
?)?.
toDouble
(),
distance:
(
json
[
'distance'
]
as
num
?)?.
toDouble
(),
districtId:
json
[
'district_id'
]
as
String
?,
districtName:
json
[
'district_name'
]
as
String
?,
cityId:
json
[
'city_id'
]
as
String
?,
cityName:
json
[
'city_name'
]
as
String
?,
);
Map
<
String
,
dynamic
>
_$ProductStoreModelToJson
(
ProductStoreModel
instance
)
=>
<
String
,
dynamic
>{
'id'
:
instance
.
id
,
'code'
:
instance
.
code
,
'name'
:
instance
.
name
,
'phone'
:
instance
.
phone
,
'email'
:
instance
.
email
,
'fax'
:
instance
.
fax
,
'address'
:
instance
.
address
,
'longitude'
:
instance
.
longitude
,
'latitude'
:
instance
.
latitude
,
'distance'
:
instance
.
distance
,
'district_id'
:
instance
.
districtId
,
'district_name'
:
instance
.
districtName
,
'city_id'
:
instance
.
cityId
,
'city_name'
:
instance
.
cityName
,
};
lib/screen/voucher/sub_widget/voucher_item_grid.dart
View file @
560b49dd
import
'package:flutter/material.dart'
;
import
'package:mypoint_flutter_app/extensions/num_extension.dart'
;
import
'package:get/get.dart'
;
import
'package:get/get_core/src/get_main.dart'
;
import
'../../../shared/router_gage.dart'
;
import
'../../../widgets/custom_price_tag.dart'
;
import
'../../../widgets/image_loader.dart'
;
import
'../models/product_model.dart'
;
...
...
@@ -9,23 +11,34 @@ class VoucherItemGrid extends StatelessWidget {
const
VoucherItemGrid
({
super
.
key
,
required
this
.
items
});
@override
Widget
build
(
BuildContext
context
)
{
Widget
build
(
BuildContext
context
)
{
final
double
itemWidth
=
MediaQuery
.
of
(
context
).
size
.
width
*
0.7
;
final
double
imageHeight
=
itemWidth
/
(
16
/
9
);
final
double
totalHeight
=
imageHeight
+
80
;
return
SizedBox
(
height:
totalHeight
,
child:
ListView
.
separated
(
scrollDirection:
Axis
.
horizontal
,
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
),
itemCount:
items
.
length
,
separatorBuilder:
(
context
,
index
)
=>
const
SizedBox
(
width:
12
),
itemBuilder:
(
context
,
index
)
=>
_VoucherGridItem
(
product:
items
[
index
],
itemWidth:
itemWidth
),
height:
totalHeight
,
child:
ListView
.
separated
(
scrollDirection:
Axis
.
horizontal
,
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
),
itemCount:
items
.
length
,
separatorBuilder:
(
context
,
index
)
=>
const
SizedBox
(
width:
12
),
itemBuilder:
(
context
,
index
)
{
final
product
=
items
[
index
];
return
GestureDetector
(
onTap:
()
{
Get
.
toNamed
(
voucherDetailScreen
,
arguments:
product
.
id
);
},
child:
_VoucherGridItem
(
product:
product
,
itemWidth:
itemWidth
,
),
);
},
),
);
}
}
class
_VoucherGridItem
extends
StatelessWidget
{
...
...
lib/widgets/dashed_line.dart
0 → 100644
View file @
560b49dd
import
'package:flutter/material.dart'
;
class
DashedLine
extends
StatelessWidget
{
final
double
height
;
final
double
dashWidth
;
final
double
dashSpacing
;
final
Color
color
;
const
DashedLine
({
Key
?
key
,
this
.
height
=
1
,
this
.
dashWidth
=
4
,
this
.
dashSpacing
=
4
,
this
.
color
=
Colors
.
grey
,
})
:
super
(
key:
key
);
@override
Widget
build
(
BuildContext
context
)
{
return
CustomPaint
(
painter:
_DashedLinePainter
(
color:
color
,
dashWidth:
dashWidth
,
dashSpacing:
dashSpacing
,
height:
height
,
),
size:
Size
(
double
.
infinity
,
height
),
);
}
}
class
_DashedLinePainter
extends
CustomPainter
{
final
double
dashWidth
;
final
double
dashSpacing
;
final
double
height
;
final
Color
color
;
_DashedLinePainter
({
required
this
.
dashWidth
,
required
this
.
dashSpacing
,
required
this
.
height
,
required
this
.
color
,
});
@override
void
paint
(
Canvas
canvas
,
Size
size
)
{
final
paint
=
Paint
()
..
color
=
color
..
strokeWidth
=
height
;
double
startX
=
0
;
while
(
startX
<
size
.
width
)
{
canvas
.
drawLine
(
Offset
(
startX
,
0
),
Offset
(
startX
+
dashWidth
,
0
),
paint
);
startX
+=
dashWidth
+
dashSpacing
;
}
}
@override
bool
shouldRepaint
(
CustomPainter
oldDelegate
)
=>
false
;
}
lib/widgets/measure_size.dart
0 → 100644
View file @
560b49dd
import
'package:flutter/cupertino.dart'
;
class
MeasureSize
extends
StatefulWidget
{
final
Widget
child
;
final
ValueChanged
<
Size
>
onChange
;
const
MeasureSize
({
super
.
key
,
required
this
.
child
,
required
this
.
onChange
});
@override
_MeasureSizeState
createState
()
=>
_MeasureSizeState
();
}
class
_MeasureSizeState
extends
State
<
MeasureSize
>
{
Size
?
oldSize
;
@override
Widget
build
(
BuildContext
context
)
{
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
final
contextBox
=
context
.
findRenderObject
()
as
RenderBox
?;
if
(
contextBox
!=
null
&&
contextBox
.
hasSize
)
{
final
newSize
=
contextBox
.
size
;
if
(
oldSize
!=
newSize
)
{
oldSize
=
newSize
;
widget
.
onChange
(
newSize
);
}
}
});
return
widget
.
child
;
}
}
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