diff --git a/lib/classes/consts.dart b/lib/classes/consts.dart index bba9342..4416079 100644 --- a/lib/classes/consts.dart +++ b/lib/classes/consts.dart @@ -109,7 +109,7 @@ class ApiConsts { //DummyCards for the UI - static int tabletMinLength = 500; + static int tabletMinLength = 600; // Notification Center GUIDs for OTP static String notificationCenterAppGUID = diff --git a/lib/main.dart b/lib/main.dart index 5face45..3568b6b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -39,11 +39,16 @@ bool isTablet = false; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); await EasyLocalization.ensureInitialized(); AppState().setPostParamsInitConfig(); HttpOverrides.global = MyHttpOverrides(); - isTablet = MediaQueryData.fromWindow(WidgetsBinding.instance.window).size.shortestSide >= ApiConsts.tabletMinLength; + isTablet = MediaQueryData.fromView(WidgetsBinding.instance.window).size.shortestSide >= ApiConsts.tabletMinLength; runApp( EasyLocalization( @@ -89,6 +94,8 @@ class MyApp extends StatelessWidget { return SafeArea( top: false, // Set to true if you want to avoid the notch area as well bottom: Platform.isIOS ? false : true, + left: false, + right: false, child: MaterialApp( navigatorKey: AppRoutes.navigatorKey, builder: (BuildContext context, Widget? child) { diff --git a/lib/ui/landing/dashboard_screen.dart b/lib/ui/landing/dashboard_screen.dart index 6f6caf3..998da9b 100644 --- a/lib/ui/landing/dashboard_screen.dart +++ b/lib/ui/landing/dashboard_screen.dart @@ -28,6 +28,7 @@ import 'package:mohem_flutter_app/ui/marathon/marathon_provider.dart'; import 'package:mohem_flutter_app/ui/marathon/widgets/marathon_banner.dart'; import 'package:mohem_flutter_app/widgets/bottom_sheet.dart'; import 'package:mohem_flutter_app/widgets/mark_attendance_widget.dart'; +import 'package:mohem_flutter_app/widgets/responsive/duo_responsive.dart'; import 'package:mohem_flutter_app/widgets/shimmer/dashboard_shimmer_widget.dart'; import 'package:mohem_flutter_app/widgets/shimmer/offers_shimmer_widget.dart'; @@ -257,9 +258,12 @@ class _DashboardScreenState extends State with WidgetsBindingOb @override Widget build(BuildContext context) { isDisplayMazaya = checkIfPrivilegedForMazaya(); + bool isUnfolded = DuoResponsive.isUnfolded(context); return SafeArea( bottom: Platform.isAndroid ? true : false, top: false, + left: false, + right: false, child: Scaffold( key: _scaffoldState, body: Column( @@ -321,7 +325,7 @@ class _DashboardScreenState extends State with WidgetsBindingOb children: [ Expanded( child: AspectRatio( - aspectRatio: 159 / 159, + aspectRatio: isUnfolded ? 1.55 : 159 / 159, child: Consumer( builder: (BuildContext context, DashboardProviderModel model, Widget? child) { return (model.isAttendanceTrackingLoading @@ -465,7 +469,7 @@ class _DashboardScreenState extends State with WidgetsBindingOb child: ClipRRect( borderRadius: BorderRadius.circular(20), child: AspectRatio( - aspectRatio: 386 / 121.37, + aspectRatio: isUnfolded ? 3.5 : 386 / 121.37, child: SvgPicture.asset( AppState().isArabic(context) ? "assets/images/mazaya_banner_ar.svg" : "assets/images/mazaya_banner_en.svg", width: double.infinity, diff --git a/lib/ui/landing/widget/app_drawer.dart b/lib/ui/landing/widget/app_drawer.dart index 3f9b2e4..a6aa24a 100644 --- a/lib/ui/landing/widget/app_drawer.dart +++ b/lib/ui/landing/widget/app_drawer.dart @@ -18,6 +18,7 @@ import 'package:mohem_flutter_app/provider/dashboard_provider_model.dart'; import 'package:mohem_flutter_app/ui/dialogs/id/business_card_dialog.dart'; import 'package:mohem_flutter_app/ui/dialogs/id/employee_digital_id_dialog.dart'; import 'package:mohem_flutter_app/widgets/dialogs/dialogs.dart'; +import 'package:mohem_flutter_app/widgets/responsive/duo_responsive.dart'; import 'package:provider/provider.dart'; class AppDrawer extends StatefulWidget { @@ -44,11 +45,7 @@ class _AppDrawerState extends State { try { GenericResponseModel? response = await ProfileApiClient().getEmployeeBusinessCardInfo(); Utils.hideLoading(context); - showMDialog( - context, - child: BusinessCardDialog(qrBase64: response?.businessCardQROutside, employeeInfo: response?.empBusinessCardInfoModel), - isBusniessCard: true, - ); + showMDialog(context, child: BusinessCardDialog(qrBase64: response?.businessCardQROutside, employeeInfo: response?.empBusinessCardInfoModel), isBusniessCard: true); } catch (ex) { Utils.hideLoading(context); Utils.handleException(ex, context, null); @@ -57,42 +54,31 @@ class _AppDrawerState extends State { @override Widget build(BuildContext context) { + double screenWidth = MediaQuery.sizeOf(context).width; + double drawerWidth = (screenWidth * 303 / 375).clamp(260.0, DuoResponsive.maxDrawerWidth); + if (drawerMenuItemList.isEmpty) { drawerMenuItemList = Provider.of(context, listen: false).drawerMenuItemList; } return Drawer( - width: MediaQuery.of(context).size.width * 303 / 375, + width: drawerWidth, child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Image.asset("assets/images/logos/main_mohemm_logo.png", width: 134, height: 24), - const Icon(Icons.clear).onPress(() => Navigator.pop(context)), - ], + children: [Image.asset("assets/images/logos/main_mohemm_logo.png", width: 134, height: 24), const Icon(Icons.clear).onPress(() => Navigator.pop(context))], ).paddingOnly(left: 4, right: 14), Row( children: [ AppState().memberInformationList!.eMPLOYEEIMAGE == null - ? SvgPicture.asset( - "assets/images/user.svg", - height: 52, - width: 52, - ) - : CircleAvatar( - radius: 52 / 2, - backgroundImage: MemoryImage(Utils.dataFromBase64String(AppState().memberInformationList!.eMPLOYEEIMAGE!)), - backgroundColor: Colors.black, - ), + ? SvgPicture.asset("assets/images/user.svg", height: 52, width: 52) + : CircleAvatar(radius: 52 / 2, backgroundImage: MemoryImage(Utils.dataFromBase64String(AppState().memberInformationList!.eMPLOYEEIMAGE!)), backgroundColor: Colors.black), 12.width, Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AppState().memberInformationList!.eMPLOYEENAME!.toText18(isBold: true), - AppState().memberInformationList!.getPositionName().toText14(weight: FontWeight.w500), - ], - ).expanded + children: [AppState().memberInformationList!.eMPLOYEENAME!.toText18(isBold: true), AppState().memberInformationList!.getPositionName().toText14(weight: FontWeight.w500)], + ).expanded, ], ).paddingOnly(left: 14, right: 14, top: 21, bottom: 21), // Row( @@ -117,67 +103,71 @@ class _AppDrawerState extends State { // ), // ], // ).paddingOnly(left: 14, right: 14, bottom: 14), - const Divider( - height: 1, - thickness: 1, - color: MyColors.lightGreyEFColor, - ), + const Divider(height: 1, thickness: 1, color: MyColors.lightGreyEFColor), ListView( padding: const EdgeInsets.only(top: 21, bottom: 21), children: [ ListView.builder( - padding: EdgeInsets.zero, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: drawerMenuItemList.length, - itemBuilder: (cxt, index) { - return menuItem(drawerMenuItemList[index].icon, drawerMenuItemList[index].title, drawerMenuItemList[index].routeName, onPress: () { + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: drawerMenuItemList.length, + itemBuilder: (cxt, index) { + return menuItem( + drawerMenuItemList[index].icon, + drawerMenuItemList[index].title, + drawerMenuItemList[index].routeName, + onPress: () { Navigator.pushNamed(context, drawerMenuItemList[index].routeName); - }); - }), + }, + ); + }, + ), menuItem("assets/images/drawer/employee_id.svg", LocaleKeys.employeeDigitalID.tr(), "", closeDrawer: false, onPress: () => showMDialog(context, child: EmployeeDigitialIdDialog())), if (AppState().businessCardPrivilege) menuItem("assets/images/drawer/view_business_card.svg", LocaleKeys.viewBusinessCard.tr(), "", closeDrawer: false, onPress: () => _showBusinessCard(context)), - menuItem("assets/images/drawer/logout.svg", LocaleKeys.logout.tr(), "", color: MyColors.redA3Color, closeDrawer: false, onPress: (){Utils.performLogout(context, chatData);}), + menuItem( + "assets/images/drawer/logout.svg", + LocaleKeys.logout.tr(), + "", + color: MyColors.redA3Color, + closeDrawer: false, + onPress: () { + Utils.performLogout(context, chatData); + }, + ), // menuItem("assets/images/drawer/logout.svg", LocaleKeys.logout.tr(), "", color: MyColors.redA3Color, closeDrawer: false, onPress: () {Navigator.pushNamed(context, AppRoutes.survey,); ], ).expanded, - const Divider( - height: 1, - thickness: 1, - color: MyColors.lightGreyEFColor, - ), + const Divider(height: 1, thickness: 1, color: MyColors.lightGreyEFColor), Row( children: [ RichText( - text: TextSpan(text: LocaleKeys.poweredBy.tr() + " ", style: const TextStyle(color: MyColors.grey98Color, fontSize: 14, letterSpacing: -0.56, fontWeight: FontWeight.w600), children: [ - TextSpan( - text: LocaleKeys.cloudSolutions.tr(), - style: const TextStyle(color: MyColors.grey3AColor, fontSize: 14, letterSpacing: -0.56, fontWeight: FontWeight.w600), - ), - ]), + text: TextSpan( + text: LocaleKeys.poweredBy.tr() + " ", + style: const TextStyle(color: MyColors.grey98Color, fontSize: 14, letterSpacing: -0.56, fontWeight: FontWeight.w600), + children: [TextSpan(text: LocaleKeys.cloudSolutions.tr(), style: const TextStyle(color: MyColors.grey3AColor, fontSize: 14, letterSpacing: -0.56, fontWeight: FontWeight.w600))], + ), ).expanded, - Image.asset("assets/images/logos/bn_cloud_soloution.jpg", width: 40, height: 40) + Image.asset("assets/images/logos/bn_cloud_soloution.jpg", width: 40, height: 40), ], - ).paddingOnly(left: 21, right: 21, top: 21) + ).paddingOnly(left: 21, right: 21, top: 21), ], ).paddingOnly(top: 21, bottom: 21), ); } Widget menuItem(String icon, String title, String routeName, {Color? color, bool closeDrawer = true, VoidCallback? onPress}) { - return Row( - children: [ - SvgPicture.asset(icon, height: 20, width: 20), - 9.width, - title.toText14(color: color, textAlign: AppState().isArabic(context) ? TextAlign.right : null).expanded, - ], - ).paddingOnly(left: 21, top: 10, bottom: 10, right: 21).onPress(closeDrawer - ? () async { - Navigator.pop(context); - Future.delayed(const Duration(microseconds: 200), onPress); - } - : onPress!); + return Row(children: [SvgPicture.asset(icon, height: 20, width: 20), 9.width, title.toText14(color: color, textAlign: AppState().isArabic(context) ? TextAlign.right : null).expanded]) + .paddingOnly(left: 21, top: 10, bottom: 10, right: 21) + .onPress( + closeDrawer + ? () async { + Navigator.pop(context); + Future.delayed(const Duration(microseconds: 200), onPress); + } + : onPress!, + ); } void postLanguageChange(BuildContext context) { @@ -189,13 +179,13 @@ class _AppDrawerState extends State { setState(() {}); } -// void performLogout() async { -// // AppState().isAuthenticated = false; -// // AppState().isLogged = false; -// // AppState().setPostParamsInitConfig(); -// // chatData.disposeData(); -// // // SharedPreferences prefs = await SharedPreferences.getInstance(); -// // // await prefs.clear(); -// // Navigator.pushNamedAndRemoveUntil(context, AppRoutes.login, (Route route) => false, arguments: null); -// } + // void performLogout() async { + // // AppState().isAuthenticated = false; + // // AppState().isLogged = false; + // // AppState().setPostParamsInitConfig(); + // // chatData.disposeData(); + // // // SharedPreferences prefs = await SharedPreferences.getInstance(); + // // // await prefs.clear(); + // // Navigator.pushNamedAndRemoveUntil(context, AppRoutes.login, (Route route) => false, arguments: null); + // } } diff --git a/lib/ui/landing/widget/menus_widget.dart b/lib/ui/landing/widget/menus_widget.dart index a523390..e3193e0 100644 --- a/lib/ui/landing/widget/menus_widget.dart +++ b/lib/ui/landing/widget/menus_widget.dart @@ -13,6 +13,7 @@ import 'package:mohem_flutter_app/models/sso_auth_model.dart'; import 'package:mohem_flutter_app/provider/dashboard_provider_model.dart'; import 'package:mohem_flutter_app/ui/my_attendance/dynamic_screens/dynamic_listview_screen.dart'; import 'package:mohem_flutter_app/ui/screens/ticket/ticket_detailed_screen.dart'; +import 'package:mohem_flutter_app/widgets/responsive/duo_responsive.dart'; import 'package:mohem_flutter_app/widgets/shimmer/dashboard_shimmer_widget.dart'; import 'package:provider/provider.dart'; @@ -20,11 +21,12 @@ class MenusWidget extends StatelessWidget { @override Widget build(BuildContext context) { List namesColor = [0xff125765, 0xff239D8F, 0xff2BB8A8, 0xff1D92AA]; + bool isUnfolded = DuoResponsive.isUnfolded(context); return Consumer( builder: (cxt, data, child) { return GridView( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, childAspectRatio: 2 / 2, crossAxisSpacing: 9, mainAxisSpacing: 9), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, childAspectRatio: isUnfolded ? 1.55 : 2 / 2, crossAxisSpacing: 9, mainAxisSpacing: 9), padding: EdgeInsets.zero, shrinkWrap: true, primary: false, diff --git a/lib/ui/mazaya/retailers_list_screen.dart b/lib/ui/mazaya/retailers_list_screen.dart index 38d5e92..e9577ea 100644 --- a/lib/ui/mazaya/retailers_list_screen.dart +++ b/lib/ui/mazaya/retailers_list_screen.dart @@ -1010,6 +1010,8 @@ class _RetailersListScreenState extends State { ], ), body: SafeArea( + left: false, + right: false, child: RefreshIndicator( onRefresh: _onRefresh, color: MyColors.textMixColor, diff --git a/lib/ui/mazaya/widgets/wala_plus_retailer_grid.dart b/lib/ui/mazaya/widgets/wala_plus_retailer_grid.dart index a28a60e..32a45f6 100644 --- a/lib/ui/mazaya/widgets/wala_plus_retailer_grid.dart +++ b/lib/ui/mazaya/widgets/wala_plus_retailer_grid.dart @@ -32,9 +32,7 @@ class WalaPlusRetailerGrid extends StatelessWidget { bool get _hasAnyChips { for (WalaPlusRetailerModel r in retailers) { - if (r.normalizedTypes.isNotEmpty || - r.displayCity.trim().isNotEmpty || - r.displayUsage.trim().isNotEmpty) { + if (r.normalizedTypes.isNotEmpty || r.displayCity.trim().isNotEmpty || r.displayUsage.trim().isNotEmpty) { return true; } } @@ -47,50 +45,53 @@ class WalaPlusRetailerGrid extends StatelessWidget { return 235; } - SliverGridDelegateWithFixedCrossAxisCount get _gridDelegate => - SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: isTablet ? 4 : 2, - mainAxisExtent: _effectiveMainAxisExtent, - crossAxisSpacing: 12, - mainAxisSpacing: 16, - ); + SliverGridDelegateWithFixedCrossAxisCount _gridDelegateForWidth(double width) { + int columns = width >= 680 ? 4 : (width >= 480 ? 3 : 2); + return SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: columns, mainAxisExtent: _effectiveMainAxisExtent, crossAxisSpacing: 12, mainAxisSpacing: 16); + } @override Widget build(BuildContext context) { - if (isLoading) { - return Shimmer.fromColors( - baseColor: const Color(0xFFE2E8F0), - highlightColor: const Color(0xFFF8FAFC), - child: GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: _gridDelegate, - itemCount: shimmerItemCount, - itemBuilder: (BuildContext _, int __) => _buildShimmerCard(), - ), - ); - } + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + SliverGridDelegateWithFixedCrossAxisCount gridDelegate = _gridDelegateForWidth(constraints.maxWidth); - if (retailers.isEmpty) { - return Padding(padding: const EdgeInsets.only(top: 40), child: Center(child: LocaleKeys.noOffersAvailable.tr().toText14(color: const Color(0xFF64748B)))); - } + if (isLoading) { + return Shimmer.fromColors( + baseColor: const Color(0xFFE2E8F0), + highlightColor: const Color(0xFFF8FAFC), + child: GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: gridDelegate, + itemCount: shimmerItemCount, + itemBuilder: (BuildContext _, int __) => _buildShimmerCard(), + ), + ); + } - return Column( - children: [ - GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: _gridDelegate, - itemCount: retailers.length, - itemBuilder: (BuildContext context, int index) { - return WalaPlusRetailerCard(item: retailers[index], isAr: isAr, onTap: () => onTapRetailer(retailers[index]), onFavoriteTap: () => onFavoriteTap(retailers[index])); - }, - ), - if (isLoadingMore) ...[ - 16.height, - Center(child: Padding(padding: const EdgeInsets.symmetric(vertical: 12), child: Image.asset("assets/images/logos/loading_mohemm_logo.gif", height: 48.0, width: 48.0))), - ], - ], + if (retailers.isEmpty) { + return Padding(padding: const EdgeInsets.only(top: 40), child: Center(child: LocaleKeys.noOffersAvailable.tr().toText14(color: const Color(0xFF64748B)))); + } + + return Column( + children: [ + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: gridDelegate, + itemCount: retailers.length, + itemBuilder: (BuildContext context, int index) { + return WalaPlusRetailerCard(item: retailers[index], isAr: isAr, onTap: () => onTapRetailer(retailers[index]), onFavoriteTap: () => onFavoriteTap(retailers[index])); + }, + ), + if (isLoadingMore) ...[ + 16.height, + Center(child: Padding(padding: const EdgeInsets.symmetric(vertical: 12), child: Image.asset("assets/images/logos/loading_mohemm_logo.gif", height: 48.0, width: 48.0))), + ], + ], + ); + }, ); } diff --git a/lib/widgets/responsive/duo_responsive.dart b/lib/widgets/responsive/duo_responsive.dart new file mode 100644 index 0000000..b5de642 --- /dev/null +++ b/lib/widgets/responsive/duo_responsive.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; + +/// Responsive & hinge-aware layout utilities for iPhone Duo (folded, unfolded, and Split View postures). +class DuoResponsive { + DuoResponsive._(); + + /// Viewport width threshold (in logical points) where the layout transitions + /// from folded / narrow Split View to the unfolded inner display posture. + static const double unfoldedBreakpoint = 600.0; + + /// Reserved center fold clearance (~40pt) for unfolded dual-pane postures + /// since MediaQuery.displayFeatures does not natively report the iOS fold yet. + static const double foldGap = 40.0; + + /// Maximum width for dialogs and modal bottom sheets so they do not stretch + /// awkwardly across the 40pt center fold when unfolded. + static const double maxModalWidth = 420.0; + + /// Standard capped width for navigation drawers on wide/unfolded screens. + static const double maxDrawerWidth = 320.0; + + /// Width of the adaptive side navigation rail when unfolded. + static const double sideRailWidth = 88.0; + + /// Returns true when the current window width is in the unfolded / expanded posture. + static bool isUnfolded(BuildContext context) { + return MediaQuery.sizeOf(context).width >= unfoldedBreakpoint; + } + + /// Returns true when the given constraint or viewport width is in the unfolded posture. + static bool isWideWidth(double width) { + return width >= unfoldedBreakpoint; + } + + /// Calculates the width of a single pane when split across the center fold gap. + static double singlePaneWidth(double availableWidth, {double horizontalPadding = 0.0}) { + double usable = availableWidth - (horizontalPadding * 2); + if (!isWideWidth(availableWidth)) { + return usable; + } + return ((usable - foldGap) / 2).clamp(0.0, double.infinity); + } + + /// Returns adaptive dialog insetPadding so dialogs stay within a single pane + /// or centered with a comfortable width instead of stretching across the hinge. + static EdgeInsets dialogInsetPadding(BuildContext context, {double defaultHorizontal = 21.0, double defaultVertical = 24.0}) { + Size size = MediaQuery.sizeOf(context); + if (size.width >= unfoldedBreakpoint) { + double horizontal = ((size.width - maxModalWidth) / 2).clamp(defaultHorizontal, size.width * 0.35); + return EdgeInsets.symmetric(horizontal: horizontal, vertical: defaultVertical); + } + return EdgeInsets.symmetric(horizontal: defaultHorizontal, vertical: defaultVertical); + } + + /// Returns BoxConstraints for modal bottom sheets so they never stretch across the fold. + static BoxConstraints bottomSheetConstraints(BuildContext context, {double? maxHeight}) { + Size size = MediaQuery.sizeOf(context); + bool unfolded = size.width >= unfoldedBreakpoint; + return BoxConstraints( + maxWidth: unfolded ? maxModalWidth : size.width, + maxHeight: maxHeight ?? double.infinity, + ); + } +} + +/// A two-pane row that automatically inserts [DuoResponsive.foldGap] (~40pt) +/// between [startPane] and [endPane] when in the unfolded posture, or [compactSpacing] +/// when in the folded / compact posture. +class DuoSplitRow extends StatelessWidget { + final Widget startPane; + final Widget endPane; + final double compactSpacing; + final CrossAxisAlignment crossAxisAlignment; + + const DuoSplitRow({ + Key? key, + required this.startPane, + required this.endPane, + this.compactSpacing = 9.0, + this.crossAxisAlignment = CrossAxisAlignment.start, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + bool isWide = DuoResponsive.isWideWidth(constraints.maxWidth) || DuoResponsive.isUnfolded(context); + double gap = isWide ? DuoResponsive.foldGap : compactSpacing; + return Row( + crossAxisAlignment: crossAxisAlignment, + children: [ + Expanded(child: startPane), + SizedBox(width: gap), + Expanded(child: endPane), + ], + ); + }, + ); + } +} + +/// Constrains wide content (such as forms, cards, or lists) on unfolded screens +/// while letting it fill the width naturally when folded. +class DuoContentConstraint extends StatelessWidget { + final Widget child; + final double maxWidth; + final AlignmentGeometry alignment; + + const DuoContentConstraint({ + Key? key, + required this.child, + this.maxWidth = 720.0, + this.alignment = Alignment.topCenter, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + if (constraints.maxWidth <= maxWidth) { + return child; + } + return Align( + alignment: alignment, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: maxWidth), + child: child, + ), + ); + }, + ); + } +}