Commit cc202df4 authored by DatHV's avatar DatHV
Browse files

update change id, popup common manager

parent 417358c5
import 'package:json_annotation/json_annotation.dart';
import 'package:mypoint_flutter_app/extensions/datetime_extensions.dart';
import 'package:mypoint_flutter_app/extensions/string_extension.dart';
part 'product_mobile_card_model.g.dart';
@JsonSerializable(explicitToJson: true)
......@@ -56,6 +58,11 @@ class ProductMobileCardModel {
_$ProductMobileCardModelFromJson(json);
Map<String, dynamic> toJson() => _$ProductMobileCardModelToJson(this);
String get expire {
final ex = endTime ?? "";
return ex.toDate()?.toFormattedString() ?? "";
}
}
@JsonSerializable()
......
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
import 'package:mypoint_flutter_app/extensions/num_extension.dart';
import 'package:mypoint_flutter_app/screen/mobile_card/product_mobile_card_viewmodel.dart';
import 'package:mypoint_flutter_app/screen/mobile_card/usable_mobile_card_popup.dart';
import 'package:mypoint_flutter_app/widgets/custom_app_bar.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../resources/base_color.dart';
import '../../widgets/alert/custom_alert_dialog.dart';
import '../../widgets/alert/data_alert_model.dart';
import '../../widgets/custom_navigation_bar.dart';
class ProductMobileCardScreen extends BaseScreen {
const ProductMobileCardScreen({super.key});
......@@ -45,7 +43,7 @@ class _ProductMobileCardScreenState extends BaseState<ProductMobileCardScreen> w
@override
Widget createBody() {
return Scaffold(
appBar: CustomAppBar.back(title: "Đổi mã thẻ nạp"),
appBar: CustomNavigationBar(title: "Đổi mã thẻ nạp"),
body: Obx(() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
......
......@@ -15,7 +15,7 @@ class OrderMenuScreen extends StatelessWidget {
OrderMenuScreen({super.key});
final List<_OrderMenuItem> items = [
_OrderMenuItem(title: 'Thẻ nạp của tôi', icon: Icons.credit_card, type: ''),
_OrderMenuItem(title: 'Thẻ nạp của tôi', icon: Icons.credit_card, type: 'VIEW_MY_MOBILE_CARD'),
_OrderMenuItem(title: 'Sổ sức khỏe điện tử', icon: Icons.medical_services_outlined, type: ''),
_OrderMenuItem(title: 'Dịch vụ giao thông', icon: Icons.traffic_outlined, type: 'APP_SCREEN_MY_VNTRA_PACKAGE'),
];
......
......@@ -5,12 +5,14 @@ import 'package:mypoint_flutter_app/extensions/num_extension.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../directional/directional_action_type.dart';
import '../../preference/package_info.dart';
import '../../preference/point/header_home_model.dart';
import '../../resources/base_color.dart';
import '../../shared/router_gage.dart';
import '../../widgets/alert/data_alert_model.dart';
import '../home/header_home_viewmodel.dart';
import '../popup_manager/popup_runner_helper.dart';
class PersonalScreen extends BaseScreen {
const PersonalScreen({super.key});
......@@ -19,7 +21,7 @@ class PersonalScreen extends BaseScreen {
State<PersonalScreen> createState() => _PersonalScreenState();
}
class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState {
class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState, PopupOnInit {
final _headerHomeVM = Get.find<HeaderHomeViewModel>();
String? _version, _buildNumber;
......@@ -28,6 +30,18 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState {
super.initState();
_loadAppInfo();
_headerHomeVM.freshData();
// WidgetsBinding.instance.addPostFrameCallback((_) async {
// if (!mounted) return;
// await PopupManagerViewModel.instance.ensureLoaded();
// final popup = PopupManagerViewModel.instance.getForScreen(DirectionalScreenName.personal.rawValue);
// final id = popup?.id ?? '';
// if (id.isEmpty || popup == null) return;
// await showPopupManagerScreen(
// context,
// modelPopup: popup,
// );
// });
runPopupCheck(DirectionalScreenName.personal);
}
Future<void> _loadAppInfo() async {
......@@ -189,7 +203,7 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState {
{'icon': Icons.receipt_long_outlined, 'title': 'Lịch sử giao dịch', 'sectionDivider': true, 'type': 'APP_SCREEN_TRANSACTION_HISTORIES'},
{'icon': Icons.history_outlined, 'title': 'Lịch sử điểm', 'type': 'APP_SCREEN_SURVERY_APP'},
{'icon': Icons.history_outlined, 'title': 'Lịch sử hoàn điểm', 'type': 'APP_SCREEN_REFUND_HISTORY'},
{'icon': Icons.account_balance_wallet_outlined, 'title': 'Quản lý tài khoản/thẻ', 'type': 'APP_SCREEN_ELECTRIC_BILL'},
{'icon': Icons.account_balance_wallet_outlined, 'title': 'Quản lý tài khoản/thẻ', 'type': 'BANK_ACCOUNT_MANAGER'},
{'icon': Icons.favorite_border, 'title': 'Yêu thích', 'type': 'APP_SCREEN_CATEGORY_TAB_FAVORITE'},
{'icon': Icons.shopping_bag_outlined, 'title': 'Đơn mua', 'sectionDivider': true, 'type': 'APP_SCREEN_ORDER_MENU'},
{'icon': Icons.info_outline, 'title': 'Giới thiệu MyPoint', 'sectionDivider': true, 'type': 'VIEW_WEBSITE_PAGE'},
......@@ -293,7 +307,8 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState {
});
final phone = DataPreference.instance.phoneNumberUsedForLoginScreen;
final displayName = DataPreference.instance.displayName;
if (phone != null && !found) {
print("Safe back to login screen with phone: $phone, displayName: $displayName, found: $found");
if (phone != null && found) {
Get.offAllNamed(loginScreen, arguments: {"phone": phone, 'fullName': displayName});
} else {
DataPreference.instance.clearData();
......
import 'package:json_annotation/json_annotation.dart';
import 'package:mypoint_flutter_app/directional/directional_screen.dart';
part 'popup_manager_model.g.dart';
@JsonSerializable()
......@@ -81,6 +82,13 @@ class PopupManagerModel {
this.requestId,
});
DirectionalScreen? get directional {
return DirectionalScreen.build(
clickActionType: clickActionType,
clickActionParam: clickActionParam,
);
}
factory PopupManagerModel.fromJson(Map<String, dynamic> json) =>
_$PopupManagerModelFromJson(json);
......
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:mypoint_flutter_app/screen/popup_manager/popup_manager_model.dart';
/// Giao diện điều hướng để bạn nối với router của app
typedef PopupNavigator = void Function({
required String name,
required String identifier,
String? title,
String? body,
});
import 'package:mypoint_flutter_app/screen/popup_manager/popup_manager_viewmodel.dart';
/// Logger tuỳ bạn hook vào hệ thống hiện có (Firebase, MoEngage, …)
void logPopupShowing({required String popupId, required String requestId}) {
......@@ -22,56 +15,42 @@ void logPopupClick({required String popupId}) {
debugPrint('[Popup] click: popup_id=$popupId');
}
/// ==== API hiển thị popup (gọi giống hàm Swift) ====
Future<void> showPopup(
BuildContext context, {
required PopupManagerModel modelPopup,
required PopupNavigator onNavigate,
VoidCallback? onDismissed, // thay cho NotificationCenter
}) async {
Future<void> showPopupManagerScreen(
BuildContext context, {
required PopupManagerModel modelPopup,
VoidCallback? onDismissed,
}) async {
int timeCountDown = int.tryParse(modelPopup.timeCountDown ?? '1000000') ?? 1000000;
final popupId = modelPopup.id ?? '';
final requestId = modelPopup.requestId ?? '';
logPopupShowing(popupId: popupId, requestId: requestId);
await PopupManagerViewModel.instance.markShownOnce(popupId);
await showGeneralDialog(
context: context,
barrierDismissible: false, // giống SwiftEntryKit, user không chạm ra ngoài để tắt
barrierDismissible: false,
barrierLabel: 'popup',
transitionDuration: const Duration(milliseconds: 220),
pageBuilder: (_, __, ___) {
return _BasePopupView(
model: modelPopup,
initialCountdown: timeCountDown,
onNavigate: onNavigate,
onDismissed: () {
onDismissed?.call();
},
);
},
transitionBuilder: (_, anim, __, child) {
return Transform.scale(
scale: 0.96 + (0.04 * anim.value),
child: Opacity(opacity: anim.value, child: child),
);
return Transform.scale(scale: 0.96 + (0.04 * anim.value), child: Opacity(opacity: anim.value, child: child));
},
);
}
/// ==== Widget nền của popup (tương đương BasePopupView + SwiftEntryKit display) ====
class _BasePopupView extends StatefulWidget {
final PopupManagerModel model;
final int initialCountdown;
final PopupNavigator onNavigate;
final VoidCallback onDismissed;
const _BasePopupView({
required this.model,
required this.initialCountdown,
required this.onNavigate,
required this.onDismissed,
});
const _BasePopupView({required this.model, required this.initialCountdown, required this.onDismissed});
@override
State<_BasePopupView> createState() => _BasePopupViewState();
......@@ -80,7 +59,7 @@ class _BasePopupView extends StatefulWidget {
class _BasePopupViewState extends State<_BasePopupView> {
Timer? _timer;
late int _countdown;
double? _imageAspectRatio; // width / height
double? _imageAspectRatio;
bool _imageLoaded = false;
@override
......@@ -113,35 +92,16 @@ class _BasePopupViewState extends State<_BasePopupView> {
if (mounted) Navigator.of(context).pop();
}
void _onImageTap() {
final model = widget.model;
if ((model.clickActionType ?? '').isEmpty) return;
_timer?.cancel();
logPopupClick(popupId: model.id ?? '');
// Điều hướng tương đương DirectionalScreen.begin(...)
widget.onNavigate(
name: model.clickActionType!,
identifier: model.clickActionParam ?? '',
title: model.popupTitleTemplate ?? '',
body: model.popupBodyTemplate ?? '',
void _onContentTap() {
logPopupClick(popupId: widget.model.id ?? '');
print(
'Popup clicked: ${widget.model.directional?.clickActionType ?? ''} - ${widget.model.directional?.clickActionParam ?? ''}',
);
_dismiss();
}
void _onCancelTap() async {
final model = widget.model;
if ((model.clickActionType ?? '') == 'VIEW_GIFT') {
// Show "GiftMessageView" dạng bottom sheet
await showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (_) => _GiftMessageSheet(model: model, onNavigate: widget.onNavigate),
);
}
_timer?.cancel();
_dismiss();
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.model.directional?.begin();
});
}
@override
......@@ -151,18 +111,10 @@ class _BasePopupViewState extends State<_BasePopupView> {
final body = (model.popupBodyTemplate ?? '').trim();
final hasTitle = title.isNotEmpty;
final hasBody = body.isNotEmpty;
// Tính phần height phụ theo Swift (55 + 50, trừ bớt khi ẩn)
int extra = 55 + 50;
if (!hasTitle) extra -= 55;
if (!hasBody) extra -= 50;
final media = MediaQuery.of(context);
final screenW = media.size.width;
final maxPopupHeight = media.size.height - 200;
// Card radius phụ thuộc điều kiện
final imageCornerRadius = (!hasTitle && !hasBody) ? 12.0 : 12.0; // ảnh trên cùng vẫn bo 12
final imageCornerRadius = 12.0; // ảnh trên cùng vẫn bo 12
final subHeaderRadius = (hasBody && !hasTitle) ? 12.0 : 0.0;
return Material(
......@@ -172,7 +124,6 @@ class _BasePopupViewState extends State<_BasePopupView> {
padding: const EdgeInsets.symmetric(horizontal: 20),
child: LayoutBuilder(
builder: (context, constraints) {
// Nếu đã biết tỷ lệ ảnh: tính height theo công thức Swift
double imageHeight = 0;
if (_imageAspectRatio != null && _imageAspectRatio! > 0) {
// image.size.height*(screenW - 40)/image.size.width
......@@ -182,79 +133,59 @@ class _BasePopupViewState extends State<_BasePopupView> {
final content = Column(
mainAxisSize: MainAxisSize.min,
children: [
// Nút đóng
Align(
alignment: Alignment.topRight,
child: IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: _onCancelTap,
),
child: IconButton(icon: const Icon(Icons.close, color: Colors.white), onPressed: _dismiss),
),
ConstrainedBox(
constraints: BoxConstraints(
maxHeight: maxPopupHeight,
minWidth: double.infinity,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Material(
color: Colors.white,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
// Ảnh
_buildImage(
url: model.imageURL,
heightHint: imageHeight > 0 ? imageHeight : null,
cornerRadius: imageCornerRadius,
),
// Header
if (hasTitle)
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
constraints: BoxConstraints(maxHeight: maxPopupHeight, minWidth: double.infinity),
child: GestureDetector(
onTap: _onContentTap,
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Material(
color: Colors.white,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
// Ảnh
_buildImage(
url: model.imageURL,
heightHint: imageHeight > 0 ? imageHeight : null,
cornerRadius: imageCornerRadius,
),
// Header
if (hasTitle)
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Text(
title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700),
),
),
),
// SubHeader
if (hasBody)
Container(
decoration: BoxDecoration(
color: const Color(0xFFF6F7F9),
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(subHeaderRadius),
// SubHeader
if (hasBody)
Container(
decoration: BoxDecoration(
color: const Color(0xFFF6F7F9),
borderRadius: BorderRadius.vertical(bottom: Radius.circular(subHeaderRadius)),
),
padding: const EdgeInsets.fromLTRB(16, 10, 16, 16),
child: Text(body, style: const TextStyle(fontSize: 15, height: 1.4)),
),
// Dòng trạng thái đếm ngược (tuỳ chọn)
Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 16),
child: Text(
body,
style: const TextStyle(fontSize: 15, height: 1.4),
),
),
// Dòng trạng thái đếm ngược (tuỳ chọn)
Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 16),
child: Text(
_countdown > 0
? '$_countdown seconds dismiss popup'
: 'Closing…',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
_countdown > 0 ? '$_countdown seconds dismiss popup' : 'Closing…',
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
textAlign: TextAlign.right,
),
textAlign: TextAlign.right,
),
),
],
],
),
),
),
),
......@@ -262,7 +193,6 @@ class _BasePopupViewState extends State<_BasePopupView> {
),
],
);
return content;
},
),
......@@ -271,14 +201,9 @@ class _BasePopupViewState extends State<_BasePopupView> {
);
}
Widget _buildImage({
required String? url,
double? heightHint,
required double cornerRadius,
}) {
Widget _buildImage({required String? url, double? heightHint, required double cornerRadius}) {
final imageUrl = (url ?? '').trim();
if (imageUrl.isEmpty) {
// Placeholder fallback
return Container(
height: 160,
color: const Color(0xFFE9ECF1),
......@@ -287,22 +212,19 @@ class _BasePopupViewState extends State<_BasePopupView> {
);
}
return GestureDetector(
onTap: _onImageTap,
child: ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(cornerRadius)),
child: _NetworkImageWithInfo(
imageUrl: imageUrl,
heightHint: heightHint,
onResolved: (width, height) {
if (!_imageLoaded && width > 0 && height > 0) {
setState(() {
_imageLoaded = true;
_imageAspectRatio = width / height; // width/height
});
}
},
),
return ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(cornerRadius)),
child: _NetworkImageWithInfo(
imageUrl: imageUrl,
heightHint: heightHint,
onResolved: (width, height) {
if (!_imageLoaded && width > 0 && height > 0) {
setState(() {
_imageLoaded = true;
_imageAspectRatio = width / height; // width/height
});
}
},
),
);
}
......@@ -313,12 +235,7 @@ class _NetworkImageWithInfo extends StatefulWidget {
final String imageUrl;
final double? heightHint;
final void Function(int width, int height) onResolved;
const _NetworkImageWithInfo({
required this.imageUrl,
required this.onResolved,
this.heightHint,
});
const _NetworkImageWithInfo({required this.imageUrl, required this.onResolved, this.heightHint});
@override
State<_NetworkImageWithInfo> createState() => _NetworkImageWithInfoState();
......@@ -363,90 +280,9 @@ class _NetworkImageWithInfoState extends State<_NetworkImageWithInfo> {
@override
Widget build(BuildContext context) {
if (_info == null && widget.heightHint == null) {
// loading skeleton
return AspectRatio(
aspectRatio: 16 / 9,
child: Container(color: const Color(0xFFE9ECF1)),
);
return AspectRatio(aspectRatio: 16 / 9, child: Container(color: const Color(0xFFE9ECF1)));
}
final height = widget.heightHint;
return Image.network(
widget.imageUrl,
height: height,
width: double.infinity,
fit: BoxFit.cover,
);
}
}
/// ==== GiftMessageView tương đương (bottom sheet) ====
class _GiftMessageSheet extends StatelessWidget {
final PopupManagerModel model;
final PopupNavigator onNavigate;
const _GiftMessageSheet({
required this.model,
required this.onNavigate,ff
});
@override
Widget build(BuildContext context) {
final radius = const Radius.circular(16);
return Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16 + 24),
child: SafeArea(
top: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(width: 36, height: 4, decoration: BoxDecoration(color: Colors.grey.shade300, borderRadius: BorderRadius.circular(2))),
const SizedBox(height: 12),
Text(
model.popupTitleTemplate?.isNotEmpty == true ? model.popupTitleTemplate! : 'Quà tặng của bạn',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700),
),
const SizedBox(height: 8),
Text(
model.popupBodyTemplate?.isNotEmpty == true ? model.popupBodyTemplate! : 'Nhấn "Sử dụng ngay" để tiếp tục.',
style: const TextStyle(fontSize: 14, color: Colors.black87),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
if ((model.clickActionType ?? '').isEmpty) {
Navigator.of(context).pop();
return;
}
onNavigate(
name: model.clickActionType!,
identifier: model.clickActionParam ?? '',
title: model.popupTitleTemplate ?? '',
body: model.popupBodyTemplate ?? '',
);
Navigator.of(context).pop();
},
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 14),
),
child: const Text('Sử dụng ngay'),
),
),
const SizedBox(height: 8),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Để sau'),
),
],
),
),
);
return Image.network(widget.imageUrl, height: height, width: double.infinity, fit: BoxFit.cover);
}
}
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import 'package:mypoint_flutter_app/screen/popup_manager/popup_manager_model.dart';
import '../../base/restful_api_viewmodel.dart';
class PopupManagerViewModel extends RestfulApiViewModel {
PopupManagerViewModel._();
static final PopupManagerViewModel instance = PopupManagerViewModel._();
final Set<String> _shownIds = {};
List<PopupManagerModel>? _popupData;
bool _loaded = false;
Future<void>? _loadingFuture;
Future<void> ensureLoaded() async {
if (_loaded) return;
if (_loadingFuture != null) {
return _loadingFuture;
}
_loadingFuture = _getPopupManagerDataInternal();
await _loadingFuture;
}
Future<void> _getPopupManagerDataInternal() async {
try {
const Duration(seconds: 3); // Giả lập thời gian tải dữ liệu
final response = await client.getPopupManagerCommonScreen();
_popupData = response.data ?? [];
// _popupData = [
// PopupManagerModel(
// id: '1',
// screenToShow: 'APP_SCREEN_HOME',
// clickActionType: 'VIEW_PRODUCT_VOUCHER',
// clickActionParam: '50760',
// posActionID: 'action1',
// posActionCode: 'code1',
// timeToShow: '10:00-18:00',
// timeCountDown: '30',
// hourStartInDay: '9',
// hourStopInDay: '17',
// afterPosID: 'pos1',
// afterPosCode: 'posCode1',
// afterPosName: 'POS Name 1',
// marketingRequestDescription: 'Marketing description here.',
// effectiveFromDate: '2023-01-01',
// effectiveToDate: '2023-12-31',
// scheduleRunTypeCode: 'daily',
// scheduleRunTypeName: 'Daily Schedule',
// scheduleAtTime: '12:00',
// popupTitleTemplate: 'APP_SCREEN_HOME',
// popupBodyTemplate: 'Enjoy your stay and check out our features.',
// imageID: 'image123',
// imageURL: 'https://picsum.photos/1200/800',
// ),
// PopupManagerModel(
// id: '2',
// screenToShow: 'APP_SCREEN_POINTBACK',
// clickActionType: 'APP_SCREEN_SIM_SERVICE',
// clickActionParam: 'https://example.com/settings',
// posActionID: 'action2',
// posActionCode: 'code2',
// timeToShow: '08:00-20:00',
// timeCountDown: '60',
// hourStartInDay: '8',
// hourStopInDay: '20',
// afterPosID: 'pos2',
// afterPosCode: 'posCode2',
// afterPosName: 'POS Name 2',
// marketingRequestDescription: 'Settings popup description.',
// effectiveFromDate: '2023-01-01',
// effectiveToDate: '2023-12-31',
// scheduleRunTypeCode: 'weekly',
// scheduleRunTypeName: 'Weekly Schedule',
// scheduleAtTime: '10:00',
// popupTitleTemplate: 'APP_SCREEN_POINTBACK',
// popupBodyTemplate: 'Check out the new settings options.',
// imageID: 'image456',
// imageURL: 'https://picsum.photos/1200/800',
// ),
// PopupManagerModel(
// id: '3',
// screenToShow: 'APP_SCREEN_PRODUCT_VOUCHER',
// clickActionType: 'APP_SCREEN_GIFTS',
// clickActionParam: 'Profile updated successfully.',
// posActionID: 'action3',
// posActionCode: 'code3',
// timeToShow: '09:00-21:00',
// timeCountDown: '45',
// hourStartInDay: '9',
// hourStopInDay: '21',
// afterPosID: 'pos3',
// afterPosCode: 'posCode3',
// afterPosName: 'POS Name 3',
// marketingRequestDescription: 'Profile update alert.',
// effectiveFromDate: '2023-01-01',
// effectiveToDate: '2023-12-31',
// scheduleRunTypeCode: 'monthly',
// scheduleRunTypeName: 'Monthly Schedule',
// scheduleAtTime: '15:00',
// popupTitleTemplate: 'APP_SCREEN_PRODUCT_VOUCHER',
// popupBodyTemplate: 'Your profile has been updated successfully.',
// imageID: 'image789',
// imageURL: 'https://picsum.photos/1200/800',
// ),
// PopupManagerModel(
// id: '4',
// screenToShow: 'APP_SCREEN_GAME_BUNDLE',
// clickActionType: 'APP_SCREEN_CAMPAIGN_WALKING',
// clickActionParam: '1',
// posActionID: 'action3',
// posActionCode: 'code3',
// timeToShow: '09:00-21:00',
// timeCountDown: '45',
// hourStartInDay: '9',
// hourStopInDay: '21',
// afterPosID: 'pos3',
// afterPosCode: 'posCode3',
// afterPosName: 'POS Name 3',
// marketingRequestDescription: 'Profile update alert.',
// effectiveFromDate: '2023-01-01',
// effectiveToDate: '2023-12-31',
// scheduleRunTypeCode: 'monthly',
// scheduleRunTypeName: 'Monthly Schedule',
// scheduleAtTime: '15:00',
// popupTitleTemplate: 'APP_SCREEN_GAME_BUNDLE',
// popupBodyTemplate: 'Your profile has been updated successfully.',
// imageID: 'image789',
// imageURL: 'https://picsum.photos/1200/800',
// ),
// PopupManagerModel(
// id: '5',
// screenToShow: 'APP_SCREEN_PERSONAL',
// clickActionType: 'APP_SCREEN_SIM_SERVICE',
// clickActionParam: 'Profile updated successfully.',
// posActionID: 'action3',
// posActionCode: 'code3',
// timeToShow: '09:00-21:00',
// timeCountDown: '45',
// hourStartInDay: '9',
// hourStopInDay: '21',
// afterPosID: 'pos3',
// afterPosCode: 'posCode3',
// afterPosName: 'POS Name 3',
// marketingRequestDescription: 'Profile update alert.',
// effectiveFromDate: '2023-01-01',
// effectiveToDate: '2023-12-31',
// scheduleRunTypeCode: 'monthly',
// scheduleRunTypeName: 'Monthly Schedule',
// scheduleAtTime: '15:00',
// popupTitleTemplate: 'APP_SCREEN_PERSONAL',
// popupBodyTemplate: 'Your profile has been updated successfully.',
// imageID: 'image789',
// imageURL: 'https://picsum.photos/1200/800',
// ),
// ];
_loaded = true;
} catch (e) {
_popupData = [];
_loaded = true;
rethrow;
} finally {
_loadingFuture = null;
}
}
PopupManagerModel? getForScreen(String screenName) {
if (_popupData == null || _popupData!.isEmpty) return null;
final idx = _popupData!.indexWhere(
(e) => (e.screenToShow ?? '').trim().toUpperCase() == screenName.trim().toUpperCase(),
);
if (idx < 0) return null;
final found = _popupData![idx];
if (_shownIds.contains(found.id)) return null;
return found;
}
Future<void> markShownOnce(String popupId) async {
_shownIds.add(popupId);
}
Future<void> reset() async {
_shownIds.clear();
_popupData = [];
_loaded = false;
_loadingFuture = null;
}
}
import 'package:flutter/material.dart';
import 'package:mypoint_flutter_app/screen/popup_manager/popup_manager_screen.dart';
import '../../directional/directional_action_type.dart';
import 'popup_manager_viewmodel.dart';
class PopupRunner {
static Future<void> _showIfAvailable(BuildContext context, {required String screenName}) async {
await PopupManagerViewModel.instance.ensureLoaded();
final popup = PopupManagerViewModel.instance.getForScreen(screenName,);
if (popup == null || (popup.id ?? '').isEmpty) return;
if (!context.mounted) return;
await showPopupManagerScreen(
context,
modelPopup: popup,
);
}
}
mixin PopupOnInit<T extends StatefulWidget> on State<T> {
bool _popupChecked = false;
void runPopupCheck(DirectionalScreenName directional) {
if (_popupChecked) return;
_popupChecked = true;
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (!mounted) return;
await PopupRunner._showIfAvailable(
context,
screenName: directional.rawValue,
);
});
}
}
\ No newline at end of file
// setting_screen.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:mypoint_flutter_app/screen/setting/setting_viewmodel.dart';
import '../../shared/router_gage.dart';
import '../../widgets/bottom_sheet_helper.dart';
import '../../widgets/custom_app_bar.dart';
import '../../widgets/custom_navigation_bar.dart';
import '../change_pass/change_pass_screen.dart';
import '../delete_account/delete_account_dialog.dart';
......@@ -32,7 +31,7 @@ class _SettingScreenState extends State<SettingScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar.back(title: "Cài đặt"),
appBar: CustomNavigationBar(title: "Cài đặt"),
backgroundColor: const Color(0xFFF5F6F7),
body: Column(
children: [
......
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/base/restful_api_viewmodel.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import 'package:mypoint_flutter_app/shared/router_gage.dart';
import '../../base/base_response_model.dart';
import '../../configs/constants.dart';
import '../../model/auth/profile_response_model.dart';
import '../../model/update_response_model.dart';
import '../../preference/data_preference.dart';
import '../../preference/point/point_manager.dart';
import '../main_tab_screen/main_tab_screen.dart';
import '../onboarding/onboarding_screen.dart';
import 'package:url_launcher/url_launcher.dart';
import '../popup_manager/popup_manager_viewmodel.dart';
class SplashScreenViewModel extends RestfulApiViewModel {
var infoAppUpdate = BaseResponseModel<UpdateResponseModel>().obs;
......@@ -35,7 +35,7 @@ class SplashScreenViewModel extends RestfulApiViewModel {
}
Future<void> getUserProfile() async {
if (!(await DataPreference.instance.logged)) {
if (!(DataPreference.instance.logged)) {
Get.toNamed(onboardingScreen);
return;
}
......@@ -44,20 +44,28 @@ class SplashScreenViewModel extends RestfulApiViewModel {
hideLoading();
final userProfile = value.data;
if (value.isSuccess && userProfile != null) {
await DataPreference.instance.saveUserProfile(userProfile);
await UserPointManager().fetchUserPoint();
Get.toNamed(mainScreen);
_freshDataAndToMainScreen(userProfile);
} else {
DataPreference.instance.clearLoginToken();
Get.toNamed(onboardingScreen);
}
});
}
void _freshDataAndToMainScreen(ProfileResponseModel userProfile) async {
WidgetsBinding.instance.addPostFrameCallback((_) async {
await DataPreference.instance.saveUserProfile(userProfile);
await UserPointManager().fetchUserPoint();
await PopupManagerViewModel.instance.ensureLoaded();
Get.toNamed(mainScreen);
});
}
}
class EmptyCodable {
EmptyCodable.fromJson(dynamic json);
Map<String, dynamic> toJson() {
return {};
}
}
\ No newline at end of file
}
......@@ -6,6 +6,7 @@ import 'package:mypoint_flutter_app/shared/router_gage.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../resources/base_color.dart';
import '../../widgets/back_button.dart';
import '../../widgets/custom_navigation_bar.dart';
import '../faqs/faqs_screen.dart';
import '../pageDetail/campaign_detail_screen.dart';
import '../pageDetail/model/detail_page_rule_type.dart';
......@@ -52,13 +53,7 @@ class _SupportScreenState extends State<SupportScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: CustomBackButton(),
title: const Text("Hỗ trợ", style: TextStyle(fontWeight: FontWeight.bold)),
backgroundColor: Colors.white,
foregroundColor: Colors.black,
elevation: 0,
),
appBar: CustomNavigationBar(title: "Hỗ trợ"),
body: Obx(() {
if (controller.supportItems.isEmpty) {
return const Center(child: CircularProgressIndicator());
......
import 'package:contacts_service/contacts_service.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
......@@ -8,6 +7,7 @@ import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../preference/data_preference.dart';
import '../../resources/base_color.dart';
import '../../shared/router_gage.dart';
import '../contacts/contacts_picker.dart';
import 'brand_select_sheet_widget.dart';
class PhoneTopUpScreen extends StatefulWidget {
......@@ -353,22 +353,19 @@ class _PhoneTopUpScreenState extends State<PhoneTopUpScreen> {
Future<void> pickContact(BuildContext context) async {
try {
// Gọi sẽ tự động hiện dialog yêu cầu quyền (nếu cần)
final Contact? contact = await ContactsService.openDeviceContactPicker();
if (contact != null && contact.phones != null && contact.phones!.isNotEmpty) {
String phone = contact.phones!.first.value ?? '';
phone = phone.replaceAll(RegExp(r'[\s\-\(\)]'), '');
_phoneController.text = phone;
_viewModel.phoneNumber.value = phone;
_viewModel.checkMobileNetwork();
} else {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text("Không tìm thấy số điện thoại hợp lệ")));
}
final contact = await showContactPicker(context);
if (contact == null) return;
if (contact.phones.isEmpty) return;
String phone = contact.phones.first.number;
phone = phone.replaceAll(RegExp(r'[\s\-\(\)]'), '');
_phoneController.text = phone;
_viewModel.phoneNumber.value = phone;
_viewModel.checkMobileNetwork();
} catch (e) {
print("❌ Lỗi khi truy cập danh bạ: $e");
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Không thể truy cập danh bạ")));
debugPrint('❌ pickContact error: $e');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Không thể truy cập danh bạ')),
);
}
}
}
......@@ -33,7 +33,7 @@ class _TransactionHistoryScreenState extends State<TransactionHistoryScreen> {
_buildCategoryTabs(),
_buildSummaryBox(summary),
const SizedBox(height: 8),
if (items.isEmpty) Expanded(child: Center(child: EmptyWidget(size: Size(200, 200)))),
if (items.isEmpty) Expanded(child: Center(child: EmptyWidget(size: Size(160, 160)))),
Expanded(
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
......
......@@ -4,16 +4,27 @@ import 'package:mypoint_flutter_app/shared/direction_google_map.dart';
import '../models/product_store_model.dart';
class StoreListSection extends StatelessWidget {
class StoreListSection extends StatefulWidget {
final List<ProductStoreModel> stores;
final String? brandLogo;
const StoreListSection({Key? key, required this.stores, this.brandLogo}) : super(key: key);
const StoreListSection({super.key, required this.stores, this.brandLogo});
@override
State<StoreListSection> createState() => _StoreListSectionState();
}
class _StoreListSectionState extends State<StoreListSection> {
bool _seeAllStore = false;
final int _numberOfStore = 4;
List<ProductStoreModel> get displayStores {
return _seeAllStore ? widget.stores : widget.stores.take(_numberOfStore).toList();
}
@override
Widget build(BuildContext context) {
return Obx(() {
if (stores.isEmpty) {
if (displayStores.isEmpty) {
return const SizedBox.shrink();
}
return Container(
......@@ -25,7 +36,7 @@ class StoreListSection extends StatelessWidget {
children: [
const Text('Địa điểm áp dụng:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
const SizedBox(height: 10),
...stores.map(
...displayStores.map(
(store) => InkWell(
onTap: () {
_onTapStore(store);
......@@ -33,6 +44,20 @@ class StoreListSection extends StatelessWidget {
child: _buildStoreItem(store),
),
),
if (widget.stores.length > _numberOfStore)
GestureDetector(
onTap: () {
setState(() {
_seeAllStore = !_seeAllStore;
});
},
child: Center(
child: Text(
_seeAllStore ? 'Thu gọn' : 'Xem tất cả',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Colors.blueAccent),
),
),
),
],
),
);
......@@ -53,7 +78,7 @@ class StoreListSection extends StatelessWidget {
children: [
ClipOval(
child: Image.network(
brandLogo ?? "",
widget.brandLogo ?? "",
width: 20,
height: 20,
fit: BoxFit.cover,
......@@ -61,25 +86,28 @@ class StoreListSection extends StatelessWidget {
),
),
const SizedBox(width: 8),
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,
),
child: Text(store.name ?? '', style: const TextStyle(fontWeight: FontWeight.w600), softWrap: true),
),
],
),
const SizedBox(height: 4),
if ((store.address ?? '').isNotEmpty)
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,
),
),
],
),
],
),
);
......
import '../../mobile_card/models/product_mobile_card_model.dart';
class MyVoucherResponse {
final int? listStart;
final int? listLimit;
final int? listTotal;
final List<ProductMobileCardModel>? listItems;
MyVoucherResponse({
this.listStart,
this.listLimit,
this.listTotal,
this.listItems,
});
factory MyVoucherResponse.fromJson(Map<String, dynamic> json) {
return MyVoucherResponse(
listStart: json['list_start'] as int?,
listLimit: json['list_limit'] as int?,
listTotal: json['list_total'] as int?,
listItems: (json['list_items'] as List<dynamic>?)
?.map((e) => ProductMobileCardModel.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
Map<String, dynamic> toJson() {
return {
'list_start': listStart,
'list_limit': listLimit,
'list_total': listTotal,
'list_items': listItems?.map((e) => e.toJson()).toList(),
};
}
}
\ No newline at end of file
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/configs/constants.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../../base/restful_api_viewmodel.dart';
import '../../mobile_card/models/product_mobile_card_model.dart';
import '../models/my_product_status_type.dart';
class MyMobileCardListViewModel extends RestfulApiViewModel {
final RxInt selectedTabIndex = 0.obs;
var myProducts = <ProductMobileCardModel>[].obs;
void Function(String message)? onShowAlertError;
@override
void onInit() {
super.onInit();
freshData(isRefresh: true);
}
void selectTab(int index) {
selectedTabIndex.value = index;
freshData(isRefresh: true);
}
void freshData({bool isRefresh = false}) {
final body = {
"index": isRefresh ? 0 : myProducts.length,
"size": 20,
};
final status = selectedTabIndex.value == 0 ? MyProductStatusType.waiting : MyProductStatusType.used;
client.getMyMobileCards(status, body).then((response) {
if (!response.isSuccess) {
onShowAlertError?.call(response.errorMessage ?? Constants.commonError);
}
final result = response.data?.listItems ?? [];
if (isRefresh) {
myProducts.clear();
}
myProducts.addAll(result);
}).catchError((error) {
myProducts.clear();
print('Error fetching products: $error');
});
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../widgets/custom_empty_widget.dart';
import '../../../widgets/custom_navigation_bar.dart';
import '../../../widgets/image_loader.dart';
import '../../mobile_card/models/product_mobile_card_model.dart';
import 'my_mobile_card_list_viewmodel.dart';
import 'package:dotted_border/dotted_border.dart';
class MyMobileCardListScreen extends StatefulWidget {
const MyMobileCardListScreen({super.key});
@override
State<MyMobileCardListScreen> createState() => _MyMobileCardListScreenState();
}
class _MyMobileCardListScreenState extends State<MyMobileCardListScreen> {
late final MyMobileCardListViewModel _viewModel = Get.put(MyMobileCardListViewModel());
@override
void initState() {
super.initState();
// _viewModel = Get.put(MyProductListViewModel());
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
return Scaffold(
appBar: CustomNavigationBar(title: 'Thẻ nạp của tôi',),
body: Obx(
() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [_buildTab('Đang có', 0), _buildTab('Đã sử dụng', 1)],
),
),
const Divider(height: 1),
if (_viewModel.myProducts.isEmpty)
Expanded(child: EmptyWidget(size: Size(screenWidth / 2, screenWidth / 2)))
else
Expanded(
child: RefreshIndicator(
onRefresh: () async {
_viewModel.freshData(isRefresh: true);
},
child: ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: _viewModel.myProducts.length,
itemBuilder: (_, index) {
if (index >= _viewModel.myProducts.length) {
_viewModel.freshData(isRefresh: false);
return const Center(
child: Padding(padding: EdgeInsets.all(16), child: CircularProgressIndicator()),
);
}
final product = _viewModel.myProducts[index];
return _buildVoucherItem(product);
},
),
),
),
],
),
),
);
}
Widget _buildTab(String title, int index) {
return GestureDetector(
onTap: () => _viewModel.selectTab(index),
child: Obx(
() => Column(
children: [
Text(
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: _viewModel.selectedTabIndex.value == index ? Colors.red : Colors.black54,
),
),
const SizedBox(height: 4),
if (_viewModel.selectedTabIndex.value == index) Container(height: 2, width: 60, color: Colors.red),
],
),
),
);
}
Widget _buildVoucherItem(ProductMobileCardModel product) {
return GestureDetector(
onTap: () {
// Get.toNamed(voucherDetailScreen, arguments: {"customerProductId": product.id});
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 8),
padding: const EdgeInsets.all(0),
child: DottedBorder(
color: Colors.redAccent.withOpacity(0.3),
borderType: BorderType.RRect,
radius: const Radius.circular(12),
dashPattern: const [3, 3],
strokeWidth: 1,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.redAccent.withOpacity(0.03),
),
padding: const EdgeInsets.all(12),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: loadNetworkImage(
url: product.brandLogo ?? '',
width: 64,
height: 64,
fit: BoxFit.cover,
placeholderAsset: "assets/images/bg_default_11.png",
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.brandName ?? '',
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.black54, fontSize: 12),
),
const SizedBox(height: 6),
Text(product.name ?? '', style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500)),
const SizedBox(height: 6),
Text('HSD: ${product.expire}', style: const TextStyle(color: Colors.black54, fontSize: 12)),
],
),
),
],
),
),
),
),
);
}
}
......@@ -99,12 +99,16 @@ class _MyVoucherListScreenState extends State<MyVoucherListScreen> {
margin: const EdgeInsets.symmetric(vertical: 8),
padding: const EdgeInsets.all(0),
child: DottedBorder(
color: Colors.redAccent.withOpacity(0.6),
color: Colors.redAccent.withOpacity(0.3),
borderType: BorderType.RRect,
radius: const Radius.circular(12),
dashPattern: const [6, 4],
dashPattern: const [3, 3],
strokeWidth: 1,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.redAccent.withOpacity(0.03),
),
padding: const EdgeInsets.all(12),
child: Row(
children: [
......@@ -127,9 +131,9 @@ class _MyVoucherListScreenState extends State<MyVoucherListScreen> {
product.brandName ?? '',
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.black54, fontSize: 12),
),
const SizedBox(height: 4),
const SizedBox(height: 6),
Text(product.title ?? '', style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500)),
const SizedBox(height: 4),
const SizedBox(height: 6),
Text('HSD: ${product.expire}', style: const TextStyle(color: Colors.black54, fontSize: 12)),
],
),
......
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/screen/voucher/voucher_list/voucher_list_screen.dart';
import '../../directional/directional_action_type.dart';
import '../../shared/router_gage.dart';
import '../home/header_home_viewmodel.dart';
import '../popup_manager/popup_manager_screen.dart';
import '../popup_manager/popup_manager_viewmodel.dart';
import '../popup_manager/popup_runner_helper.dart';
import 'voucher_tab_viewmodel.dart';
import 'sub_widget/voucher_action_menu.dart';
import 'sub_widget/voucher_item_grid.dart';
......@@ -17,17 +21,22 @@ class VoucherTabScreen extends StatefulWidget {
State<VoucherTabScreen> createState() => _VoucherTabScreenState();
}
class _VoucherTabScreenState extends State<VoucherTabScreen> {
class _VoucherTabScreenState extends State<VoucherTabScreen> with PopupOnInit {
final _headerHomeVM = Get.find<HeaderHomeViewModel>();
@override
void initState() {
super.initState();
runPopupCheck(DirectionalScreenName.productVoucher);
}
@override
Widget build(BuildContext context) {
final VoucherTabViewModel viewModel = Get.put(VoucherTabViewModel());
return Scaffold(
appBar: CustomNavigationBar(
title: "Ưu đãi",
showBackButton: false,
leftButtons: [],
backgroundImage: _headerHomeVM.headerData.background ?? "assets/images/bg_header_navi.png",
rightButtons: [
IconButton(
......
......@@ -8,6 +8,7 @@ import 'package:webview_flutter/webview_flutter.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../directional/directional_screen.dart';
import '../../widgets/custom_navigation_bar.dart';
class BaseWebViewInput {
final String? title;
......@@ -58,7 +59,7 @@ class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicSta
onWebResourceError: (error) {
hideLoading();
if (error.description != 'about:blank') {
showAlertError(content: error.description);
// showAlertError(content: error.description);
}
},
onNavigationRequest: _handleNavigation,
......@@ -81,13 +82,19 @@ class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicSta
appBar:
input.isFullScreen
? null
: AppBar(
title: Text(
input.title ?? _dynamicTitle ?? Uri.parse(input.url).host,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87),
: CustomNavigationBar(
title: input.title ?? _dynamicTitle ?? Uri.parse(input.url).host,
leftButtons: [
CustomBackButton(onPressed: _handleBack),
],
),
leading: CustomBackButton(onPressed: _handleBack),
),
// AppBar(
// title: Text(
// input.title ?? _dynamicTitle ?? Uri.parse(input.url).host,
// style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87),
// ),
// leading: CustomBackButton(onPressed: _handleBack),
// ),
body: Stack(
children: [
SafeArea(
......
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