Commit a030a6e7 authored by DatHV's avatar DatHV
Browse files

update fix bug, selected avatar image

parent 682ab1de
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
......
......@@ -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();
......
......@@ -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),
],
),
),
),
],
),
),
......
......@@ -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),
],
),
),
......
......@@ -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
......
......@@ -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 ?? "",
};
}
......
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: loadNetworkImage(
url: avatar,
child: _buildAvatarImage(
avatar?.isNotEmpty == true ? avatar : fallbackAvatar,
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);
......
......@@ -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 {
......
......@@ -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,
......
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),
),
],
),
);
}
}
......@@ -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();
},
),
......
......@@ -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) {
......
......@@ -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),
),
),
],
),
),
),
),
],
),
);
......
......@@ -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,
),
),
),
],
),
),
),
),
],
),
);
......
......@@ -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+2025123101
version: 2.01.01+2026010801
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
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment