From a2fbe9c649920265bd867f35b727b5cefbeb5a84 Mon Sep 17 00:00:00 2001 From: Serious-senpai <57554044+Serious-senpai@users.noreply.github.com> Date: Thu, 19 Dec 2024 18:30:27 +0700 Subject: [PATCH] Display drawer based on screen size --- .github/workflows/tests.yml | 8 +- .../integration_test/widget_test.dart | 49 ++- .../lib/src/widgets/common.dart | 384 +++++++++--------- .../lib/src/widgets/home.dart | 8 +- .../lib/src/widgets/login.dart | 6 +- 5 files changed, 247 insertions(+), 208 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8cf78ec..b4dd304 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -95,6 +95,10 @@ jobs: VNPAY_SECRET_KEY: ${{ secrets.VNPAY_SECRET_KEY }} PORT: 8000 + strategy: + matrix: + resolution: [375x667x24, 1024x768x32, 1920x1080x24] + steps: - name: Download repository uses: actions/download-artifact@v4 @@ -146,7 +150,7 @@ jobs: - name: Run integration tests timeout-minutes: 30 working-directory: app/resident_manager - run: xvfb-run flutter test integration_test + run: xvfb-run --server-args="-screen 0 ${{ matrix.resolution }}" flutter test integration_test - name: Stop API server run: | @@ -165,7 +169,7 @@ jobs: - name: Upload coverage report uses: actions/upload-artifact@v4 with: - name: coverage-flutter-integration + name: coverage-flutter-integration-${{ matrix.resolution }} path: .coverage.flutter-integration include-hidden-files: true diff --git a/app/resident_manager/integration_test/widget_test.dart b/app/resident_manager/integration_test/widget_test.dart index 4cbb5cf..7c010e2 100644 --- a/app/resident_manager/integration_test/widget_test.dart +++ b/app/resident_manager/integration_test/widget_test.dart @@ -8,6 +8,7 @@ import "package:integration_test/integration_test.dart"; import "package:resident_manager/main.dart"; import "package:resident_manager/src/config.dart"; import "package:resident_manager/src/state.dart"; +import "package:resident_manager/src/utils.dart"; import "package:resident_manager/src/widgets/home.dart"; import "package:resident_manager/src/widgets/login.dart"; import "package:resident_manager/src/widgets/register.dart"; @@ -113,6 +114,9 @@ void main() { testWidgets( "Administrator login", (tester) async { + final screenSize = tester.view.physicalSize; + final hiddenDrawer = screenSize.width < ScreenWidth.EXTRA_LARGE; + final state = ApplicationState(); await state.prepare(); await state.deauthorize(); // Start integration test without existing authorization data @@ -132,8 +136,10 @@ void main() { await pumpUntilFound((widget) => widget is RegisterQueuePage, findsOneWidget, tester); // Open drawer - await tester.tap(find.byIcon(Icons.menu_outlined)); - await tester.pumpAndSettle(); + if (hiddenDrawer) { + await tester.tap(find.byIcon(Icons.menu_outlined)); + await tester.pumpAndSettle(); + } // Open residents list await tester.tap(find.byIcon(Icons.people_outlined)); @@ -141,8 +147,10 @@ void main() { expect(find.byWidgetPredicate((widget) => widget is ResidentsPage), findsOneWidget); // Open drawer - await tester.tap(find.byIcon(Icons.menu_outlined)); - await tester.pumpAndSettle(); + if (hiddenDrawer) { + await tester.tap(find.byIcon(Icons.menu_outlined)); + await tester.pumpAndSettle(); + } // Open rooms list await tester.tap(find.byIcon(Icons.room_outlined)); @@ -150,8 +158,10 @@ void main() { expect(find.byWidgetPredicate((widget) => widget is RoomsPage), findsOneWidget); // Open drawer - await tester.tap(find.byIcon(Icons.menu_outlined)); - await tester.pumpAndSettle(); + if (hiddenDrawer) { + await tester.tap(find.byIcon(Icons.menu_outlined)); + await tester.pumpAndSettle(); + } // Logout await tester.tap(find.byIcon(Icons.logout_outlined)); @@ -164,6 +174,9 @@ void main() { testWidgets( "Resident registration", (tester) async { + final screenSize = tester.view.physicalSize; + final hiddenDrawer = screenSize.width < ScreenWidth.EXTRA_LARGE; + final state = ApplicationState(); await state.prepare(); await state.deauthorize(); // Start integration test without existing authorization data @@ -266,8 +279,10 @@ void main() { await tester.drag(find.byType(CustomScrollView), const Offset(0, -100)); // Open drawer - await tester.tap(find.byIcon(Icons.menu_outlined)); - await tester.pumpAndSettle(); + if (hiddenDrawer) { + await tester.tap(find.byIcon(Icons.menu_outlined)); + await tester.pumpAndSettle(); + } // Logout await tester.tap(find.byIcon(Icons.logout_outlined)); @@ -289,8 +304,10 @@ void main() { await pumpUntilFound((widget) => widget is HomePage, findsOneWidget, tester); // Open drawer - await tester.tap(find.byIcon(Icons.menu_outlined)); - await tester.pumpAndSettle(); + if (hiddenDrawer) { + await tester.tap(find.byIcon(Icons.menu_outlined)); + await tester.pumpAndSettle(); + } // Logout await tester.tap(find.byIcon(Icons.logout_outlined)); @@ -312,8 +329,10 @@ void main() { await pumpUntilFound((widget) => widget is RegisterQueuePage, findsOneWidget, tester); // Open drawer - await tester.tap(find.byIcon(Icons.menu_outlined)); - await tester.pumpAndSettle(); + if (hiddenDrawer) { + await tester.tap(find.byIcon(Icons.menu_outlined)); + await tester.pumpAndSettle(); + } // View resident list await tester.tap(find.byIcon(Icons.people_outlined)); @@ -406,8 +425,10 @@ void main() { await tester.drag(find.byType(CustomScrollView), const Offset(0, -100)); // Open drawer - await tester.tap(find.byIcon(Icons.menu_outlined)); - await tester.pumpAndSettle(); + if (hiddenDrawer) { + await tester.tap(find.byIcon(Icons.menu_outlined)); + await tester.pumpAndSettle(); + } // Logout await tester.tap(find.byIcon(Icons.logout_outlined)); diff --git a/app/resident_manager/lib/src/widgets/common.dart b/app/resident_manager/lib/src/widgets/common.dart index 6c5fc27..5705d25 100644 --- a/app/resident_manager/lib/src/widgets/common.dart +++ b/app/resident_manager/lib/src/widgets/common.dart @@ -5,6 +5,7 @@ import "state.dart"; import "../routes.dart"; import "../state.dart"; import "../translations.dart"; +import "../utils.dart"; abstract class AbstractCommonState extends State { ApplicationState get state => widget.state; @@ -111,36 +112,214 @@ class _CommonScaffoldState extends State MediaQuery.of(context).size.width < ScreenWidth.EXTRA_LARGE; + + Widget buildDrawer(BuildContext context) { + final currentRoute = ModalRoute.of(context)?.settings.name; + final state = widget.widgetState.state; + final navigator = [ + DrawerHeader( + decoration: const BoxDecoration(color: Colors.grey), + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const CircleAvatar(child: Icon(Icons.account_circle_outlined)), + Text(state.loggedInAsAdmin ? AppLocale.Admin.getString(context) : state.resident?.name ?? AppLocale.NotYetLoggedIn.getString(context)), + ], + ), + ), + ), + ]; + + Widget routeTile({ + required Icon leading, + required String title, + required String route, + required bool popAll, + }) => + ListTile( + leading: leading, + title: Text( + title, + style: currentRoute == route ? const TextStyle(color: Colors.blue) : null, + ), + onTap: () { + closeDrawer(); + if (currentRoute != route) { + if (popAll) { + Navigator.popUntil(context, (route) => route.isFirst); + } + + Navigator.pushReplacementNamed(context, route); + } + }, + ); + + if (!widget.widgetState.state.loggedIn) { + // Not yet logged in + navigator.addAll( + [ + routeTile( + leading: const Icon(Icons.lock_outlined), + title: AppLocale.Login.getString(context), + route: ApplicationRoute.login, + popAll: true, + ), + routeTile( + leading: const Icon(Icons.how_to_reg_outlined), + title: AppLocale.Register.getString(context), + route: ApplicationRoute.register, + popAll: true, + ), + ], + ); + } else { + // Logged in... + if (widget.widgetState.state.loggedInAsAdmin) { + // ... as admin + navigator.addAll( + [ + routeTile( + leading: const Icon(Icons.how_to_reg_outlined), + title: AppLocale.RegisterQueue.getString(context), + route: ApplicationRoute.adminRegisterQueue, + popAll: false, + ), + routeTile( + leading: const Icon(Icons.people_outlined), + title: AppLocale.ResidentsList.getString(context), + route: ApplicationRoute.adminResidentsPage, + popAll: false, + ), + routeTile( + leading: const Icon(Icons.room_outlined), + title: AppLocale.RoomsList.getString(context), + route: ApplicationRoute.adminRoomsPage, + popAll: false, + ), + routeTile( + leading: const Icon(Icons.payment_outlined), + title: AppLocale.FeeList.getString(context), + route: ApplicationRoute.adminFeesPage, + popAll: false, + ), + ], + ); + } else { + // ... as resident + navigator.addAll( + [ + routeTile( + leading: const Icon(Icons.home_outlined), + title: AppLocale.Home.getString(context), + route: ApplicationRoute.home, + popAll: false, + ), + routeTile( + leading: const Icon(Icons.person_outlined), + title: AppLocale.PersonalInfo.getString(context), + route: ApplicationRoute.personalInfo, + popAll: false, + ), + routeTile( + leading: const Icon(Icons.payment_outlined), + title: AppLocale.FeeList.getString(context), + route: ApplicationRoute.payment, + popAll: false, + ), + ], + ); + } + + navigator.add( + ListTile( + leading: const Icon(Icons.logout_outlined), + title: Text(AppLocale.Logout.getString(context)), + onTap: () async { + await widget.widgetState.state.deauthorize(); + if (context.mounted) { + Navigator.popUntil(context, (route) => route.isFirst); + await Navigator.pushReplacementNamed(context, ApplicationRoute.login); + } + }, + ), + ); + } + + return Drawer( + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.zero)), + child: Stack( + children: [ + ListView(children: navigator), + Positioned( + bottom: 5, + left: 5, + child: Row( + children: [ + IconButton( + icon: Image.asset("assets/flags/en.png", height: 20, width: 20), + iconSize: 20, + onPressed: () => widget.widgetState.state.localization.translate("en"), + padding: EdgeInsets.zero, + ), + const SizedBox.square(dimension: 20), + IconButton( + icon: Image.asset("assets/flags/vi.png", height: 20, width: 20), + iconSize: 20, + onPressed: () => widget.widgetState.state.localization.translate("vi"), + padding: EdgeInsets.zero, + ), + ], + ), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { final canPop = Navigator.canPop(context); + final mediaQuery = MediaQuery.of(context); + return Scaffold( key: scaffoldKey, - body: CustomScrollView( - controller: scrollController, - slivers: [ - SliverAppBar( - flexibleSpace: FlexibleSpaceBar( - background: Image.asset( - "assets/vector-background-blue.png", - fit: BoxFit.cover, - ), - ), - floating: true, - pinned: false, - snap: false, - leading: canPop - ? IconButton( - onPressed: () => Navigator.pop(context), - icon: const Icon(Icons.arrow_back_outlined), - ) - : IconButton( - onPressed: () => openDrawer(), - icon: const Icon(Icons.menu_outlined), + body: Row( + children: [ + if (!hiddenDrawer) buildDrawer(context), + Expanded( + child: CustomScrollView( + controller: scrollController, + slivers: [ + SliverAppBar( + flexibleSpace: FlexibleSpaceBar( + background: Image.asset( + "assets/vector-background-blue.png", + fit: BoxFit.cover, + ), ), - title: widget.title, + floating: true, + pinned: false, + snap: false, + leading: canPop + ? IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.arrow_back_outlined), + ) + : hiddenDrawer + ? IconButton( + onPressed: () => openDrawer(), + icon: const Icon(Icons.menu_outlined), + ) + : null, + title: widget.title, + ), + ...widget.slivers, + ], + ), ), - ...widget.slivers, ], ), floatingActionButton: (scrollPosition == null || scrollPosition!.pixels == 0) @@ -165,164 +344,7 @@ class _CommonScaffoldState extends State[ - DrawerHeader( - decoration: const BoxDecoration(color: Colors.grey), - child: Padding( - padding: const EdgeInsets.all(10), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const CircleAvatar(child: Icon(Icons.account_circle_outlined)), - Text(state.loggedInAsAdmin ? AppLocale.Admin.getString(context) : state.resident?.name ?? AppLocale.NotYetLoggedIn.getString(context)), - ], - ), - ), - ), - ]; - - Widget routeTile({ - required Icon leading, - required String title, - required String route, - required bool popAll, - }) => - ListTile( - leading: leading, - title: Text( - title, - style: currentRoute == route ? const TextStyle(color: Colors.blue) : null, - ), - onTap: () { - closeDrawer(); - if (currentRoute != route) { - if (popAll) { - Navigator.popUntil(context, (route) => route.isFirst); - Navigator.pushReplacementNamed(context, route); - } else { - Navigator.pushReplacementNamed(context, route); - } - } - }, - ); - - if (!widget.widgetState.state.loggedIn) { - // Not yet logged in - navigator.add( - routeTile( - leading: const Icon(Icons.lock_outlined), - title: AppLocale.Login.getString(context), - route: ApplicationRoute.login, - popAll: true, - ), - ); - } else { - // Logged in... - if (widget.widgetState.state.loggedInAsAdmin) { - // ... as admin - navigator.addAll( - [ - routeTile( - leading: const Icon(Icons.how_to_reg_outlined), - title: AppLocale.RegisterQueue.getString(context), - route: ApplicationRoute.adminRegisterQueue, - popAll: false, - ), - routeTile( - leading: const Icon(Icons.people_outlined), - title: AppLocale.ResidentsList.getString(context), - route: ApplicationRoute.adminResidentsPage, - popAll: false, - ), - routeTile( - leading: const Icon(Icons.room_outlined), - title: AppLocale.RoomsList.getString(context), - route: ApplicationRoute.adminRoomsPage, - popAll: false, - ), - routeTile( - leading: const Icon(Icons.payment_outlined), - title: AppLocale.FeeList.getString(context), - route: ApplicationRoute.adminFeesPage, - popAll: false, - ), - ], - ); - } else { - // ... as resident - navigator.addAll( - [ - routeTile( - leading: const Icon(Icons.home_outlined), - title: AppLocale.Home.getString(context), - route: ApplicationRoute.home, - popAll: false, - ), - routeTile( - leading: const Icon(Icons.person_outlined), - title: AppLocale.PersonalInfo.getString(context), - route: ApplicationRoute.personalInfo, - popAll: false, - ), - routeTile( - leading: const Icon(Icons.payment_outlined), - title: AppLocale.FeeList.getString(context), - route: ApplicationRoute.payment, - popAll: false, - ), - ], - ); - } - - navigator.add( - ListTile( - leading: const Icon(Icons.logout_outlined), - title: Text(AppLocale.Logout.getString(context)), - onTap: () async { - await widget.widgetState.state.deauthorize(); - if (context.mounted) { - Navigator.popUntil(context, (route) => route.isFirst); - await Navigator.pushReplacementNamed(context, ApplicationRoute.login); - } - }, - ), - ); - } - - return Drawer( - child: Stack( - children: [ - ListView(children: navigator), - Positioned( - bottom: 5, - left: 5, - child: Row( - children: [ - IconButton( - icon: Image.asset("assets/flags/en.png", height: 20, width: 20), - iconSize: 20, - onPressed: () => widget.widgetState.state.localization.translate("en"), - padding: EdgeInsets.zero, - ), - const SizedBox.square(dimension: 20), - IconButton( - icon: Image.asset("assets/flags/vi.png", height: 20, width: 20), - iconSize: 20, - onPressed: () => widget.widgetState.state.localization.translate("vi"), - padding: EdgeInsets.zero, - ), - ], - ), - ), - ], - ), - ); - }, - ), + drawer: hiddenDrawer ? buildDrawer(context) : null, ); } } diff --git a/app/resident_manager/lib/src/widgets/home.dart b/app/resident_manager/lib/src/widgets/home.dart index 760e978..186512b 100644 --- a/app/resident_manager/lib/src/widgets/home.dart +++ b/app/resident_manager/lib/src/widgets/home.dart @@ -73,18 +73,14 @@ class _HomePageState extends AbstractCommonState with CommonScaffoldSt child: TextButton.icon( icon: const Icon(Icons.person_outlined), label: Text(AppLocale.PersonalInfo.getString(context)), - onPressed: () async { - await pushNamedAndRefresh(context, ApplicationRoute.personalInfo); - }, + onPressed: () => Navigator.pushReplacementNamed(context, ApplicationRoute.personalInfo), ), ), Expanded( child: TextButton.icon( icon: const Icon(Icons.receipt_long_outlined), label: Text(AppLocale.Payment.getString(context)), - onPressed: () async { - await pushNamedAndRefresh(context, ApplicationRoute.payment); - }, + onPressed: () => Navigator.pushReplacementNamed(context, ApplicationRoute.payment), ), ), Expanded( diff --git a/app/resident_manager/lib/src/widgets/login.dart b/app/resident_manager/lib/src/widgets/login.dart index f308e84..b610b8b 100644 --- a/app/resident_manager/lib/src/widgets/login.dart +++ b/app/resident_manager/lib/src/widgets/login.dart @@ -83,10 +83,6 @@ class _LoginPageState extends AbstractCommonState with CommonScaffold ); } - Future _residentRegister() async { - await pushNamedAndRefresh(context, ApplicationRoute.register); - } - @override CommonScaffold build(BuildContext context) { final mediaQuery = MediaQuery.of(context); @@ -186,7 +182,7 @@ class _LoginPageState extends AbstractCommonState with CommonScaffold AppLocale.RegisterAsResident.getString(context), style: const TextStyle(color: Colors.white), ), - onPressed: _actionLock.locked ? null : () => _residentRegister(), + onPressed: _actionLock.locked ? null : () => pushNamedAndRefresh(context, ApplicationRoute.register), ), ), Container(