From eb60c768f11f916923fe283df1cfe98af9e232b2 Mon Sep 17 00:00:00 2001 From: sembauke Date: Tue, 25 Mar 2025 14:27:47 +0100 Subject: [PATCH 01/24] feat: new block layouts --- mobile-app/lib/ui/theme/fcc_theme.dart | 1 - .../lib/ui/views/learn/block/block_view.dart | 287 +++++++++--------- .../learn/superblock/superblock_view.dart | 1 + 3 files changed, 141 insertions(+), 148 deletions(-) diff --git a/mobile-app/lib/ui/theme/fcc_theme.dart b/mobile-app/lib/ui/theme/fcc_theme.dart index bfb9e75ca..944ae9947 100644 --- a/mobile-app/lib/ui/theme/fcc_theme.dart +++ b/mobile-app/lib/ui/theme/fcc_theme.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; class FccTheme { static ThemeData themeDark = ThemeData( brightness: Brightness.dark, - fontFamily: 'RobotoMono', scaffoldBackgroundColor: const Color.fromRGBO(0x2A, 0x2A, 0x40, 1), appBarTheme: const AppBarTheme( centerTitle: true, diff --git a/mobile-app/lib/ui/views/learn/block/block_view.dart b/mobile-app/lib/ui/views/learn/block/block_view.dart index 9fcd8cb21..02d586d8f 100644 --- a/mobile-app/lib/ui/views/learn/block/block_view.dart +++ b/mobile-app/lib/ui/views/learn/block/block_view.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import 'package:freecodecamp/extensions/i18n_extension.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; import 'package:freecodecamp/ui/views/learn/block/block_viewmodel.dart'; -import 'package:freecodecamp/ui/views/learn/utils/learn_globals.dart'; -import 'package:freecodecamp/ui/views/learn/widgets/download_button_widget.dart'; +// import 'package:freecodecamp/ui/views/learn/utils/learn_globals.dart'; +// import 'package:freecodecamp/ui/views/learn/widgets/download_button_widget.dart'; import 'package:freecodecamp/ui/views/learn/widgets/open_close_icon_widget.dart'; -import 'package:freecodecamp/ui/views/learn/widgets/progressbar_widget.dart'; -import 'package:freecodecamp/ui/widgets/drawer_widget/drawer_widget_view.dart'; +// import 'package:freecodecamp/ui/views/learn/widgets/progressbar_widget.dart'; +// import 'package:freecodecamp/ui/widgets/drawer_widget/drawer_widget_view.dart'; import 'package:stacked/stacked.dart'; class BlockView extends StatelessWidget { @@ -36,83 +36,130 @@ class BlockView extends StatelessWidget { model, child, ) { - bool isCert = block.challenges.length == 1 && - !hasNoCert.contains(block.superBlock.dashedName); - bool isDialogue = hasDialogue.contains(block.superBlock.dashedName); - int calculateProgress = - (model.challengesCompleted / block.challenges.length * 100).round(); - - bool hasProgress = calculateProgress > 0; - - return Column( - children: [ - BlockHeader( - isCertification: isCert, - block: block, - model: model, - ), - if (hasProgress && isStepBased) - ChallengeProgressBar( - block: block, - model: model, + return Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: const Color.fromRGBO(0x3b, 0x3b, 0x4f, 1), ), - if (model.isOpen || isCert) - Container( - color: const Color(0xFF0a0a23), - child: InkWell( - onTap: isCert - ? () async { - model.routeToCertification(block); - } - : () {}, - child: Column( - children: [ - for (String blockString in block.description) - Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 16, - ), - child: Text( - blockString, - style: TextStyle( + color: const Color.fromRGBO(0x1b, 0x1b, 0x32, 1), + ), + padding: const EdgeInsets.all(8), + width: MediaQuery.of(context).size.width, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.symmetric( + horizontal: 8, + ), + child: Icon( + Icons.heat_pump_rounded, + color: Colors.blue, + size: 40, + ), + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + block.name, + style: const TextStyle( + wordSpacing: 0, + letterSpacing: 0, fontSize: 18, - fontWeight: FontWeight.w600, - height: 1.2, - fontFamily: 'Lato', - color: Colors.white.withValues(alpha: 0.87), + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextButton( + onPressed: () {}, + style: TextButton.styleFrom( + backgroundColor: + const Color.fromRGBO(0x1b, 0x1b, 0x32, 1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: const BorderSide( + color: Color.fromRGBO(0x3b, 0x3b, 0x4f, 1), ), ), ), - if (model.isDev && !isCert) - DownloadButton( - model: model, - block: block, + child: Text( + model.isOpen ? 'Hide Steps' : 'Show Steps', ), - if (isDialogue) ...[ - buildDivider(), - dialogueWidget( - block.challenges, - context, - model, - ) - ], - if (!isCert && isStepBased && !isDialogue) ...[ - buildDivider(), - gridWidget(context, model) - ], - if (!isStepBased && !isCert) ...[ - buildDivider(), - listWidget(context, model) - ], - Container( - height: 25, - ) - ], - ), + ), + ), + ], ), - ), - ], + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: + const EdgeInsets.only(left: 8, top: 8, bottom: 8), + height: 200, + width: MediaQuery.of(context).size.width - 45, + child: GridView.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + mainAxisExtent: 35, + mainAxisSpacing: 3, + crossAxisSpacing: 3), + itemCount: block.challenges.length, + itemBuilder: (context, step) { + return ChallengeTile( + block: block, + model: model, + step: step + 1, + challengeId: block.challengeTiles[step].id, + isDowloaded: false, + ); + }, + ), + ), + ], + ), + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () {}, + style: TextButton.styleFrom( + backgroundColor: + const Color.fromRGBO(0x5a, 0x01, 0xa7, 1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + 'Start', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + ), + ], + ) + ], + ), + ), ); }, ); @@ -186,45 +233,6 @@ class BlockView extends StatelessWidget { ); } - Widget gridWidget(BuildContext context, BlockViewModel model) { - return SizedBox( - height: 300, - child: GridView.count( - padding: const EdgeInsets.all(16), - crossAxisCount: (MediaQuery.of(context).size.width / 70 - - MediaQuery.of(context).viewPadding.horizontal) - .round(), - children: List.generate( - block.challenges.length, - (step) { - return FutureBuilder( - future: model.isChallengeDownloaded( - block.challengeTiles[step].id, - ), - builder: (context, snapshot) { - if (snapshot.hasData) { - return Center( - child: ChallengeTile( - block: block, - model: model, - step: step + 1, - challengeId: block.challengeTiles[step].id, - isDowloaded: (snapshot.data is bool - ? snapshot.data as bool - : false), - ), - ); - } - - return const CircularProgressIndicator(); - }, - ); - }, - ), - ), - ); - } - Widget listWidget(BuildContext context, BlockViewModel model) { return Column( children: [ @@ -356,42 +364,27 @@ class ChallengeTile extends StatelessWidget { Widget build(BuildContext context) { bool isCompleted = model.completedChallenge(challengeId); - return GridTile( - child: Container( - margin: const EdgeInsets.all(2), - decoration: BoxDecoration( - border: Border.all( - color: isDowloaded && model.isDownloading && isCompleted - ? Colors.green - : Colors.white.withValues(alpha: 0.01), - width: isDowloaded && model.isDownloading && isCompleted ? 5 : 1, - ), - color: isCompleted - ? const Color.fromRGBO(0x00, 0x2e, 0xad, 1) - : isDowloaded && model.isDownloading && !isCompleted - ? Colors.green - : Colors.transparent, - ), - height: 70, - width: 70, - child: InkWell( - onTap: () async { - model.routeToChallengeView( - block, - challengeId, - ); - }, - child: Center( - child: Text( - step.toString(), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - ), + return TextButton( + onPressed: () {}, + style: TextButton.styleFrom( + backgroundColor: isCompleted + ? const Color.fromRGBO(0x00, 0x2e, 0xad, 0.3) + : const Color.fromRGBO(0x2a, 0x2a, 0x40, 1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + side: isCompleted + ? const BorderSide( + width: 1, + color: Color.fromRGBO(0xbc, 0xe8, 0xf1, 1), + ) + : const BorderSide( + color: Color.fromRGBO(0x3b, 0x3b, 0x4f, 1), + ), ), ), + child: Text( + step.toString(), + ), ); } } diff --git a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart index c2f857a19..d7a8eb614 100644 --- a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart +++ b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart @@ -29,6 +29,7 @@ class SuperBlockView extends StatelessWidget { appBar: AppBar( title: Text(superBlockName), ), + backgroundColor: const Color.fromRGBO(0x0a, 0x0a, 0x23, 1), body: FutureBuilder( future: model.getSuperBlockData( superBlockDashedName, From e980564b62d6706ea47635359a72db6ba0ff50e0 Mon Sep 17 00:00:00 2001 From: sembauke Date: Tue, 25 Mar 2025 15:36:26 +0100 Subject: [PATCH 02/24] refactor: improve block view layout and challenge display logic --- .../lib/ui/views/learn/block/block_view.dart | 165 ++++++++++-------- 1 file changed, 88 insertions(+), 77 deletions(-) diff --git a/mobile-app/lib/ui/views/learn/block/block_view.dart b/mobile-app/lib/ui/views/learn/block/block_view.dart index 02d586d8f..29239b30a 100644 --- a/mobile-app/lib/ui/views/learn/block/block_view.dart +++ b/mobile-app/lib/ui/views/learn/block/block_view.dart @@ -51,22 +51,18 @@ class BlockView extends StatelessWidget { child: Column( children: [ Row( - mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( - padding: EdgeInsets.symmetric( - horizontal: 8, - ), + padding: EdgeInsets.all(8), child: Icon( - Icons.heat_pump_rounded, - color: Colors.blue, + Icons.monitor, + color: Color.fromRGBO(0x19, 0x8e, 0xee, 1), size: 40, ), ), Expanded( child: Column( - mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( @@ -78,85 +74,100 @@ class BlockView extends StatelessWidget { fontWeight: FontWeight.bold, ), ), + if (block.challenges.length == 1) + Text(block.description.join()), + if (block.challenges.length > 1) + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 4, + ), + child: TextButton( + onPressed: () { + model.setIsOpen = !model.isOpen; + }, + style: TextButton.styleFrom( + backgroundColor: const Color.fromRGBO( + 0x1b, 0x1b, 0x32, 1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + side: const BorderSide( + color: Color.fromRGBO( + 0x3b, 0x3b, 0x4f, 1), + ), + ), + ), + child: Text( + model.isOpen + ? 'Hide Steps' + : 'Show Steps', + ), + ), + ), + ], + ), + if (model.isOpen) + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 195, + width: + MediaQuery.of(context).size.width - 100, + child: GridView.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 6, + mainAxisExtent: 35, + mainAxisSpacing: 3, + crossAxisSpacing: 3), + itemCount: block.challenges.length, + itemBuilder: (context, step) { + return ChallengeTile( + block: block, + model: model, + step: step + 1, + challengeId: + block.challengeTiles[step].id, + isDowloaded: false, + ); + }, + ), + ), + ], + ), ], ), ), ], ), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: TextButton( - onPressed: () {}, - style: TextButton.styleFrom( - backgroundColor: - const Color.fromRGBO(0x1b, 0x1b, 0x32, 1), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: const BorderSide( - color: Color.fromRGBO(0x3b, 0x3b, 0x4f, 1), + if (block.challenges.length == 1) + Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: TextButton( + onPressed: () {}, + style: TextButton.styleFrom( + backgroundColor: + const Color.fromRGBO(0x5a, 0x01, 0xa7, 1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + ), + child: const Text( + 'Start', + style: TextStyle(fontWeight: FontWeight.bold), ), ), ), - child: Text( - model.isOpen ? 'Hide Steps' : 'Show Steps', - ), - ), - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: - const EdgeInsets.only(left: 8, top: 8, bottom: 8), - height: 200, - width: MediaQuery.of(context).size.width - 45, - child: GridView.builder( - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - mainAxisExtent: 35, - mainAxisSpacing: 3, - crossAxisSpacing: 3), - itemCount: block.challenges.length, - itemBuilder: (context, step) { - return ChallengeTile( - block: block, - model: model, - step: step + 1, - challengeId: block.challengeTiles[step].id, - isDowloaded: false, - ); - }, - ), - ), - ], - ), - Row( - children: [ - Expanded( - child: TextButton( - onPressed: () {}, - style: TextButton.styleFrom( - backgroundColor: - const Color.fromRGBO(0x5a, 0x01, 0xa7, 1), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: const Text( - 'Start', - style: TextStyle(fontWeight: FontWeight.bold), - ), ), - ), - ], - ) + ], + ) ], ), ), From 7cb97a3a9bbc98df5f54ad1e3b4fd98236a00536 Mon Sep 17 00:00:00 2001 From: sembauke Date: Thu, 27 Mar 2025 09:23:08 +0100 Subject: [PATCH 03/24] refactor: enhance block view with progress indicator and remove unused widget --- .../lib/ui/views/learn/block/block_view.dart | 138 +++--------------- .../learn/widgets/open_close_icon_widget.dart | 35 ----- 2 files changed, 24 insertions(+), 149 deletions(-) delete mode 100644 mobile-app/lib/ui/views/learn/widgets/open_close_icon_widget.dart diff --git a/mobile-app/lib/ui/views/learn/block/block_view.dart b/mobile-app/lib/ui/views/learn/block/block_view.dart index 29239b30a..f80ec2bdd 100644 --- a/mobile-app/lib/ui/views/learn/block/block_view.dart +++ b/mobile-app/lib/ui/views/learn/block/block_view.dart @@ -1,10 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:freecodecamp/extensions/i18n_extension.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; import 'package:freecodecamp/ui/views/learn/block/block_viewmodel.dart'; // import 'package:freecodecamp/ui/views/learn/utils/learn_globals.dart'; // import 'package:freecodecamp/ui/views/learn/widgets/download_button_widget.dart'; -import 'package:freecodecamp/ui/views/learn/widgets/open_close_icon_widget.dart'; // import 'package:freecodecamp/ui/views/learn/widgets/progressbar_widget.dart'; // import 'package:freecodecamp/ui/widgets/drawer_widget/drawer_widget_view.dart'; import 'package:stacked/stacked.dart'; @@ -36,6 +34,8 @@ class BlockView extends StatelessWidget { model, child, ) { + double progress = model.challengesCompleted / block.challenges.length; + return Padding( padding: const EdgeInsets.only(bottom: 16.0), child: Container( @@ -74,6 +74,21 @@ class BlockView extends StatelessWidget { fontWeight: FontWeight.bold, ), ), + if (block.challenges.length > 1) + Padding( + padding: const EdgeInsets.only( + top: 8, bottom: 8, right: 10), + child: LinearProgressIndicator( + minHeight: 10, + value: progress, + valueColor: const AlwaysStoppedAnimation( + Color.fromRGBO(0x99, 0xc9, 0xff, 1), + ), + backgroundColor: + const Color.fromRGBO(0x2a, 0x2a, 0x40, 1), + borderRadius: BorderRadius.circular(5), + ), + ), if (block.challenges.length == 1) Text(block.description.join()), if (block.challenges.length > 1) @@ -121,7 +136,7 @@ class BlockView extends StatelessWidget { gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 6, - mainAxisExtent: 35, + mainAxisExtent: 50, mainAxisSpacing: 3, crossAxisSpacing: 3), itemCount: block.challenges.length, @@ -243,116 +258,6 @@ class BlockView extends StatelessWidget { ], ); } - - Widget listWidget(BuildContext context, BlockViewModel model) { - return Column( - children: [ - ListView.builder( - shrinkWrap: true, - itemCount: block.challenges.length, - physics: const ClampingScrollPhysics(), - itemBuilder: (context, i) => ListTile( - leading: model.getIcon( - model.completedChallenge( - block.challengeTiles[i].id, - ), - ), - title: Text(block.challengeTiles[i].name), - onTap: () async { - String challengeId = block.challengeTiles[i].id; - - model.routeToChallengeView( - block, - challengeId, - ); - }, - ), - ), - ], - ); - } -} - -class BlockHeader extends StatelessWidget { - const BlockHeader({ - Key? key, - required this.isCertification, - required this.block, - required this.model, - }) : super(key: key); - - final bool isCertification; - final BlockViewModel model; - final Block block; - - @override - Widget build(BuildContext context) { - return Container( - color: const Color(0xFF0a0a23), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (isCertification) - Container( - padding: const EdgeInsets.all(8), - margin: const EdgeInsets.only(top: 8, left: 8), - color: const Color.fromRGBO(0x00, 0x2e, 0xad, 1), - child: Text( - context.t.certification_project, - style: const TextStyle( - fontWeight: FontWeight.bold, - color: Color.fromRGBO(0x19, 0x8e, 0xee, 1), - ), - ), - ), - ListTile( - onTap: () { - model.setBlockOpenState( - block.name, - model.isOpen, - ); - }, - minVerticalPadding: 24, - trailing: !isCertification - ? OpenCloseIcon( - block: block, - model: model, - ) - : null, - title: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (!isCertification) - Padding( - padding: const EdgeInsets.fromLTRB(0, 8, 8, 8), - child: model.challengesCompleted == block.challenges.length - ? const Icon( - Icons.check_circle, - size: 20, - ) - : const Icon( - Icons.circle_outlined, - size: 20, - ), - ), - Expanded( - child: Text( - block.name, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - ), - ], - ), - ), - ], - ), - ); - } } class ChallengeTile extends StatelessWidget { @@ -376,7 +281,12 @@ class ChallengeTile extends StatelessWidget { bool isCompleted = model.completedChallenge(challengeId); return TextButton( - onPressed: () {}, + onPressed: () { + model.routeToChallengeView( + block, + challengeId, + ); + }, style: TextButton.styleFrom( backgroundColor: isCompleted ? const Color.fromRGBO(0x00, 0x2e, 0xad, 0.3) diff --git a/mobile-app/lib/ui/views/learn/widgets/open_close_icon_widget.dart b/mobile-app/lib/ui/views/learn/widgets/open_close_icon_widget.dart deleted file mode 100644 index 94cdfa047..000000000 --- a/mobile-app/lib/ui/views/learn/widgets/open_close_icon_widget.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:freecodecamp/models/learn/curriculum_model.dart'; -import 'package:freecodecamp/ui/views/learn/block/block_viewmodel.dart'; - -class OpenCloseIcon extends StatelessWidget { - const OpenCloseIcon({ - Key? key, - required this.block, - required this.model, - }) : super(key: key); - - final Block block; - final BlockViewModel model; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - iconSize: 35, - icon: model.isOpen - ? const Icon(Icons.expand_less) - : const Icon(Icons.expand_more), - onPressed: () async { - model.setBlockOpenState( - block.name, - model.isOpen, - ); - }, - ), - ], - ); - } -} From f1464a21080d0540e20abe2e32096d91a7885fb3 Mon Sep 17 00:00:00 2001 From: sembauke Date: Thu, 27 Mar 2025 09:29:02 +0100 Subject: [PATCH 04/24] refactor: update block view border radius and simplify superblock view logic --- .../lib/ui/views/learn/block/block_view.dart | 2 +- .../learn/superblock/superblock_view.dart | 19 ++++--------------- .../superblock/superblock_viewmodel.dart | 7 ------- 3 files changed, 5 insertions(+), 23 deletions(-) diff --git a/mobile-app/lib/ui/views/learn/block/block_view.dart b/mobile-app/lib/ui/views/learn/block/block_view.dart index f80ec2bdd..a4d463b4b 100644 --- a/mobile-app/lib/ui/views/learn/block/block_view.dart +++ b/mobile-app/lib/ui/views/learn/block/block_view.dart @@ -86,7 +86,7 @@ class BlockView extends StatelessWidget { ), backgroundColor: const Color.fromRGBO(0x2a, 0x2a, 0x40, 1), - borderRadius: BorderRadius.circular(5), + borderRadius: BorderRadius.circular(10), ), ), if (block.challenges.length == 1) diff --git a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart index d7a8eb614..44ecd7b02 100644 --- a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart +++ b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart @@ -83,21 +83,10 @@ class SuperBlockView extends StatelessWidget { ), child: Column( children: [ - FutureBuilder( - future: model.getBlockOpenState(superBlock.blocks![i]), - builder: (context, snapshot) { - if (snapshot.hasData) { - bool isOpen = snapshot.data!; - - return BlockView( - block: superBlock.blocks![i], - isOpen: isOpen, - isStepBased: superBlock.blocks![i].isStepBased, - ); - } - - return const CircularProgressIndicator(); - }, + BlockView( + block: superBlock.blocks![i], + isOpen: false, + isStepBased: superBlock.blocks![i].isStepBased, ) ], ), diff --git a/mobile-app/lib/ui/views/learn/superblock/superblock_viewmodel.dart b/mobile-app/lib/ui/views/learn/superblock/superblock_viewmodel.dart index 89d0d636f..0b45caffc 100644 --- a/mobile-app/lib/ui/views/learn/superblock/superblock_viewmodel.dart +++ b/mobile-app/lib/ui/views/learn/superblock/superblock_viewmodel.dart @@ -6,7 +6,6 @@ import 'package:freecodecamp/service/authentication/authentication_service.dart' import 'package:freecodecamp/service/dio_service.dart'; import 'package:freecodecamp/service/learn/learn_offline_service.dart'; import 'package:freecodecamp/service/learn/learn_service.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:stacked/stacked.dart'; class SuperBlockViewModel extends BaseViewModel { @@ -43,12 +42,6 @@ class SuperBlockViewModel extends BaseViewModel { } } - Future getBlockOpenState(Block block) async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - - return prefs.getBool(block.name) ?? block.order == 0; - } - Future getSuperBlockData( String dashedName, String name, From 51ef07f03bb3e28db591cf8b3964c2d5e94168f3 Mon Sep 17 00:00:00 2001 From: sembauke Date: Thu, 27 Mar 2025 09:50:10 +0100 Subject: [PATCH 05/24] refactor: clean up unused imports in block view and enhance certification routing --- mobile-app/lib/ui/views/learn/block/block_view.dart | 8 +++----- .../ui/views/learn/challenge/templates/template_view.dart | 1 + 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/mobile-app/lib/ui/views/learn/block/block_view.dart b/mobile-app/lib/ui/views/learn/block/block_view.dart index a4d463b4b..80551fd22 100644 --- a/mobile-app/lib/ui/views/learn/block/block_view.dart +++ b/mobile-app/lib/ui/views/learn/block/block_view.dart @@ -1,10 +1,6 @@ import 'package:flutter/material.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; import 'package:freecodecamp/ui/views/learn/block/block_viewmodel.dart'; -// import 'package:freecodecamp/ui/views/learn/utils/learn_globals.dart'; -// import 'package:freecodecamp/ui/views/learn/widgets/download_button_widget.dart'; -// import 'package:freecodecamp/ui/views/learn/widgets/progressbar_widget.dart'; -// import 'package:freecodecamp/ui/widgets/drawer_widget/drawer_widget_view.dart'; import 'package:stacked/stacked.dart'; class BlockView extends StatelessWidget { @@ -166,7 +162,9 @@ class BlockView extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(8.0), child: TextButton( - onPressed: () {}, + onPressed: () { + model.routeToCertification(block); + }, style: TextButton.styleFrom( backgroundColor: const Color.fromRGBO(0x5a, 0x01, 0xa7, 1), diff --git a/mobile-app/lib/ui/views/learn/challenge/templates/template_view.dart b/mobile-app/lib/ui/views/learn/challenge/templates/template_view.dart index 2cb1ac595..afcd5be9f 100644 --- a/mobile-app/lib/ui/views/learn/challenge/templates/template_view.dart +++ b/mobile-app/lib/ui/views/learn/challenge/templates/template_view.dart @@ -38,6 +38,7 @@ class ChallengeTemplateView extends StatelessWidget { int challNum = tiles.indexWhere((el) => el.id == challenge.id) + 1; switch (challengeType) { + case 14: case 0: return ChallengeView( challenge: challenge, From 92c33ce1bfbe9a125f8081fe1b48f70bb1c68285 Mon Sep 17 00:00:00 2001 From: sembauke Date: Thu, 27 Mar 2025 10:05:36 +0100 Subject: [PATCH 06/24] refactor: simplify block separator logic and remove unused padding method --- .../ui/views/learn/superblock/superblock_view.dart | 4 ++-- .../views/learn/superblock/superblock_viewmodel.dart | 12 ------------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart index 44ecd7b02..f946dfecd 100644 --- a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart +++ b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart @@ -69,8 +69,8 @@ class SuperBlockView extends StatelessWidget { return true; }, child: ListView.separated( - separatorBuilder: (context, int i) => Divider( - height: model.getPaddingBetweenBlocks(superBlock.blocks![i]), + separatorBuilder: (context, int i) => const Divider( + height: 0, color: Colors.transparent, ), shrinkWrap: true, diff --git a/mobile-app/lib/ui/views/learn/superblock/superblock_viewmodel.dart b/mobile-app/lib/ui/views/learn/superblock/superblock_viewmodel.dart index 0b45caffc..bf416fdf0 100644 --- a/mobile-app/lib/ui/views/learn/superblock/superblock_viewmodel.dart +++ b/mobile-app/lib/ui/views/learn/superblock/superblock_viewmodel.dart @@ -20,18 +20,6 @@ class SuperBlockViewModel extends BaseViewModel { final _dio = DioService.dio; - double getPaddingBetweenBlocks(Block block) { - if (block.isStepBased) { - return 3.0; - } - - if (block.dashedName == 'es6') { - return 0; - } - - return 50.0; - } - EdgeInsets getPaddingBeginAndEnd(int index, int challenges) { if (index == 0) { return const EdgeInsets.only(top: 16); From ceb62ae375bcfdb4b1188d328cb6da020f3b0643 Mon Sep 17 00:00:00 2001 From: sembauke Date: Thu, 27 Mar 2025 10:48:13 +0100 Subject: [PATCH 07/24] refactor: introduce BlockType and BlockLayout enums and add new grid and list view templates --- mobile-app/lib/models/learn/curriculum_model.dart | 12 ++++++++++++ .../views/learn/block/templates/grid/grid_view.dart | 0 .../learn/block/templates/grid/grid_viewmodel.dart | 0 .../learn/block/templates/link/link_viewmodel.dart | 0 .../views/learn/block/templates/list/list_view.dart | 0 .../learn/block/templates/list/list_viewmodel.dart | 0 6 files changed, 12 insertions(+) create mode 100644 mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart create mode 100644 mobile-app/lib/ui/views/learn/block/templates/grid/grid_viewmodel.dart create mode 100644 mobile-app/lib/ui/views/learn/block/templates/link/link_viewmodel.dart create mode 100644 mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart create mode 100644 mobile-app/lib/ui/views/learn/block/templates/list/list_viewmodel.dart diff --git a/mobile-app/lib/models/learn/curriculum_model.dart b/mobile-app/lib/models/learn/curriculum_model.dart index 3f38e6e46..edcfec116 100644 --- a/mobile-app/lib/models/learn/curriculum_model.dart +++ b/mobile-app/lib/models/learn/curriculum_model.dart @@ -46,6 +46,18 @@ class SuperBlock { } } +enum BlockType { lecture, workshop, lab, review, quiz, exam, legacy } + +enum BlockLayout { + challengeList, + challengeGrid, + challengeLink, + project, + legacyChallengeList, + legacyChallengeGrid, + legacyChallengeLink +} + class Block { final String name; final String dashedName; diff --git a/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart b/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart new file mode 100644 index 000000000..e69de29bb diff --git a/mobile-app/lib/ui/views/learn/block/templates/grid/grid_viewmodel.dart b/mobile-app/lib/ui/views/learn/block/templates/grid/grid_viewmodel.dart new file mode 100644 index 000000000..e69de29bb diff --git a/mobile-app/lib/ui/views/learn/block/templates/link/link_viewmodel.dart b/mobile-app/lib/ui/views/learn/block/templates/link/link_viewmodel.dart new file mode 100644 index 000000000..e69de29bb diff --git a/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart b/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart new file mode 100644 index 000000000..e69de29bb diff --git a/mobile-app/lib/ui/views/learn/block/templates/list/list_viewmodel.dart b/mobile-app/lib/ui/views/learn/block/templates/list/list_viewmodel.dart new file mode 100644 index 000000000..e69de29bb From 27fdde2e74e834493e89bf31abbc16f37bfddd19 Mon Sep 17 00:00:00 2001 From: sembauke Date: Thu, 27 Mar 2025 11:08:03 +0100 Subject: [PATCH 08/24] refactor: implement BlockGridView, BlockLinkView, and BlockListView with corresponding view models; rename BlockViewModel to BlockTemplateViewModel and update references --- .../lib/ui/views/learn/block/block_view.dart | 239 ++---------------- .../ui/views/learn/block/block_viewmodel.dart | 72 +++--- .../learn/block/templates/grid/grid_view.dart | 15 ++ .../block/templates/grid/grid_viewmodel.dart | 3 + .../learn/block/templates/link/link_view.dart | 15 ++ .../block/templates/link/link_viewmodel.dart | 3 + .../learn/block/templates/list/list_view.dart | 15 ++ .../block/templates/list/list_viewmodel.dart | 3 + .../learn/superblock/superblock_view.dart | 2 +- .../learn/widgets/download_button_widget.dart | 93 ------- .../learn/widgets/progressbar_widget.dart | 47 ---- 11 files changed, 106 insertions(+), 401 deletions(-) create mode 100644 mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart delete mode 100644 mobile-app/lib/ui/views/learn/widgets/download_button_widget.dart delete mode 100644 mobile-app/lib/ui/views/learn/widgets/progressbar_widget.dart diff --git a/mobile-app/lib/ui/views/learn/block/block_view.dart b/mobile-app/lib/ui/views/learn/block/block_view.dart index 80551fd22..c6a3b3cdd 100644 --- a/mobile-app/lib/ui/views/learn/block/block_view.dart +++ b/mobile-app/lib/ui/views/learn/block/block_view.dart @@ -3,12 +3,12 @@ import 'package:freecodecamp/models/learn/curriculum_model.dart'; import 'package:freecodecamp/ui/views/learn/block/block_viewmodel.dart'; import 'package:stacked/stacked.dart'; -class BlockView extends StatelessWidget { +class BlockTemplateView extends StatelessWidget { final Block block; final bool isOpen; final bool isStepBased; - const BlockView({ + const BlockTemplateView({ Key? key, required this.block, required this.isOpen, @@ -17,245 +17,36 @@ class BlockView extends StatelessWidget { @override Widget build(BuildContext context) { - return ViewModelBuilder.reactive( + return ViewModelBuilder.reactive( onViewModelReady: (model) async { model.init(block.challengeTiles); - model.setIsOpen = isOpen; - model.setIsDownloaded = await model.isBlockDownloaded(block); model.setIsDev = await model.developerService.developmentMode(); }, - viewModelBuilder: () => BlockViewModel(), + viewModelBuilder: () => BlockTemplateViewModel(), builder: ( context, model, child, ) { - double progress = model.challengesCompleted / block.challenges.length; + // double progress = model.challengesCompleted / block.challenges.length; return Padding( padding: const EdgeInsets.only(bottom: 16.0), child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: const Color.fromRGBO(0x3b, 0x3b, 0x4f, 1), - ), - color: const Color.fromRGBO(0x1b, 0x1b, 0x32, 1), - ), - padding: const EdgeInsets.all(8), - width: MediaQuery.of(context).size.width, - child: Column( - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Padding( - padding: EdgeInsets.all(8), - child: Icon( - Icons.monitor, - color: Color.fromRGBO(0x19, 0x8e, 0xee, 1), - size: 40, - ), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - block.name, - style: const TextStyle( - wordSpacing: 0, - letterSpacing: 0, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - if (block.challenges.length > 1) - Padding( - padding: const EdgeInsets.only( - top: 8, bottom: 8, right: 10), - child: LinearProgressIndicator( - minHeight: 10, - value: progress, - valueColor: const AlwaysStoppedAnimation( - Color.fromRGBO(0x99, 0xc9, 0xff, 1), - ), - backgroundColor: - const Color.fromRGBO(0x2a, 0x2a, 0x40, 1), - borderRadius: BorderRadius.circular(10), - ), - ), - if (block.challenges.length == 1) - Text(block.description.join()), - if (block.challenges.length > 1) - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric( - vertical: 4, - ), - child: TextButton( - onPressed: () { - model.setIsOpen = !model.isOpen; - }, - style: TextButton.styleFrom( - backgroundColor: const Color.fromRGBO( - 0x1b, 0x1b, 0x32, 1), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), - side: const BorderSide( - color: Color.fromRGBO( - 0x3b, 0x3b, 0x4f, 1), - ), - ), - ), - child: Text( - model.isOpen - ? 'Hide Steps' - : 'Show Steps', - ), - ), - ), - ], - ), - if (model.isOpen) - Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: 195, - width: - MediaQuery.of(context).size.width - 100, - child: GridView.builder( - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 6, - mainAxisExtent: 50, - mainAxisSpacing: 3, - crossAxisSpacing: 3), - itemCount: block.challenges.length, - itemBuilder: (context, step) { - return ChallengeTile( - block: block, - model: model, - step: step + 1, - challengeId: - block.challengeTiles[step].id, - isDowloaded: false, - ); - }, - ), - ), - ], - ), - ], - ), - ), - ], + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: const Color.fromRGBO(0x3b, 0x3b, 0x4f, 1), ), - if (block.challenges.length == 1) - Row( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: TextButton( - onPressed: () { - model.routeToCertification(block); - }, - style: TextButton.styleFrom( - backgroundColor: - const Color.fromRGBO(0x5a, 0x01, 0xa7, 1), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), - ), - ), - child: const Text( - 'Start', - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - ), - ), - ], - ) - ], - ), - ), + color: const Color.fromRGBO(0x1b, 0x1b, 0x32, 1), + ), + padding: const EdgeInsets.all(8), + width: MediaQuery.of(context).size.width, + child: Container()), ); }, ); } - - Widget dialogueWidget( - List challenges, - BuildContext context, - BlockViewModel model, - ) { - List> structure = []; - - List dialogueHeaders = []; - int dialogueIndex = 0; - - dialogueHeaders.add(challenges[0]); - structure.add([]); - - for (int i = 1; i < challenges.length; i++) { - if (challenges[i].title.contains('Dialogue')) { - structure.add([]); - dialogueHeaders.add(challenges[i]); - dialogueIndex++; - } else { - structure[dialogueIndex].add(challenges[i]); - } - } - return Column( - children: [ - ...List.generate(structure.length, (step) { - return Column( - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - dialogueHeaders[step].title, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ), - GridView.count( - physics: const ClampingScrollPhysics(), - shrinkWrap: true, - padding: const EdgeInsets.all(16), - crossAxisCount: (MediaQuery.of(context).size.width / 70 - - MediaQuery.of(context).viewPadding.horizontal) - .round(), - children: List.generate( - structure[step].length, - (index) { - return Center( - child: ChallengeTile( - block: block, - model: model, - challengeId: structure[step][index].id, - step: int.parse( - structure[step][index].title.split('Task')[1], - ), - isDowloaded: false, - ), - ); - }, - ), - ), - ], - ); - }) - ], - ); - } } class ChallengeTile extends StatelessWidget { @@ -269,7 +60,7 @@ class ChallengeTile extends StatelessWidget { }) : super(key: key); final Block block; - final BlockViewModel model; + final BlockTemplateViewModel model; final int step; final bool isDowloaded; final String challengeId; diff --git a/mobile-app/lib/ui/views/learn/block/block_viewmodel.dart b/mobile-app/lib/ui/views/learn/block/block_viewmodel.dart index 710b8f8bc..4d56243a9 100644 --- a/mobile-app/lib/ui/views/learn/block/block_viewmodel.dart +++ b/mobile-app/lib/ui/views/learn/block/block_viewmodel.dart @@ -15,7 +15,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; -class BlockViewModel extends BaseViewModel { +class BlockTemplateViewModel extends BaseViewModel { final NavigationService _navigationService = locator(); final AuthenticationService _auth = locator(); @@ -173,13 +173,13 @@ class BlockViewModel extends BaseViewModel { setIsDownloading = false; } - learnOfflineService.cancelChallengeDownload(block.dashedName).then( - (value) async { - setIsDownloaded = await isBlockDownloaded( - block, - ); - }, - ); + // learnOfflineService.cancelChallengeDownload(block.dashedName).then( + // (value) async { + // setIsDownloaded = await isBlockDownloaded( + // block, + // ); + // }, + // ); notifyListeners(); } catch (e) { @@ -200,32 +200,32 @@ class BlockViewModel extends BaseViewModel { .then((value) async { setIsDownloaded = true; }); - setIsDownloading = await isBlockDownloaded( - block, - ); - } - - Future isBlockDownloaded(Block incBlock) async { - List? blocks = await learnOfflineService.getCachedBlocks( - incBlock.superBlock.dashedName, - ); - - if (blocks != null) { - for (Block block in blocks) { - if (block.dashedName == incBlock.dashedName) { - return true; - } - } - } - - return false; - } - - Future isChallengeDownloaded(String id) async { - List downloaded = - await learnOfflineService.checkStoredChallenges(); - List ids = downloaded.map((e) => e!.id).toList(); - - return ids.contains(id); - } + // setIsDownloading = await isBlockDownloaded( + // block, + // ); + } + + // Future isBlockDownloaded(Block incBlock) async { + // List? blocks = await learnOfflineService.getCachedBlocks( + // incBlock.superBlock.dashedName, + // ); + + // if (blocks != null) { + // for (Block block in blocks) { + // if (block.dashedName == incBlock.dashedName) { + // return true; + // } + // } + // } + + // return false; + // } + + // Future isChallengeDownloaded(String id) async { + // List downloaded = + // await learnOfflineService.checkStoredChallenges(); + // List ids = downloaded.map((e) => e!.id).toList(); + + // return ids.contains(id); + // } } diff --git a/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart b/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart index e69de29bb..3c960e080 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart @@ -0,0 +1,15 @@ +import 'package:flutter/widgets.dart'; +import 'package:freecodecamp/ui/views/learn/block/templates/grid/grid_viewmodel.dart'; +import 'package:stacked/stacked.dart'; + +class BlockGridView extends StatelessWidget { + const BlockGridView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ViewModelBuilder.reactive( + viewModelBuilder: () => BlockGridViewModel(), + builder: (context, model, child) => Container(), + ); + } +} diff --git a/mobile-app/lib/ui/views/learn/block/templates/grid/grid_viewmodel.dart b/mobile-app/lib/ui/views/learn/block/templates/grid/grid_viewmodel.dart index e69de29bb..f2ece42f2 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/grid/grid_viewmodel.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/grid/grid_viewmodel.dart @@ -0,0 +1,3 @@ +import 'package:stacked/stacked.dart'; + +class BlockGridViewModel extends BaseViewModel {} diff --git a/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart b/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart new file mode 100644 index 000000000..994c9cc02 --- /dev/null +++ b/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart @@ -0,0 +1,15 @@ +import 'package:flutter/widgets.dart'; +import 'package:freecodecamp/ui/views/learn/block/templates/link/link_viewmodel.dart'; +import 'package:stacked/stacked.dart'; + +class BlockLinkView extends StatelessWidget { + const BlockLinkView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ViewModelBuilder.reactive( + viewModelBuilder: () => BlockLinkViewModel(), + builder: (context, model, child) => Container(), + ); + } +} diff --git a/mobile-app/lib/ui/views/learn/block/templates/link/link_viewmodel.dart b/mobile-app/lib/ui/views/learn/block/templates/link/link_viewmodel.dart index e69de29bb..d89e88b48 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/link/link_viewmodel.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/link/link_viewmodel.dart @@ -0,0 +1,3 @@ +import 'package:stacked/stacked.dart'; + +class BlockLinkViewModel extends BaseViewModel {} diff --git a/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart b/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart index e69de29bb..c3e3c08b3 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart @@ -0,0 +1,15 @@ +import 'package:flutter/widgets.dart'; +import 'package:freecodecamp/ui/views/learn/block/templates/grid/grid_viewmodel.dart'; +import 'package:stacked/stacked.dart'; + +class BlockListView extends StatelessWidget { + const BlockListView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ViewModelBuilder.reactive( + viewModelBuilder: () => BlockGridViewModel(), + builder: (context, model, child) => Container(), + ); + } +} diff --git a/mobile-app/lib/ui/views/learn/block/templates/list/list_viewmodel.dart b/mobile-app/lib/ui/views/learn/block/templates/list/list_viewmodel.dart index e69de29bb..663792ee9 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/list/list_viewmodel.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/list/list_viewmodel.dart @@ -0,0 +1,3 @@ +import 'package:stacked/stacked.dart'; + +class BlockListViewModel extends BaseViewModel {} diff --git a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart index f946dfecd..aa2fe6dab 100644 --- a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart +++ b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart @@ -83,7 +83,7 @@ class SuperBlockView extends StatelessWidget { ), child: Column( children: [ - BlockView( + BlockTemplateView( block: superBlock.blocks![i], isOpen: false, isStepBased: superBlock.blocks![i].isStepBased, diff --git a/mobile-app/lib/ui/views/learn/widgets/download_button_widget.dart b/mobile-app/lib/ui/views/learn/widgets/download_button_widget.dart deleted file mode 100644 index 1d886094b..000000000 --- a/mobile-app/lib/ui/views/learn/widgets/download_button_widget.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:freecodecamp/extensions/i18n_extension.dart'; -import 'package:freecodecamp/models/learn/curriculum_model.dart'; -import 'package:freecodecamp/ui/views/learn/block/block_viewmodel.dart'; - -class DownloadButton extends StatelessWidget { - const DownloadButton({ - Key? key, - required this.model, - required this.block, - }) : super(key: key); - - final BlockViewModel model; - final Block block; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - if (!model.isDownloaded || model.isDownloading) - Container( - width: MediaQuery.of(context).size.width, - margin: const EdgeInsets.symmetric(horizontal: 40), - child: ElevatedButton( - onPressed: !model.isDownloading - ? () async { - await model.startDownload(block); - } - : null, - style: ElevatedButton.styleFrom( - backgroundColor: const Color.fromRGBO(0x2A, 0x2A, 0x40, 1), - ), - child: !model.isDownloading - ? Text(context.t.challenge_download) - : StreamBuilder( - stream: model.learnOfflineService.downloadStream.stream, - builder: ((context, snapshot) { - if (snapshot.connectionState == - ConnectionState.waiting) { - return Text( - context.t.challenge_download_starting, - ); - } - - // if (snapshot.hasError) { - // model.stopDownload(block.dashedName); - - // Timer(const Duration(seconds: 5), () { - // model.setIsDownloading = false; - // }); - - // return const Text('An Error has Occured'); - // } - - if (snapshot.hasData) { - return Text( - '${(snapshot.data as double).toStringAsFixed(2)}%', - ); - } - - return Text( - context.t.challenge_download, - ); - }), - ), - ), - ), - if (model.isDownloaded || model.isDownloading) - Container( - width: MediaQuery.of(context).size.width, - margin: const EdgeInsets.symmetric(horizontal: 40), - child: ElevatedButton( - onPressed: () async { - model.isDownloading - ? model.stopDownload(block, false) - : model.stopDownload(block, true); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color.fromRGBO(0x2A, 0x2A, 0x40, 1), - ), - child: model.isDownloading - ? Text( - context.t.challenge_download_cancel, - ) - : Text( - context.t.challenge_download_delete, - ), - ), - ) - ], - ); - } -} diff --git a/mobile-app/lib/ui/views/learn/widgets/progressbar_widget.dart b/mobile-app/lib/ui/views/learn/widgets/progressbar_widget.dart deleted file mode 100644 index 99ccde890..000000000 --- a/mobile-app/lib/ui/views/learn/widgets/progressbar_widget.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:freecodecamp/models/learn/curriculum_model.dart'; -import 'package:freecodecamp/ui/views/learn/block/block_viewmodel.dart'; - -class ChallengeProgressBar extends StatelessWidget { - const ChallengeProgressBar({ - Key? key, - required this.block, - required this.model, - }) : super(key: key); - - final Block block; - final BlockViewModel model; - - @override - Widget build(BuildContext context) { - return Container( - color: const Color(0xFF0a0a23), - child: Container( - margin: const EdgeInsets.only(bottom: 1), - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - child: Row( - children: [ - Expanded( - child: LinearProgressIndicator( - color: const Color.fromRGBO(0x19, 0x8e, 0xee, 1), - backgroundColor: const Color.fromRGBO(0x2A, 0x2A, 0x40, 1), - minHeight: 10, - value: model.challengesCompleted / block.challenges.length, - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - '${(model.challengesCompleted / block.challenges.length * 100).round().toString()}%', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ) - ], - ), - ), - ); - } -} From 7f6d0c52366a34d1de4649a06f9cca69b6a29f7b Mon Sep 17 00:00:00 2001 From: sembauke Date: Thu, 27 Mar 2025 11:11:10 +0100 Subject: [PATCH 09/24] refactor: remove unused downloading logic and clean up BlockTemplateViewModel --- .../ui/views/learn/block/block_viewmodel.dart | 102 ------------------ 1 file changed, 102 deletions(-) diff --git a/mobile-app/lib/ui/views/learn/block/block_viewmodel.dart b/mobile-app/lib/ui/views/learn/block/block_viewmodel.dart index 4d56243a9..1225ae3a7 100644 --- a/mobile-app/lib/ui/views/learn/block/block_viewmodel.dart +++ b/mobile-app/lib/ui/views/learn/block/block_viewmodel.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:freecodecamp/app/app.locator.dart'; import 'package:freecodecamp/app/app.router.dart'; -import 'package:freecodecamp/models/learn/challenge_model.dart'; import 'package:freecodecamp/models/learn/completed_challenge_model.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; import 'package:freecodecamp/models/main/user_model.dart'; @@ -27,12 +26,6 @@ class BlockTemplateViewModel extends BaseViewModel { bool _isOpen = false; bool get isOpen => _isOpen; - bool _isDownloading = false; - bool get isDownloading => _isDownloading; - - bool _isDownloaded = false; - bool get isDownloaded => _isDownloaded; - final bool _isDownloadingSpecific = false; bool get isDownloadingSpecific => _isDownloadingSpecific; @@ -50,16 +43,6 @@ class BlockTemplateViewModel extends BaseViewModel { notifyListeners(); } - set setIsDownloading(bool value) { - _isDownloading = value; - notifyListeners(); - } - - set setIsDownloaded(bool value) { - _isDownloaded = value; - notifyListeners(); - } - set setIsDev(bool value) { _isDev = value; notifyListeners(); @@ -106,25 +89,6 @@ class BlockTemplateViewModel extends BaseViewModel { user = await _auth.userModel; setNumberOfCompletedChallenges(challengeBatch); notifyListeners(); - - learnOfflineService.downloadSub = - learnOfflineService.downloadStream.stream.listen( - (event) { - if (event == 100.00) { - setIsDownloading = false; - learnOfflineService.downloadStream.sink.add(0); - } else { - notifyListeners(); - } - }, - onDone: () { - setIsDownloading = false; - }, - ); - } - - void testChallenge(Challenge challenge) { - learnOfflineService.storeDownloadedChallenge(challenge); } void setNumberOfCompletedChallenges(List challengeBatch) { @@ -162,70 +126,4 @@ class BlockTemplateViewModel extends BaseViewModel { } return const Icon(Icons.circle_outlined); } - - void stopDownload(Block block, bool isAlreadyDownloaded) async { - try { - if (!isAlreadyDownloaded) { - learnOfflineService.downloadSub!.pause(); - learnOfflineService.batchSub!.pause(); - learnOfflineService.timer!.cancel(); - - setIsDownloading = false; - } - - // learnOfflineService.cancelChallengeDownload(block.dashedName).then( - // (value) async { - // setIsDownloaded = await isBlockDownloaded( - // block, - // ); - // }, - // ); - - notifyListeners(); - } catch (e) { - throw error(e); - } - } - - Future startDownload(Block block) async { - String url = LearnService.baseUrl; - learnOfflineService - .getChallengeBatch( - block, - block.challengeTiles - .map((e) => - '$url/challenges/${block.superBlock.dashedName}/${block.dashedName}/${e.id}.json') - .toList(), - ) - .then((value) async { - setIsDownloaded = true; - }); - // setIsDownloading = await isBlockDownloaded( - // block, - // ); - } - - // Future isBlockDownloaded(Block incBlock) async { - // List? blocks = await learnOfflineService.getCachedBlocks( - // incBlock.superBlock.dashedName, - // ); - - // if (blocks != null) { - // for (Block block in blocks) { - // if (block.dashedName == incBlock.dashedName) { - // return true; - // } - // } - // } - - // return false; - // } - - // Future isChallengeDownloaded(String id) async { - // List downloaded = - // await learnOfflineService.checkStoredChallenges(); - // List ids = downloaded.map((e) => e!.id).toList(); - - // return ids.contains(id); - // } } From b7fb59ea9fa08eb2d0a6bfa328d4104a34073121 Mon Sep 17 00:00:00 2001 From: sembauke Date: Thu, 27 Mar 2025 13:17:10 +0100 Subject: [PATCH 10/24] refactor: remove isStepBased property from SuperBlockView and Block model; update BlockListView and BlockLinkView to accept new parameters --- .../lib/models/learn/curriculum_model.dart | 57 +++---- .../service/learn/learn_offline_service.dart | 2 +- .../lib/ui/views/learn/block/block_view.dart | 133 ++++++++-------- .../ui/views/learn/block/block_viewmodel.dart | 9 -- .../learn/block/templates/grid/grid_view.dart | 143 +++++++++++++++++- .../learn/block/templates/link/link_view.dart | 45 +++++- .../learn/block/templates/list/list_view.dart | 13 +- .../learn/superblock/superblock_view.dart | 1 - 8 files changed, 287 insertions(+), 116 deletions(-) diff --git a/mobile-app/lib/models/learn/curriculum_model.dart b/mobile-app/lib/models/learn/curriculum_model.dart index edcfec116..84cc192cc 100644 --- a/mobile-app/lib/models/learn/curriculum_model.dart +++ b/mobile-app/lib/models/learn/curriculum_model.dart @@ -53,17 +53,15 @@ enum BlockLayout { challengeGrid, challengeLink, project, - legacyChallengeList, - legacyChallengeGrid, - legacyChallengeLink } class Block { final String name; final String dashedName; final SuperBlock superBlock; + final BlockLayout layout; + final BlockType type; final List description; - final bool isStepBased; final int order; final List challenges; @@ -71,25 +69,16 @@ class Block { Block({ required this.superBlock, + required this.layout, + required this.type, required this.name, required this.dashedName, required this.description, - required this.isStepBased, required this.order, required this.challenges, required this.challengeTiles, }); - static bool checkIfStepBased(String superblock) { - List stepbased = [ - '2022/responsive-web-design', - 'a2-english-for-developers', - 'b1-english-for-developers' - ]; - - return stepbased.contains(superblock); - } - factory Block.fromJson( Map data, List description, @@ -102,18 +91,34 @@ class Block { data['challengeTiles'] = []; + BlockLayout handleLayout(String? layout) { + switch (layout) { + case 'project-list': + case 'challenge-list': + case 'legacy-challenge-list': + return BlockLayout.challengeList; + case 'challenge-grid': + case 'legacy-challenge-grid': + return BlockLayout.challengeGrid; + case 'link': + case 'legacy-link': + return BlockLayout.challengeLink; + default: + return BlockLayout.challengeGrid; + } + } + return Block( superBlock: SuperBlock( dashedName: superBlockDashedName, name: superBlockName, ), + layout: handleLayout(data['blockLayout']), + type: BlockType.legacy, name: data['name'], dashedName: key, description: description, order: data['order'], - isStepBased: checkIfStepBased( - superBlockDashedName, - ), challenges: (data['challengeOrder'] as List) .map( (dynamic challenge) => ChallengeOrder( @@ -137,22 +142,6 @@ class Block { .toList(), ); } - - static Map toCachedObject(Block block) { - return { - 'superBlock': { - 'dashedName': block.superBlock.dashedName, - 'name': block.superBlock.name, - }, - 'name': block.name, - 'dashedName': block.dashedName, - 'description': block.description, - 'order': block.order, - 'isStepBased': block.isStepBased, - 'challengeOrder': block.challenges, - 'challengeTiles': block.challenges, - }; - } } class ChallengeListTile { diff --git a/mobile-app/lib/service/learn/learn_offline_service.dart b/mobile-app/lib/service/learn/learn_offline_service.dart index 112fc9f25..8dfc8146b 100644 --- a/mobile-app/lib/service/learn/learn_offline_service.dart +++ b/mobile-app/lib/service/learn/learn_offline_service.dart @@ -267,7 +267,7 @@ class LearnOfflineService { try { SharedPreferences prefs = await SharedPreferences.getInstance(); - Map blockToJson = Block.toCachedObject(block); + Map blockToJson = {}; List? storedBlocks = prefs.getStringList('storedBlocks'); if (storedBlocks == null) { diff --git a/mobile-app/lib/ui/views/learn/block/block_view.dart b/mobile-app/lib/ui/views/learn/block/block_view.dart index c6a3b3cdd..51cfb836e 100644 --- a/mobile-app/lib/ui/views/learn/block/block_view.dart +++ b/mobile-app/lib/ui/views/learn/block/block_view.dart @@ -1,18 +1,19 @@ import 'package:flutter/material.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; import 'package:freecodecamp/ui/views/learn/block/block_viewmodel.dart'; +import 'package:freecodecamp/ui/views/learn/block/templates/grid/grid_view.dart'; +import 'package:freecodecamp/ui/views/learn/block/templates/link/link_view.dart'; +import 'package:freecodecamp/ui/views/learn/block/templates/list/list_view.dart'; import 'package:stacked/stacked.dart'; class BlockTemplateView extends StatelessWidget { final Block block; final bool isOpen; - final bool isStepBased; const BlockTemplateView({ Key? key, required this.block, required this.isOpen, - required this.isStepBased, }) : super(key: key); @override @@ -28,73 +29,79 @@ class BlockTemplateView extends StatelessWidget { model, child, ) { - // double progress = model.challengesCompleted / block.challenges.length; - return Padding( padding: const EdgeInsets.only(bottom: 16.0), child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: const Color.fromRGBO(0x3b, 0x3b, 0x4f, 1), - ), - color: const Color.fromRGBO(0x1b, 0x1b, 0x32, 1), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: const Color.fromRGBO(0x3b, 0x3b, 0x4f, 1), ), - padding: const EdgeInsets.all(8), - width: MediaQuery.of(context).size.width, - child: Container()), - ); - }, - ); - } -} - -class ChallengeTile extends StatelessWidget { - const ChallengeTile({ - Key? key, - required this.block, - required this.model, - required this.step, - required this.isDowloaded, - required this.challengeId, - }) : super(key: key); - - final Block block; - final BlockTemplateViewModel model; - final int step; - final bool isDowloaded; - final String challengeId; - - @override - Widget build(BuildContext context) { - bool isCompleted = model.completedChallenge(challengeId); - - return TextButton( - onPressed: () { - model.routeToChallengeView( - block, - challengeId, + color: const Color.fromRGBO(0x1b, 0x1b, 0x32, 1), + ), + padding: const EdgeInsets.all(8), + width: MediaQuery.of(context).size.width, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Column( + children: [ + Padding( + padding: EdgeInsets.all(8.0), + child: Column( + children: [ + Icon( + Icons.monitor, + color: Color.fromRGBO(0x19, 0x8e, 0xee, 1), + ) + ], + ), + ), + ], + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + block.name, + style: const TextStyle( + wordSpacing: 0, + letterSpacing: 0, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Row( + children: [ + Builder( + builder: (BuildContext context) { + switch (block.layout) { + case BlockLayout.challengeGrid: + return BlockGridView( + block: block, model: model); + case BlockLayout.challengeList: + return BlockListView( + block: block, model: model); + case BlockLayout.challengeLink: + return BlockLinkView( + block: block, model: model); + default: + return BlockGridView( + block: block, model: model); + } + }, + ), + ], + ), + ], + ), + ), + ], + ), + ), ); }, - style: TextButton.styleFrom( - backgroundColor: isCompleted - ? const Color.fromRGBO(0x00, 0x2e, 0xad, 0.3) - : const Color.fromRGBO(0x2a, 0x2a, 0x40, 1), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), - side: isCompleted - ? const BorderSide( - width: 1, - color: Color.fromRGBO(0xbc, 0xe8, 0xf1, 1), - ) - : const BorderSide( - color: Color.fromRGBO(0x3b, 0x3b, 0x4f, 1), - ), - ), - ), - child: Text( - step.toString(), - ), ); } } diff --git a/mobile-app/lib/ui/views/learn/block/block_viewmodel.dart b/mobile-app/lib/ui/views/learn/block/block_viewmodel.dart index 1225ae3a7..2b64930d9 100644 --- a/mobile-app/lib/ui/views/learn/block/block_viewmodel.dart +++ b/mobile-app/lib/ui/views/learn/block/block_viewmodel.dart @@ -1,6 +1,4 @@ import 'dart:async'; - -import 'package:flutter/material.dart'; import 'package:freecodecamp/app/app.locator.dart'; import 'package:freecodecamp/app/app.router.dart'; import 'package:freecodecamp/models/learn/completed_challenge_model.dart'; @@ -119,11 +117,4 @@ class BlockTemplateViewModel extends BaseViewModel { return false; } - - Icon getIcon(bool completed) { - if (completed) { - return const Icon(Icons.check_circle); - } - return const Icon(Icons.circle_outlined); - } } diff --git a/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart b/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart index 3c960e080..4aacc0d11 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart @@ -1,15 +1,152 @@ -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:freecodecamp/models/learn/curriculum_model.dart'; +import 'package:freecodecamp/ui/views/learn/block/block_viewmodel.dart'; import 'package:freecodecamp/ui/views/learn/block/templates/grid/grid_viewmodel.dart'; import 'package:stacked/stacked.dart'; class BlockGridView extends StatelessWidget { - const BlockGridView({Key? key}) : super(key: key); + const BlockGridView({ + Key? key, + required this.block, + required this.model, + }) : super(key: key); + + final Block block; + final BlockTemplateViewModel model; @override Widget build(BuildContext context) { + // We want to make sure never to divide by 0 and show + // a progress percentage of 1% if non have been completed. + + double progress = model.challengesCompleted == 0 + ? 0.01 + : model.challengesCompleted / block.challenges.length; + return ViewModelBuilder.reactive( viewModelBuilder: () => BlockGridViewModel(), - builder: (context, model, child) => Container(), + builder: (context, childModel, child) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + // For some dumb reason the progress indicator does not + // get a specified width from the column. + width: MediaQuery.of(context).size.width * 0.7725, + child: LinearProgressIndicator( + minHeight: 10, + value: progress, + valueColor: const AlwaysStoppedAnimation( + Color.fromRGBO(0x99, 0xc9, 0xff, 1), + ), + backgroundColor: const Color.fromRGBO(0x2a, 0x2a, 0x40, 1), + borderRadius: BorderRadius.circular(10), + ), + ), + Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 4, + ), + child: TextButton( + onPressed: () { + model.setIsOpen = !model.isOpen; + }, + style: TextButton.styleFrom( + backgroundColor: const Color.fromRGBO(0x1b, 0x1b, 0x32, 1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + side: const BorderSide( + color: Color.fromRGBO(0x3b, 0x3b, 0x4f, 1), + ), + ), + ), + child: Text( + model.isOpen ? 'Hide Steps' : 'Show Steps', + ), + ), + ), + ], + ), + if (model.isOpen) + Row( + children: [ + SizedBox( + height: 195, + width: MediaQuery.of(context).size.width - 100, + child: GridView.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 6, + mainAxisExtent: 50, + mainAxisSpacing: 3, + crossAxisSpacing: 3), + itemCount: block.challenges.length, + itemBuilder: (context, step) { + return ChallengeTile( + block: block, + model: model, + step: step + 1, + challengeId: block.challengeTiles[step].id, + isDowloaded: false, + ); + }, + ), + ), + ], + ), + ], + ), + ); + } +} + +class ChallengeTile extends StatelessWidget { + const ChallengeTile({ + Key? key, + required this.block, + required this.model, + required this.step, + required this.isDowloaded, + required this.challengeId, + }) : super(key: key); + + final Block block; + final BlockTemplateViewModel model; + final int step; + final bool isDowloaded; + final String challengeId; + + @override + Widget build(BuildContext context) { + bool isCompleted = model.completedChallenge(challengeId); + + return TextButton( + onPressed: () { + model.routeToChallengeView( + block, + challengeId, + ); + }, + style: TextButton.styleFrom( + backgroundColor: isCompleted + ? const Color.fromRGBO(0x00, 0x2e, 0xad, 0.3) + : const Color.fromRGBO(0x2a, 0x2a, 0x40, 1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + side: isCompleted + ? const BorderSide( + width: 1, + color: Color.fromRGBO(0xbc, 0xe8, 0xf1, 1), + ) + : const BorderSide( + color: Color.fromRGBO(0x3b, 0x3b, 0x4f, 1), + ), + ), + ), + child: Text( + step.toString(), + ), ); } } diff --git a/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart b/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart index 994c9cc02..bf8efe79f 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart @@ -1,15 +1,54 @@ -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:freecodecamp/models/learn/curriculum_model.dart'; +import 'package:freecodecamp/ui/views/learn/block/block_viewmodel.dart'; import 'package:freecodecamp/ui/views/learn/block/templates/link/link_viewmodel.dart'; import 'package:stacked/stacked.dart'; class BlockLinkView extends StatelessWidget { - const BlockLinkView({Key? key}) : super(key: key); + const BlockLinkView({ + Key? key, + required this.block, + required this.model, + }) : super(key: key); + + final Block block; + final BlockTemplateViewModel model; @override Widget build(BuildContext context) { return ViewModelBuilder.reactive( viewModelBuilder: () => BlockLinkViewModel(), - builder: (context, model, child) => Container(), + builder: (context, childModel, child) { + return Expanded( + child: Column( + children: [ + Text(block.description.join()), + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () { + model.routeToCertification(block); + }, + style: TextButton.styleFrom( + backgroundColor: + const Color.fromRGBO(0x5a, 0x01, 0xa7, 1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + ), + child: const Text( + 'Start', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + ), + ], + ), + ], + ), + ); + }, ); } } diff --git a/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart b/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart index c3e3c08b3..4962e18c0 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart @@ -1,15 +1,24 @@ import 'package:flutter/widgets.dart'; +import 'package:freecodecamp/models/learn/curriculum_model.dart'; +import 'package:freecodecamp/ui/views/learn/block/block_viewmodel.dart'; import 'package:freecodecamp/ui/views/learn/block/templates/grid/grid_viewmodel.dart'; import 'package:stacked/stacked.dart'; class BlockListView extends StatelessWidget { - const BlockListView({Key? key}) : super(key: key); + const BlockListView({ + Key? key, + required this.block, + required this.model, + }) : super(key: key); + + final Block block; + final BlockTemplateViewModel model; @override Widget build(BuildContext context) { return ViewModelBuilder.reactive( viewModelBuilder: () => BlockGridViewModel(), - builder: (context, model, child) => Container(), + builder: (context, childModel, child) => const Text('list view'), ); } } diff --git a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart index aa2fe6dab..95a526484 100644 --- a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart +++ b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart @@ -86,7 +86,6 @@ class SuperBlockView extends StatelessWidget { BlockTemplateView( block: superBlock.blocks![i], isOpen: false, - isStepBased: superBlock.blocks![i].isStepBased, ) ], ), From a440a44b28e3604d30558a28c8a8bed4955b5795 Mon Sep 17 00:00:00 2001 From: sembauke Date: Thu, 27 Mar 2025 13:35:41 +0100 Subject: [PATCH 11/24] refactor: update imports in block templates and SuperBlockView --- .../learn/block/{block_view.dart => block_template_view.dart} | 2 +- .../{block_viewmodel.dart => block_template_viewmodel.dart} | 0 .../lib/ui/views/learn/block/templates/grid/grid_view.dart | 2 +- .../lib/ui/views/learn/block/templates/link/link_view.dart | 2 +- .../lib/ui/views/learn/block/templates/list/list_view.dart | 2 +- mobile-app/lib/ui/views/learn/superblock/superblock_view.dart | 4 ++-- 6 files changed, 6 insertions(+), 6 deletions(-) rename mobile-app/lib/ui/views/learn/block/{block_view.dart => block_template_view.dart} (97%) rename mobile-app/lib/ui/views/learn/block/{block_viewmodel.dart => block_template_viewmodel.dart} (100%) diff --git a/mobile-app/lib/ui/views/learn/block/block_view.dart b/mobile-app/lib/ui/views/learn/block/block_template_view.dart similarity index 97% rename from mobile-app/lib/ui/views/learn/block/block_view.dart rename to mobile-app/lib/ui/views/learn/block/block_template_view.dart index 51cfb836e..0e0868500 100644 --- a/mobile-app/lib/ui/views/learn/block/block_view.dart +++ b/mobile-app/lib/ui/views/learn/block/block_template_view.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; -import 'package:freecodecamp/ui/views/learn/block/block_viewmodel.dart'; +import 'package:freecodecamp/ui/views/learn/block/block_template_viewmodel.dart'; import 'package:freecodecamp/ui/views/learn/block/templates/grid/grid_view.dart'; import 'package:freecodecamp/ui/views/learn/block/templates/link/link_view.dart'; import 'package:freecodecamp/ui/views/learn/block/templates/list/list_view.dart'; diff --git a/mobile-app/lib/ui/views/learn/block/block_viewmodel.dart b/mobile-app/lib/ui/views/learn/block/block_template_viewmodel.dart similarity index 100% rename from mobile-app/lib/ui/views/learn/block/block_viewmodel.dart rename to mobile-app/lib/ui/views/learn/block/block_template_viewmodel.dart diff --git a/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart b/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart index 4aacc0d11..42ddac9ab 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; -import 'package:freecodecamp/ui/views/learn/block/block_viewmodel.dart'; +import 'package:freecodecamp/ui/views/learn/block/block_template_viewmodel.dart'; import 'package:freecodecamp/ui/views/learn/block/templates/grid/grid_viewmodel.dart'; import 'package:stacked/stacked.dart'; diff --git a/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart b/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart index bf8efe79f..c0e0dc01f 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; -import 'package:freecodecamp/ui/views/learn/block/block_viewmodel.dart'; +import 'package:freecodecamp/ui/views/learn/block/block_template_viewmodel.dart'; import 'package:freecodecamp/ui/views/learn/block/templates/link/link_viewmodel.dart'; import 'package:stacked/stacked.dart'; diff --git a/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart b/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart index 4962e18c0..822492672 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; -import 'package:freecodecamp/ui/views/learn/block/block_viewmodel.dart'; +import 'package:freecodecamp/ui/views/learn/block/block_template_viewmodel.dart'; import 'package:freecodecamp/ui/views/learn/block/templates/grid/grid_viewmodel.dart'; import 'package:stacked/stacked.dart'; diff --git a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart index 95a526484..2e6ffbabb 100644 --- a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart +++ b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:freecodecamp/extensions/i18n_extension.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; import 'package:freecodecamp/service/authentication/authentication_service.dart'; -import 'package:freecodecamp/ui/views/learn/block/block_view.dart'; +import 'package:freecodecamp/ui/views/learn/block/block_template_view.dart'; import 'package:freecodecamp/ui/views/learn/superblock/superblock_viewmodel.dart'; import 'package:stacked/stacked.dart'; @@ -85,7 +85,7 @@ class SuperBlockView extends StatelessWidget { children: [ BlockTemplateView( block: superBlock.blocks![i], - isOpen: false, + isOpen: superBlock.blocks!.length <= 5, ) ], ), From 1c7f8d210bfbb316e9cdc137d57c05c88c06e7c2 Mon Sep 17 00:00:00 2001 From: sembauke Date: Thu, 27 Mar 2025 14:16:13 +0100 Subject: [PATCH 12/24] refactor: update ListView in BlockListView to include progress indicator and toggle button; --- .../learn/block/templates/list/list_view.dart | 97 ++++++++++++++++++- .../learn/superblock/superblock_view.dart | 2 +- 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart b/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart index 822492672..d903816c2 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart @@ -1,4 +1,4 @@ -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; import 'package:freecodecamp/ui/views/learn/block/block_template_viewmodel.dart'; import 'package:freecodecamp/ui/views/learn/block/templates/grid/grid_viewmodel.dart'; @@ -16,9 +16,102 @@ class BlockListView extends StatelessWidget { @override Widget build(BuildContext context) { + double progress = model.challengesCompleted == 0 + ? 0.01 + : model.challengesCompleted / block.challenges.length; return ViewModelBuilder.reactive( viewModelBuilder: () => BlockGridViewModel(), - builder: (context, childModel, child) => const Text('list view'), + builder: (context, childModel, child) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + // For some dumb reason the progress indicator does not + // get a specified width from the column. + width: MediaQuery.of(context).size.width * 0.7725, + child: LinearProgressIndicator( + minHeight: 10, + value: progress, + valueColor: const AlwaysStoppedAnimation( + Color.fromRGBO(0x99, 0xc9, 0xff, 1), + ), + backgroundColor: const Color.fromRGBO(0x2a, 0x2a, 0x40, 1), + borderRadius: BorderRadius.circular(10), + ), + ), + Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 4, + ), + child: TextButton( + onPressed: () { + model.setIsOpen = !model.isOpen; + }, + style: TextButton.styleFrom( + backgroundColor: const Color.fromRGBO(0x1b, 0x1b, 0x32, 1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + side: const BorderSide( + color: Color.fromRGBO(0x3b, 0x3b, 0x4f, 1), + ), + ), + ), + child: Text( + model.isOpen ? 'Hide' : 'Show', + ), + ), + ), + ], + ), + if (model.isOpen) + Row( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width * 0.7725, + child: ListView.builder( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + itemCount: block.challenges.length, + itemBuilder: (context, index) { + bool isCompleted = + model.completedChallenge(block.challenges[index].id); + + return InkWell( + onTap: () { + model.routeToChallengeView( + block, + block.challenges[index].id, + ); + }, + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + side: isCompleted + ? const BorderSide( + width: 1, + color: Color.fromRGBO(0xbc, 0xe8, 0xf1, 1), + ) + : const BorderSide( + color: Color.fromRGBO(0x3b, 0x3b, 0x4f, 1), + ), + ), + color: isCompleted + ? const Color.fromRGBO(0x00, 0x2e, 0xad, 0.3) + : const Color.fromRGBO(0x2a, 0x2a, 0x40, 1), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(block.challenges[index].title), + ), + ), + ); + }, + ), + ), + ], + ), + ], + ), ); } } diff --git a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart index 2e6ffbabb..ab3d12b62 100644 --- a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart +++ b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart @@ -85,7 +85,7 @@ class SuperBlockView extends StatelessWidget { children: [ BlockTemplateView( block: superBlock.blocks![i], - isOpen: superBlock.blocks!.length <= 5, + isOpen: superBlock.blocks!.length <= 3, ) ], ), From 77c09ce76f668e55c05c36c6f2aaa7f5840bcb0a Mon Sep 17 00:00:00 2001 From: sembauke Date: Fri, 28 Mar 2025 09:01:31 +0100 Subject: [PATCH 13/24] refactor: simplify layout in BlockTemplateView and BlockGridView; adjust padding and width for better responsiveness --- .../learn/block/block_template_view.dart | 41 +++++++--------- .../learn/block/templates/grid/grid_view.dart | 23 ++++----- .../learn/block/templates/link/link_view.dart | 49 ++++++++++--------- 3 files changed, 56 insertions(+), 57 deletions(-) diff --git a/mobile-app/lib/ui/views/learn/block/block_template_view.dart b/mobile-app/lib/ui/views/learn/block/block_template_view.dart index 0e0868500..f586435b9 100644 --- a/mobile-app/lib/ui/views/learn/block/block_template_view.dart +++ b/mobile-app/lib/ui/views/learn/block/block_template_view.dart @@ -44,33 +44,28 @@ class BlockTemplateView extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Column( - children: [ - Padding( - padding: EdgeInsets.all(8.0), - child: Column( - children: [ - Icon( - Icons.monitor, - color: Color.fromRGBO(0x19, 0x8e, 0xee, 1), - ) - ], - ), - ), - ], - ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - block.name, - style: const TextStyle( - wordSpacing: 0, - letterSpacing: 0, - fontSize: 18, - fontWeight: FontWeight.bold, - ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + block.name, + style: const TextStyle( + wordSpacing: 0, + letterSpacing: 0, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], ), Row( children: [ diff --git a/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart b/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart index 42ddac9ab..43df5d36b 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart @@ -28,10 +28,11 @@ class BlockGridView extends StatelessWidget { builder: (context, childModel, child) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( + Container( + padding: const EdgeInsets.all(8), // For some dumb reason the progress indicator does not // get a specified width from the column. - width: MediaQuery.of(context).size.width * 0.7725, + width: MediaQuery.of(context).size.width * 0.9, child: LinearProgressIndicator( minHeight: 10, value: progress, @@ -45,9 +46,7 @@ class BlockGridView extends StatelessWidget { Row( children: [ Padding( - padding: const EdgeInsets.symmetric( - vertical: 4, - ), + padding: const EdgeInsets.all(8), child: TextButton( onPressed: () { model.setIsOpen = !model.isOpen; @@ -71,16 +70,18 @@ class BlockGridView extends StatelessWidget { if (model.isOpen) Row( children: [ - SizedBox( + Container( + padding: const EdgeInsets.all(8), height: 195, - width: MediaQuery.of(context).size.width - 100, + width: MediaQuery.of(context).size.width - 34, child: GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 6, - mainAxisExtent: 50, - mainAxisSpacing: 3, - crossAxisSpacing: 3), + crossAxisCount: 6, + mainAxisExtent: 60, + mainAxisSpacing: 3, + crossAxisSpacing: 3, + ), itemCount: block.challenges.length, itemBuilder: (context, step) { return ChallengeTile( diff --git a/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart b/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart index c0e0dc01f..00215451a 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart @@ -20,32 +20,35 @@ class BlockLinkView extends StatelessWidget { viewModelBuilder: () => BlockLinkViewModel(), builder: (context, childModel, child) { return Expanded( - child: Column( - children: [ - Text(block.description.join()), - Row( - children: [ - Expanded( - child: TextButton( - onPressed: () { - model.routeToCertification(block); - }, - style: TextButton.styleFrom( - backgroundColor: - const Color.fromRGBO(0x5a, 0x01, 0xa7, 1), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Text(block.description.join()), + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () { + model.routeToCertification(block); + }, + style: TextButton.styleFrom( + backgroundColor: + const Color.fromRGBO(0x5a, 0x01, 0xa7, 1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + ), + child: const Text( + 'Start', + style: TextStyle(fontWeight: FontWeight.bold), ), - ), - child: const Text( - 'Start', - style: TextStyle(fontWeight: FontWeight.bold), ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), ); }, From 12d2845f1b6a598be21e707c720566eb80161f66 Mon Sep 17 00:00:00 2001 From: sembauke Date: Fri, 28 Mar 2025 09:47:46 +0100 Subject: [PATCH 14/24] refactor: update BlockTemplateView and BlockListView to handle isOpen state; adjust layout for better responsiveness --- .../ui/views/learn/block/block_template_view.dart | 1 + .../learn/block/templates/list/list_view.dart | 14 +++++++------- .../ui/views/learn/superblock/superblock_view.dart | 3 ++- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/mobile-app/lib/ui/views/learn/block/block_template_view.dart b/mobile-app/lib/ui/views/learn/block/block_template_view.dart index f586435b9..472690abf 100644 --- a/mobile-app/lib/ui/views/learn/block/block_template_view.dart +++ b/mobile-app/lib/ui/views/learn/block/block_template_view.dart @@ -22,6 +22,7 @@ class BlockTemplateView extends StatelessWidget { onViewModelReady: (model) async { model.init(block.challengeTiles); model.setIsDev = await model.developerService.developmentMode(); + model.setIsOpen = isOpen; }, viewModelBuilder: () => BlockTemplateViewModel(), builder: ( diff --git a/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart b/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart index d903816c2..3eb4e1f67 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart @@ -24,10 +24,11 @@ class BlockListView extends StatelessWidget { builder: (context, childModel, child) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( + Container( + padding: const EdgeInsets.all(8), // For some dumb reason the progress indicator does not // get a specified width from the column. - width: MediaQuery.of(context).size.width * 0.7725, + width: MediaQuery.of(context).size.width * 0.9, child: LinearProgressIndicator( minHeight: 10, value: progress, @@ -41,9 +42,7 @@ class BlockListView extends StatelessWidget { Row( children: [ Padding( - padding: const EdgeInsets.symmetric( - vertical: 4, - ), + padding: const EdgeInsets.all(8), child: TextButton( onPressed: () { model.setIsOpen = !model.isOpen; @@ -67,8 +66,9 @@ class BlockListView extends StatelessWidget { if (model.isOpen) Row( children: [ - SizedBox( - width: MediaQuery.of(context).size.width * 0.7725, + Container( + padding: const EdgeInsets.all(8), + width: MediaQuery.of(context).size.width - 34, child: ListView.builder( shrinkWrap: true, physics: const ClampingScrollPhysics(), diff --git a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart index ab3d12b62..7582693a1 100644 --- a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart +++ b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart @@ -85,7 +85,8 @@ class SuperBlockView extends StatelessWidget { children: [ BlockTemplateView( block: superBlock.blocks![i], - isOpen: superBlock.blocks!.length <= 3, + isOpen: superBlock.blocks!.length <= 3 || + superBlock.blocks![i].order == 0, ) ], ), From ef7ecde9330fab747135e00ae0447c68a077a5b4 Mon Sep 17 00:00:00 2001 From: sembauke Date: Tue, 1 Apr 2025 11:23:39 +0200 Subject: [PATCH 15/24] feat: add challengeDialogue layout and BlockDialogueView --- .../lib/models/learn/curriculum_model.dart | 3 + .../learn/block/block_template_view.dart | 22 ++- .../templates/dialogue/dialogue_view.dart | 181 ++++++++++++++++++ .../dialogue/dialogue_viewmodel.dart | 3 + 4 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart create mode 100644 mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_viewmodel.dart diff --git a/mobile-app/lib/models/learn/curriculum_model.dart b/mobile-app/lib/models/learn/curriculum_model.dart index 84cc192cc..32cdaad8c 100644 --- a/mobile-app/lib/models/learn/curriculum_model.dart +++ b/mobile-app/lib/models/learn/curriculum_model.dart @@ -51,6 +51,7 @@ enum BlockType { lecture, workshop, lab, review, quiz, exam, legacy } enum BlockLayout { challengeList, challengeGrid, + challengeDialogue, challengeLink, project, } @@ -97,6 +98,8 @@ class Block { case 'challenge-list': case 'legacy-challenge-list': return BlockLayout.challengeList; + case 'dialogue-grid': + return BlockLayout.challengeDialogue; case 'challenge-grid': case 'legacy-challenge-grid': return BlockLayout.challengeGrid; diff --git a/mobile-app/lib/ui/views/learn/block/block_template_view.dart b/mobile-app/lib/ui/views/learn/block/block_template_view.dart index 472690abf..d7006a63f 100644 --- a/mobile-app/lib/ui/views/learn/block/block_template_view.dart +++ b/mobile-app/lib/ui/views/learn/block/block_template_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; import 'package:freecodecamp/ui/views/learn/block/block_template_viewmodel.dart'; +import 'package:freecodecamp/ui/views/learn/block/templates/dialogue/dialogue_view.dart'; import 'package:freecodecamp/ui/views/learn/block/templates/grid/grid_view.dart'; import 'package:freecodecamp/ui/views/learn/block/templates/link/link_view.dart'; import 'package:freecodecamp/ui/views/learn/block/templates/list/list_view.dart'; @@ -75,16 +76,29 @@ class BlockTemplateView extends StatelessWidget { switch (block.layout) { case BlockLayout.challengeGrid: return BlockGridView( - block: block, model: model); + block: block, + model: model, + ); + case BlockLayout.challengeDialogue: + return BlockDialogueView( + block: block, + model: model, + ); case BlockLayout.challengeList: return BlockListView( - block: block, model: model); + block: block, + model: model, + ); case BlockLayout.challengeLink: return BlockLinkView( - block: block, model: model); + block: block, + model: model, + ); default: return BlockGridView( - block: block, model: model); + block: block, + model: model, + ); } }, ), diff --git a/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart b/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart new file mode 100644 index 000000000..2bd09daa3 --- /dev/null +++ b/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart @@ -0,0 +1,181 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:freecodecamp/models/learn/curriculum_model.dart'; +import 'package:freecodecamp/ui/views/learn/block/block_template_viewmodel.dart'; +import 'package:freecodecamp/ui/views/learn/block/templates/dialogue/dialogue_viewmodel.dart'; +import 'package:stacked/stacked.dart'; + +class BlockDialogueView extends StatelessWidget { + const BlockDialogueView({ + Key? key, + required this.block, + required this.model, + }) : super(key: key); + + final Block block; + final BlockTemplateViewModel model; + + @override + Widget build(BuildContext context) { + List challenges = block.challenges; + List> structure = []; + List dialogueHeaders = []; + int dialogueIndex = 0; + + dialogueHeaders.add(challenges[0]); + structure.add([]); + + for (int i = 1; i < challenges.length; i++) { + if (challenges[i].title.contains('Dialogue')) { + structure.add([]); + dialogueHeaders.add(challenges[i]); + dialogueIndex++; + } else { + structure[dialogueIndex].add(challenges[i]); + } + } + + log(structure.toString()); + + return ViewModelBuilder.reactive( + viewModelBuilder: () => BlockDialogueViewModel(), + builder: (context, childModel, child) => Column( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width - 34, + child: ListView.builder( + physics: const ClampingScrollPhysics(), + shrinkWrap: true, + itemCount: structure.length, + itemBuilder: (context, dialogueBlock) { + return Column( + children: [ + Row( + children: [ + Expanded( + child: InkWell( + onTap: () => model.routeToChallengeView( + block, + dialogueHeaders[dialogueBlock].id, + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: model.completedChallenge( + dialogueHeaders[dialogueBlock].id, + ) + ? const Color.fromRGBO( + 0x00, 0x2e, 0xad, 0.3) + : const Color.fromRGBO(0x2a, 0x2a, 0x40, 1), + border: Border.all( + color: model.completedChallenge( + dialogueHeaders[dialogueBlock].id) + ? const Color.fromRGBO( + 0xbc, 0xe8, 0xf1, 1) + : const Color.fromRGBO( + 0x3b, 0x3b, 0x4f, 1), + width: 1, + ), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + dialogueHeaders[dialogueBlock].title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ), + ], + ), + SizedBox( + width: MediaQuery.of(context).size.width - 34, + height: 200, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: GridView.builder( + itemCount: structure[dialogueBlock].length, + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 6, + mainAxisExtent: 60, + mainAxisSpacing: 3, + crossAxisSpacing: 3, + ), + itemBuilder: (context, task) { + return ChallengeTile( + block: block, + model: model, + challengeId: structure[dialogueBlock][task].id, + step: task + 1, + isDowloaded: false, + ); + }, + ), + ), + ) + ], + ); + }, + ), + ), + ], + ), + ); + } +} + +class ChallengeTile extends StatelessWidget { + const ChallengeTile({ + Key? key, + required this.block, + required this.model, + required this.step, + required this.isDowloaded, + required this.challengeId, + }) : super(key: key); + + final Block block; + final BlockTemplateViewModel model; + final int step; + final bool isDowloaded; + final String challengeId; + + @override + Widget build(BuildContext context) { + bool isCompleted = model.completedChallenge(challengeId); + + return TextButton( + onPressed: () { + model.routeToChallengeView( + block, + challengeId, + ); + }, + style: TextButton.styleFrom( + backgroundColor: isCompleted + ? const Color.fromRGBO(0x00, 0x2e, 0xad, 0.3) + : const Color.fromRGBO(0x2a, 0x2a, 0x40, 1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + side: isCompleted + ? const BorderSide( + width: 1, + color: Color.fromRGBO(0xbc, 0xe8, 0xf1, 1), + ) + : const BorderSide( + color: Color.fromRGBO(0x3b, 0x3b, 0x4f, 1), + ), + ), + ), + child: Text( + step.toString(), + ), + ); + } +} diff --git a/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_viewmodel.dart b/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_viewmodel.dart new file mode 100644 index 000000000..6d9dee201 --- /dev/null +++ b/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_viewmodel.dart @@ -0,0 +1,3 @@ +import 'package:stacked/stacked.dart'; + +class BlockDialogueViewModel extends BaseViewModel {} From f474837e339c530e6a5c6c1c20315b6fc0676a51 Mon Sep 17 00:00:00 2001 From: sembauke Date: Thu, 3 Apr 2025 08:13:56 +0200 Subject: [PATCH 16/24] feat: add RobotoMono font family to dark theme --- mobile-app/lib/ui/theme/fcc_theme.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/mobile-app/lib/ui/theme/fcc_theme.dart b/mobile-app/lib/ui/theme/fcc_theme.dart index 944ae9947..bfb9e75ca 100644 --- a/mobile-app/lib/ui/theme/fcc_theme.dart +++ b/mobile-app/lib/ui/theme/fcc_theme.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; class FccTheme { static ThemeData themeDark = ThemeData( brightness: Brightness.dark, + fontFamily: 'RobotoMono', scaffoldBackgroundColor: const Color.fromRGBO(0x2A, 0x2A, 0x40, 1), appBarTheme: const AppBarTheme( centerTitle: true, From 40bd83e3050c5d5e0206731a9edb897abb79864b Mon Sep 17 00:00:00 2001 From: sembauke Date: Thu, 3 Apr 2025 08:20:55 +0200 Subject: [PATCH 17/24] fix: correct spelling of 'isDownloaded' in BlockDialogueView and BlockGridView Co-authored-by: Huyen 25715018+huyenltnguyen@users.noreply.github.com --- .../views/learn/block/templates/dialogue/dialogue_view.dart | 6 +++--- .../lib/ui/views/learn/block/templates/grid/grid_view.dart | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart b/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart index 2bd09daa3..360f4b79e 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart @@ -113,7 +113,7 @@ class BlockDialogueView extends StatelessWidget { model: model, challengeId: structure[dialogueBlock][task].id, step: task + 1, - isDowloaded: false, + isDownloaded: false, ); }, ), @@ -136,14 +136,14 @@ class ChallengeTile extends StatelessWidget { required this.block, required this.model, required this.step, - required this.isDowloaded, + required this.isDownloaded, required this.challengeId, }) : super(key: key); final Block block; final BlockTemplateViewModel model; final int step; - final bool isDowloaded; + final bool isDownloaded; final String challengeId; @override diff --git a/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart b/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart index 43df5d36b..bb5f45fe7 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart @@ -89,7 +89,7 @@ class BlockGridView extends StatelessWidget { model: model, step: step + 1, challengeId: block.challengeTiles[step].id, - isDowloaded: false, + isDownloaded: false, ); }, ), @@ -108,14 +108,14 @@ class ChallengeTile extends StatelessWidget { required this.block, required this.model, required this.step, - required this.isDowloaded, + required this.isDownloaded, required this.challengeId, }) : super(key: key); final Block block; final BlockTemplateViewModel model; final int step; - final bool isDowloaded; + final bool isDownloaded; final String challengeId; @override From 355792d60d7593aba7ffe403e9c3a799cdf743b2 Mon Sep 17 00:00:00 2001 From: sembauke Date: Mon, 7 Apr 2025 07:36:39 +0200 Subject: [PATCH 18/24] chore: implement Niraj's suggestions --- .../learn/block/block_template_view.dart | 36 +------- .../learn/block/block_template_viewmodel.dart | 42 ++++++++- .../templates/dialogue/dialogue_view.dart | 87 +++++-------------- .../learn/block/templates/grid/grid_view.dart | 87 +++++-------------- .../learn/block/templates/link/link_view.dart | 2 +- .../learn/superblock/superblock_view.dart | 6 +- .../views/learn/widgets/challenge_title.dart | 53 +++++++++++ 7 files changed, 136 insertions(+), 177 deletions(-) create mode 100644 mobile-app/lib/ui/views/learn/widgets/challenge_title.dart diff --git a/mobile-app/lib/ui/views/learn/block/block_template_view.dart b/mobile-app/lib/ui/views/learn/block/block_template_view.dart index d7006a63f..1e35715db 100644 --- a/mobile-app/lib/ui/views/learn/block/block_template_view.dart +++ b/mobile-app/lib/ui/views/learn/block/block_template_view.dart @@ -1,10 +1,6 @@ import 'package:flutter/material.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; import 'package:freecodecamp/ui/views/learn/block/block_template_viewmodel.dart'; -import 'package:freecodecamp/ui/views/learn/block/templates/dialogue/dialogue_view.dart'; -import 'package:freecodecamp/ui/views/learn/block/templates/grid/grid_view.dart'; -import 'package:freecodecamp/ui/views/learn/block/templates/link/link_view.dart'; -import 'package:freecodecamp/ui/views/learn/block/templates/list/list_view.dart'; import 'package:stacked/stacked.dart'; class BlockTemplateView extends StatelessWidget { @@ -71,37 +67,7 @@ class BlockTemplateView extends StatelessWidget { ), Row( children: [ - Builder( - builder: (BuildContext context) { - switch (block.layout) { - case BlockLayout.challengeGrid: - return BlockGridView( - block: block, - model: model, - ); - case BlockLayout.challengeDialogue: - return BlockDialogueView( - block: block, - model: model, - ); - case BlockLayout.challengeList: - return BlockListView( - block: block, - model: model, - ); - case BlockLayout.challengeLink: - return BlockLinkView( - block: block, - model: model, - ); - default: - return BlockGridView( - block: block, - model: model, - ); - } - }, - ), + model.getLayout(block.layout, model, block), ], ), ], diff --git a/mobile-app/lib/ui/views/learn/block/block_template_viewmodel.dart b/mobile-app/lib/ui/views/learn/block/block_template_viewmodel.dart index 2b64930d9..80be68b11 100644 --- a/mobile-app/lib/ui/views/learn/block/block_template_viewmodel.dart +++ b/mobile-app/lib/ui/views/learn/block/block_template_viewmodel.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:flutter/material.dart'; import 'package:freecodecamp/app/app.locator.dart'; import 'package:freecodecamp/app/app.router.dart'; import 'package:freecodecamp/models/learn/completed_challenge_model.dart'; @@ -8,6 +9,10 @@ import 'package:freecodecamp/service/authentication/authentication_service.dart' import 'package:freecodecamp/service/developer_service.dart'; import 'package:freecodecamp/service/learn/learn_offline_service.dart'; import 'package:freecodecamp/service/learn/learn_service.dart'; +import 'package:freecodecamp/ui/views/learn/block/templates/dialogue/dialogue_view.dart'; +import 'package:freecodecamp/ui/views/learn/block/templates/grid/grid_view.dart'; +import 'package:freecodecamp/ui/views/learn/block/templates/link/link_view.dart'; +import 'package:freecodecamp/ui/views/learn/block/templates/list/list_view.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; @@ -24,9 +29,6 @@ class BlockTemplateViewModel extends BaseViewModel { bool _isOpen = false; bool get isOpen => _isOpen; - final bool _isDownloadingSpecific = false; - bool get isDownloadingSpecific => _isDownloadingSpecific; - int _challengesCompleted = 0; int get challengesCompleted => _challengesCompleted; @@ -117,4 +119,38 @@ class BlockTemplateViewModel extends BaseViewModel { return false; } + + Widget getLayout( + BlockLayout layout, + BlockTemplateViewModel model, + Block block, + ) { + switch (layout) { + case BlockLayout.challengeGrid: + return BlockGridView( + block: block, + model: model, + ); + case BlockLayout.challengeDialogue: + return BlockDialogueView( + block: block, + model: model, + ); + case BlockLayout.challengeList: + return BlockListView( + block: block, + model: model, + ); + case BlockLayout.challengeLink: + return BlockLinkView( + block: block, + model: model, + ); + default: + return BlockGridView( + block: block, + model: model, + ); + } + } } diff --git a/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart b/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart index 360f4b79e..39b301a09 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart @@ -1,9 +1,11 @@ import 'dart:developer'; import 'package:flutter/material.dart'; +import 'package:flutter_scroll_shadow/flutter_scroll_shadow.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; import 'package:freecodecamp/ui/views/learn/block/block_template_viewmodel.dart'; import 'package:freecodecamp/ui/views/learn/block/templates/dialogue/dialogue_viewmodel.dart'; +import 'package:freecodecamp/ui/views/learn/widgets/challenge_title.dart'; import 'package:stacked/stacked.dart'; class BlockDialogueView extends StatelessWidget { @@ -98,24 +100,25 @@ class BlockDialogueView extends StatelessWidget { height: 200, child: Padding( padding: const EdgeInsets.all(8.0), - child: GridView.builder( - itemCount: structure[dialogueBlock].length, - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 6, - mainAxisExtent: 60, - mainAxisSpacing: 3, - crossAxisSpacing: 3, + child: ScrollShadow( + child: GridView.builder( + itemCount: structure[dialogueBlock].length, + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 6, + mainAxisSpacing: 3, + crossAxisSpacing: 3, + ), + itemBuilder: (context, task) { + return ChallengeTile( + block: block, + model: model, + challengeId: structure[dialogueBlock][task].id, + step: task + 1, + isDownloaded: false, + ); + }, ), - itemBuilder: (context, task) { - return ChallengeTile( - block: block, - model: model, - challengeId: structure[dialogueBlock][task].id, - step: task + 1, - isDownloaded: false, - ); - }, ), ), ) @@ -129,53 +132,3 @@ class BlockDialogueView extends StatelessWidget { ); } } - -class ChallengeTile extends StatelessWidget { - const ChallengeTile({ - Key? key, - required this.block, - required this.model, - required this.step, - required this.isDownloaded, - required this.challengeId, - }) : super(key: key); - - final Block block; - final BlockTemplateViewModel model; - final int step; - final bool isDownloaded; - final String challengeId; - - @override - Widget build(BuildContext context) { - bool isCompleted = model.completedChallenge(challengeId); - - return TextButton( - onPressed: () { - model.routeToChallengeView( - block, - challengeId, - ); - }, - style: TextButton.styleFrom( - backgroundColor: isCompleted - ? const Color.fromRGBO(0x00, 0x2e, 0xad, 0.3) - : const Color.fromRGBO(0x2a, 0x2a, 0x40, 1), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), - side: isCompleted - ? const BorderSide( - width: 1, - color: Color.fromRGBO(0xbc, 0xe8, 0xf1, 1), - ) - : const BorderSide( - color: Color.fromRGBO(0x3b, 0x3b, 0x4f, 1), - ), - ), - ), - child: Text( - step.toString(), - ), - ); - } -} diff --git a/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart b/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart index bb5f45fe7..c90b73376 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter_scroll_shadow/flutter_scroll_shadow.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; import 'package:freecodecamp/ui/views/learn/block/block_template_viewmodel.dart'; import 'package:freecodecamp/ui/views/learn/block/templates/grid/grid_viewmodel.dart'; +import 'package:freecodecamp/ui/views/learn/widgets/challenge_title.dart'; import 'package:stacked/stacked.dart'; class BlockGridView extends StatelessWidget { @@ -74,24 +76,25 @@ class BlockGridView extends StatelessWidget { padding: const EdgeInsets.all(8), height: 195, width: MediaQuery.of(context).size.width - 34, - child: GridView.builder( - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 6, - mainAxisExtent: 60, - mainAxisSpacing: 3, - crossAxisSpacing: 3, + child: ScrollShadow( + child: GridView.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 6, + mainAxisSpacing: 3, + crossAxisSpacing: 3, + ), + itemCount: block.challenges.length, + itemBuilder: (context, step) { + return ChallengeTile( + block: block, + model: model, + step: step + 1, + challengeId: block.challengeTiles[step].id, + isDownloaded: false, + ); + }, ), - itemCount: block.challenges.length, - itemBuilder: (context, step) { - return ChallengeTile( - block: block, - model: model, - step: step + 1, - challengeId: block.challengeTiles[step].id, - isDownloaded: false, - ); - }, ), ), ], @@ -101,53 +104,3 @@ class BlockGridView extends StatelessWidget { ); } } - -class ChallengeTile extends StatelessWidget { - const ChallengeTile({ - Key? key, - required this.block, - required this.model, - required this.step, - required this.isDownloaded, - required this.challengeId, - }) : super(key: key); - - final Block block; - final BlockTemplateViewModel model; - final int step; - final bool isDownloaded; - final String challengeId; - - @override - Widget build(BuildContext context) { - bool isCompleted = model.completedChallenge(challengeId); - - return TextButton( - onPressed: () { - model.routeToChallengeView( - block, - challengeId, - ); - }, - style: TextButton.styleFrom( - backgroundColor: isCompleted - ? const Color.fromRGBO(0x00, 0x2e, 0xad, 0.3) - : const Color.fromRGBO(0x2a, 0x2a, 0x40, 1), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), - side: isCompleted - ? const BorderSide( - width: 1, - color: Color.fromRGBO(0xbc, 0xe8, 0xf1, 1), - ) - : const BorderSide( - color: Color.fromRGBO(0x3b, 0x3b, 0x4f, 1), - ), - ), - ), - child: Text( - step.toString(), - ), - ); - } -} diff --git a/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart b/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart index 00215451a..2a408ecff 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart @@ -24,7 +24,7 @@ class BlockLinkView extends StatelessWidget { padding: const EdgeInsets.all(8.0), child: Column( children: [ - Text(block.description.join()), + Text(block.description.join(' ')), Row( children: [ Expanded( diff --git a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart index 7582693a1..e1594c9e5 100644 --- a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart +++ b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart @@ -84,10 +84,8 @@ class SuperBlockView extends StatelessWidget { child: Column( children: [ BlockTemplateView( - block: superBlock.blocks![i], - isOpen: superBlock.blocks!.length <= 3 || - superBlock.blocks![i].order == 0, - ) + block: superBlock.blocks![i], + isOpen: superBlock.blocks!.length <= 3) ], ), ), diff --git a/mobile-app/lib/ui/views/learn/widgets/challenge_title.dart b/mobile-app/lib/ui/views/learn/widgets/challenge_title.dart new file mode 100644 index 000000000..2c829f39c --- /dev/null +++ b/mobile-app/lib/ui/views/learn/widgets/challenge_title.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:freecodecamp/models/learn/curriculum_model.dart'; +import 'package:freecodecamp/ui/views/learn/block/block_template_viewmodel.dart'; + +class ChallengeTile extends StatelessWidget { + const ChallengeTile({ + Key? key, + required this.block, + required this.model, + required this.step, + required this.isDownloaded, + required this.challengeId, + }) : super(key: key); + + final Block block; + final BlockTemplateViewModel model; + final int step; + final bool isDownloaded; + final String challengeId; + + @override + Widget build(BuildContext context) { + bool isCompleted = model.completedChallenge(challengeId); + + return TextButton( + onPressed: () { + model.routeToChallengeView( + block, + challengeId, + ); + }, + style: TextButton.styleFrom( + backgroundColor: isCompleted + ? const Color.fromRGBO(0x00, 0x2e, 0xad, 0.3) + : const Color.fromRGBO(0x2a, 0x2a, 0x40, 1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + side: isCompleted + ? const BorderSide( + width: 1, + color: Color.fromRGBO(0xbc, 0xe8, 0xf1, 1), + ) + : const BorderSide( + color: Color.fromRGBO(0x3b, 0x3b, 0x4f, 1), + ), + ), + ), + child: Text( + step.toString(), + ), + ); + } +} From dc943241cc60be9eb4eb79dc3040848b7c87f99a Mon Sep 17 00:00:00 2001 From: sembauke Date: Tue, 8 Apr 2025 09:14:39 +0200 Subject: [PATCH 19/24] fix: correct import paths for challenge_tile and update link button color --- .../ui/views/learn/block/templates/dialogue/dialogue_view.dart | 2 +- .../lib/ui/views/learn/block/templates/grid/grid_view.dart | 2 +- .../lib/ui/views/learn/block/templates/link/link_view.dart | 2 +- .../learn/widgets/{challenge_title.dart => challenge_tile.dart} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename mobile-app/lib/ui/views/learn/widgets/{challenge_title.dart => challenge_tile.dart} (100%) diff --git a/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart b/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart index 39b301a09..7eb5e01d9 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart @@ -5,7 +5,7 @@ import 'package:flutter_scroll_shadow/flutter_scroll_shadow.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; import 'package:freecodecamp/ui/views/learn/block/block_template_viewmodel.dart'; import 'package:freecodecamp/ui/views/learn/block/templates/dialogue/dialogue_viewmodel.dart'; -import 'package:freecodecamp/ui/views/learn/widgets/challenge_title.dart'; +import 'package:freecodecamp/ui/views/learn/widgets/challenge_tile.dart'; import 'package:stacked/stacked.dart'; class BlockDialogueView extends StatelessWidget { diff --git a/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart b/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart index c90b73376..5f5b11fe1 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart @@ -3,7 +3,7 @@ import 'package:flutter_scroll_shadow/flutter_scroll_shadow.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; import 'package:freecodecamp/ui/views/learn/block/block_template_viewmodel.dart'; import 'package:freecodecamp/ui/views/learn/block/templates/grid/grid_viewmodel.dart'; -import 'package:freecodecamp/ui/views/learn/widgets/challenge_title.dart'; +import 'package:freecodecamp/ui/views/learn/widgets/challenge_tile.dart'; import 'package:stacked/stacked.dart'; class BlockGridView extends StatelessWidget { diff --git a/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart b/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart index 2a408ecff..698fd74fd 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart @@ -34,7 +34,7 @@ class BlockLinkView extends StatelessWidget { }, style: TextButton.styleFrom( backgroundColor: - const Color.fromRGBO(0x5a, 0x01, 0xa7, 1), + const Color.fromRGBO(0x99, 0xc9, 0xff, 1), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), ), diff --git a/mobile-app/lib/ui/views/learn/widgets/challenge_title.dart b/mobile-app/lib/ui/views/learn/widgets/challenge_tile.dart similarity index 100% rename from mobile-app/lib/ui/views/learn/widgets/challenge_title.dart rename to mobile-app/lib/ui/views/learn/widgets/challenge_tile.dart From db9855ce9d708d9046e651dd08989b2d47ca2f14 Mon Sep 17 00:00:00 2001 From: sembauke Date: Tue, 8 Apr 2025 09:20:13 +0200 Subject: [PATCH 20/24] fix: update button background color in BlockLinkView --- .../lib/ui/views/learn/block/templates/link/link_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart b/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart index 698fd74fd..8aa3aa1f7 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart @@ -34,7 +34,7 @@ class BlockLinkView extends StatelessWidget { }, style: TextButton.styleFrom( backgroundColor: - const Color.fromRGBO(0x99, 0xc9, 0xff, 1), + const Color.fromRGBO(0x19, 0x8e, 0xee, 1), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), ), From 449a6858cab01a8330f4c4c23d5c234d8e4a6aba Mon Sep 17 00:00:00 2001 From: sembauke Date: Tue, 8 Apr 2025 11:11:57 +0200 Subject: [PATCH 21/24] fix: add isOpen functionality to block templates and manage open states --- .../learn/block/block_template_view.dart | 11 +- .../learn/block/block_template_viewmodel.dart | 36 ++-- .../templates/dialogue/dialogue_view.dart | 156 +++++++++++------- .../learn/block/templates/grid/grid_view.dart | 20 ++- .../learn/block/templates/list/list_view.dart | 10 +- .../learn/superblock/superblock_view.dart | 72 +++++--- .../superblock/superblock_viewmodel.dart | 8 + 7 files changed, 194 insertions(+), 119 deletions(-) diff --git a/mobile-app/lib/ui/views/learn/block/block_template_view.dart b/mobile-app/lib/ui/views/learn/block/block_template_view.dart index 1e35715db..ffa9dd9db 100644 --- a/mobile-app/lib/ui/views/learn/block/block_template_view.dart +++ b/mobile-app/lib/ui/views/learn/block/block_template_view.dart @@ -6,11 +6,13 @@ import 'package:stacked/stacked.dart'; class BlockTemplateView extends StatelessWidget { final Block block; final bool isOpen; + final Function isOpenFunction; const BlockTemplateView({ Key? key, required this.block, required this.isOpen, + required this.isOpenFunction, }) : super(key: key); @override @@ -19,7 +21,6 @@ class BlockTemplateView extends StatelessWidget { onViewModelReady: (model) async { model.init(block.challengeTiles); model.setIsDev = await model.developerService.developmentMode(); - model.setIsOpen = isOpen; }, viewModelBuilder: () => BlockTemplateViewModel(), builder: ( @@ -67,7 +68,13 @@ class BlockTemplateView extends StatelessWidget { ), Row( children: [ - model.getLayout(block.layout, model, block), + model.getLayout( + block.layout, + model, + block, + isOpen, + isOpenFunction, + ), ], ), ], diff --git a/mobile-app/lib/ui/views/learn/block/block_template_viewmodel.dart b/mobile-app/lib/ui/views/learn/block/block_template_viewmodel.dart index 80be68b11..75575af18 100644 --- a/mobile-app/lib/ui/views/learn/block/block_template_viewmodel.dart +++ b/mobile-app/lib/ui/views/learn/block/block_template_viewmodel.dart @@ -13,7 +13,6 @@ import 'package:freecodecamp/ui/views/learn/block/templates/dialogue/dialogue_vi import 'package:freecodecamp/ui/views/learn/block/templates/grid/grid_view.dart'; import 'package:freecodecamp/ui/views/learn/block/templates/link/link_view.dart'; import 'package:freecodecamp/ui/views/learn/block/templates/list/list_view.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; @@ -26,9 +25,6 @@ class BlockTemplateViewModel extends BaseViewModel { bool _isDev = false; bool get isDev => _isDev; - bool _isOpen = false; - bool get isOpen => _isOpen; - int _challengesCompleted = 0; int get challengesCompleted => _challengesCompleted; @@ -38,33 +34,11 @@ class BlockTemplateViewModel extends BaseViewModel { final learnService = locator(); - set setIsOpen(bool widgetIsOpened) { - _isOpen = widgetIsOpened; - notifyListeners(); - } - set setIsDev(bool value) { _isDev = value; notifyListeners(); } - void initBlockState(String blockName) async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - - if (!prefs.containsKey(blockName)) { - prefs.setBool(blockName, true); - } - - setBlockOpenState(blockName, prefs.getBool(blockName) ?? false); - } - - void setBlockOpenState(String blockName, bool value) async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.setBool(blockName, !value); - _isOpen = !value; - notifyListeners(); - } - void routeToChallengeView(Block block, String challengeId) { _navigationService.navigateTo( Routes.challengeTemplateView, @@ -124,22 +98,30 @@ class BlockTemplateViewModel extends BaseViewModel { BlockLayout layout, BlockTemplateViewModel model, Block block, + bool isOpen, + Function isOpenFunction, ) { switch (layout) { case BlockLayout.challengeGrid: return BlockGridView( block: block, model: model, + isOpen: isOpen, + isOpenFunction: isOpenFunction, ); case BlockLayout.challengeDialogue: return BlockDialogueView( block: block, model: model, + isOpen: isOpen, + isOpenFunction: isOpenFunction, ); case BlockLayout.challengeList: return BlockListView( block: block, model: model, + isOpen: isOpen, + isOpenFunction: isOpenFunction, ); case BlockLayout.challengeLink: return BlockLinkView( @@ -150,6 +132,8 @@ class BlockTemplateViewModel extends BaseViewModel { return BlockGridView( block: block, model: model, + isOpen: isOpen, + isOpenFunction: isOpenFunction, ); } } diff --git a/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart b/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart index 7eb5e01d9..6751ef58c 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; import 'package:flutter_scroll_shadow/flutter_scroll_shadow.dart'; import 'package:freecodecamp/models/learn/curriculum_model.dart'; @@ -9,14 +7,18 @@ import 'package:freecodecamp/ui/views/learn/widgets/challenge_tile.dart'; import 'package:stacked/stacked.dart'; class BlockDialogueView extends StatelessWidget { - const BlockDialogueView({ - Key? key, - required this.block, - required this.model, - }) : super(key: key); + const BlockDialogueView( + {Key? key, + required this.block, + required this.model, + required this.isOpen, + required this.isOpenFunction}) + : super(key: key); final Block block; final BlockTemplateViewModel model; + final bool isOpen; + final Function isOpenFunction; @override Widget build(BuildContext context) { @@ -38,12 +40,35 @@ class BlockDialogueView extends StatelessWidget { } } - log(structure.toString()); - return ViewModelBuilder.reactive( viewModelBuilder: () => BlockDialogueViewModel(), builder: (context, childModel, child) => Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row( + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: TextButton( + onPressed: () { + isOpenFunction(); + }, + style: TextButton.styleFrom( + backgroundColor: const Color.fromRGBO(0x1b, 0x1b, 0x32, 1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + side: const BorderSide( + color: Color.fromRGBO(0x3b, 0x3b, 0x4f, 1), + ), + ), + ), + child: Text( + isOpen ? 'Hide Tasks' : 'Show Tasks', + ), + ), + ), + ], + ), SizedBox( width: MediaQuery.of(context).size.width - 34, child: ListView.builder( @@ -56,37 +81,48 @@ class BlockDialogueView extends StatelessWidget { Row( children: [ Expanded( - child: InkWell( - onTap: () => model.routeToChallengeView( - block, - dialogueHeaders[dialogueBlock].id, + child: Padding( + padding: EdgeInsets.only( + bottom: isOpen ? 0 : 8, + left: 8, + right: 8, ), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - color: model.completedChallenge( - dialogueHeaders[dialogueBlock].id, - ) - ? const Color.fromRGBO( - 0x00, 0x2e, 0xad, 0.3) - : const Color.fromRGBO(0x2a, 0x2a, 0x40, 1), - border: Border.all( + child: InkWell( + onTap: () => model.routeToChallengeView( + block, + dialogueHeaders[dialogueBlock].id, + ), + child: Container( + constraints: const BoxConstraints( + minHeight: 75, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), color: model.completedChallenge( - dialogueHeaders[dialogueBlock].id) + dialogueHeaders[dialogueBlock].id, + ) ? const Color.fromRGBO( - 0xbc, 0xe8, 0xf1, 1) + 0x00, 0x2e, 0xad, 0.3) : const Color.fromRGBO( - 0x3b, 0x3b, 0x4f, 1), - width: 1, + 0x2a, 0x2a, 0x40, 1), + border: Border.all( + color: model.completedChallenge( + dialogueHeaders[dialogueBlock].id) + ? const Color.fromRGBO( + 0xbc, 0xe8, 0xf1, 1) + : const Color.fromRGBO( + 0x3b, 0x3b, 0x4f, 1), + width: 1, + ), ), - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - dialogueHeaders[dialogueBlock].title, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + dialogueHeaders[dialogueBlock].title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), ), ), ), @@ -95,33 +131,35 @@ class BlockDialogueView extends StatelessWidget { ), ], ), - SizedBox( - width: MediaQuery.of(context).size.width - 34, - height: 200, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: ScrollShadow( - child: GridView.builder( - itemCount: structure[dialogueBlock].length, - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 6, - mainAxisSpacing: 3, - crossAxisSpacing: 3, + if (isOpen) + SizedBox( + width: MediaQuery.of(context).size.width - 34, + height: 200, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: ScrollShadow( + child: GridView.builder( + itemCount: structure[dialogueBlock].length, + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 6, + mainAxisSpacing: 3, + crossAxisSpacing: 3, + ), + itemBuilder: (context, task) { + return ChallengeTile( + block: block, + model: model, + challengeId: + structure[dialogueBlock][task].id, + step: task + 1, + isDownloaded: false, + ); + }, ), - itemBuilder: (context, task) { - return ChallengeTile( - block: block, - model: model, - challengeId: structure[dialogueBlock][task].id, - step: task + 1, - isDownloaded: false, - ); - }, ), ), - ), - ) + ) ], ); }, diff --git a/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart b/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart index 5f5b11fe1..7c07c90c7 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart @@ -7,14 +7,18 @@ import 'package:freecodecamp/ui/views/learn/widgets/challenge_tile.dart'; import 'package:stacked/stacked.dart'; class BlockGridView extends StatelessWidget { - const BlockGridView({ - Key? key, - required this.block, - required this.model, - }) : super(key: key); + const BlockGridView( + {Key? key, + required this.block, + required this.model, + required this.isOpen, + required this.isOpenFunction}) + : super(key: key); final Block block; final BlockTemplateViewModel model; + final bool isOpen; + final Function isOpenFunction; @override Widget build(BuildContext context) { @@ -51,7 +55,7 @@ class BlockGridView extends StatelessWidget { padding: const EdgeInsets.all(8), child: TextButton( onPressed: () { - model.setIsOpen = !model.isOpen; + isOpenFunction(); }, style: TextButton.styleFrom( backgroundColor: const Color.fromRGBO(0x1b, 0x1b, 0x32, 1), @@ -63,13 +67,13 @@ class BlockGridView extends StatelessWidget { ), ), child: Text( - model.isOpen ? 'Hide Steps' : 'Show Steps', + isOpen ? 'Hide Steps' : 'Show Steps', ), ), ), ], ), - if (model.isOpen) + if (isOpen) Row( children: [ Container( diff --git a/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart b/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart index 3eb4e1f67..9d4dc0771 100644 --- a/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart +++ b/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart @@ -9,10 +9,14 @@ class BlockListView extends StatelessWidget { Key? key, required this.block, required this.model, + required this.isOpen, + required this.isOpenFunction, }) : super(key: key); final Block block; final BlockTemplateViewModel model; + final bool isOpen; + final Function isOpenFunction; @override Widget build(BuildContext context) { @@ -45,7 +49,7 @@ class BlockListView extends StatelessWidget { padding: const EdgeInsets.all(8), child: TextButton( onPressed: () { - model.setIsOpen = !model.isOpen; + isOpenFunction(); }, style: TextButton.styleFrom( backgroundColor: const Color.fromRGBO(0x1b, 0x1b, 0x32, 1), @@ -57,13 +61,13 @@ class BlockListView extends StatelessWidget { ), ), child: Text( - model.isOpen ? 'Hide' : 'Show', + isOpen ? 'Hide' : 'Show', ), ), ), ], ), - if (model.isOpen) + if (isOpen) Row( children: [ Container( diff --git a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart index e1594c9e5..edd24ba8f 100644 --- a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart +++ b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart @@ -40,6 +40,19 @@ class SuperBlockView extends StatelessWidget { if (snapshot.hasData) { if (snapshot.data is SuperBlock) { SuperBlock superBlock = snapshot.data as SuperBlock; + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (model.blockOpenStates.isEmpty) { + Map openStates = { + if (superBlock.blocks != null) + for (var block in superBlock.blocks!) + block.dashedName: false + }; + + model.blockOpenStates = openStates; + } + }); + return blockTemplate(model, superBlock); } } @@ -68,27 +81,44 @@ class SuperBlockView extends StatelessWidget { notification.disallowIndicator(); return true; }, - child: ListView.separated( - separatorBuilder: (context, int i) => const Divider( - height: 0, - color: Colors.transparent, - ), - shrinkWrap: true, - itemCount: superBlock.blocks!.length, - physics: const ClampingScrollPhysics(), - itemBuilder: (context, i) => Padding( - padding: model.getPaddingBeginAndEnd( - i, - superBlock.blocks![i].challenges.length, - ), - child: Column( - children: [ - BlockTemplateView( - block: superBlock.blocks![i], - isOpen: superBlock.blocks!.length <= 3) - ], - ), - ), + child: CustomScrollView( + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (context, block) { + return Padding( + padding: model.getPaddingBeginAndEnd( + block, + superBlock.blocks![block].challenges.length, + ), + child: Column( + children: [ + BlockTemplateView( + key: ValueKey(block), + block: superBlock.blocks![block], + isOpen: model.blockOpenStates[ + superBlock.blocks![block].dashedName] ?? + false, + isOpenFunction: () { + Map local = model.blockOpenStates; + Block curr = superBlock.blocks![block]; + + if (local[curr.dashedName] != null) { + local[curr.dashedName] = !local[curr.dashedName]!; + } + + model.blockOpenStates = local; + }, + ) + ], + ), + ); + }, + childCount: superBlock.blocks!.length, + addAutomaticKeepAlives: true, + ), + ) + ], ), ), ); diff --git a/mobile-app/lib/ui/views/learn/superblock/superblock_viewmodel.dart b/mobile-app/lib/ui/views/learn/superblock/superblock_viewmodel.dart index bf416fdf0..d022b0374 100644 --- a/mobile-app/lib/ui/views/learn/superblock/superblock_viewmodel.dart +++ b/mobile-app/lib/ui/views/learn/superblock/superblock_viewmodel.dart @@ -20,6 +20,14 @@ class SuperBlockViewModel extends BaseViewModel { final _dio = DioService.dio; + Map _blockOpenStates = {}; + Map get blockOpenStates => _blockOpenStates; + + set blockOpenStates(Map openStates) { + _blockOpenStates = openStates; + notifyListeners(); + } + EdgeInsets getPaddingBeginAndEnd(int index, int challenges) { if (index == 0) { return const EdgeInsets.only(top: 16); From 752ab82a583cec5463b4032953d330d071223662 Mon Sep 17 00:00:00 2001 From: sembauke Date: Tue, 8 Apr 2025 12:39:54 +0200 Subject: [PATCH 22/24] fix: stop calling future builder on state update --- .../learn/superblock/superblock_view.dart | 18 ++++++++++-------- .../learn/superblock/superblock_viewmodel.dart | 8 ++++++++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart index edd24ba8f..fbbbaaead 100644 --- a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart +++ b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart @@ -22,20 +22,22 @@ class SuperBlockView extends StatelessWidget { Widget build(BuildContext context) { return ViewModelBuilder.reactive( viewModelBuilder: () => SuperBlockViewModel(), - onViewModelReady: (model) => AuthenticationService.staticIsloggedIn - ? model.auth.fetchUser() - : null, + onViewModelReady: (model) => { + AuthenticationService.staticIsloggedIn ? model.auth.fetchUser() : null, + model.setSuperBlockData = model.getSuperBlockData( + superBlockDashedName, + superBlockName, + hasInternet, + ) + }, builder: (context, model, child) => Scaffold( appBar: AppBar( title: Text(superBlockName), ), backgroundColor: const Color.fromRGBO(0x0a, 0x0a, 0x23, 1), body: FutureBuilder( - future: model.getSuperBlockData( - superBlockDashedName, - superBlockName, - hasInternet, - ), + initialData: null, + future: model.superBlockData, builder: ((context, snapshot) { if (snapshot.hasData) { if (snapshot.data is SuperBlock) { diff --git a/mobile-app/lib/ui/views/learn/superblock/superblock_viewmodel.dart b/mobile-app/lib/ui/views/learn/superblock/superblock_viewmodel.dart index d022b0374..bdaf21404 100644 --- a/mobile-app/lib/ui/views/learn/superblock/superblock_viewmodel.dart +++ b/mobile-app/lib/ui/views/learn/superblock/superblock_viewmodel.dart @@ -23,11 +23,19 @@ class SuperBlockViewModel extends BaseViewModel { Map _blockOpenStates = {}; Map get blockOpenStates => _blockOpenStates; + Future? _superBlockData; + Future? get superBlockData => _superBlockData; + set blockOpenStates(Map openStates) { _blockOpenStates = openStates; notifyListeners(); } + set setSuperBlockData(Future? superBlockData) { + _superBlockData = superBlockData; + notifyListeners(); + } + EdgeInsets getPaddingBeginAndEnd(int index, int challenges) { if (index == 0) { return const EdgeInsets.only(top: 16); From 9e1789b3b8d70990ff29f3c96661157e8dd3733f Mon Sep 17 00:00:00 2001 From: sembauke Date: Tue, 8 Apr 2025 15:26:21 +0200 Subject: [PATCH 23/24] fix: set the first block to open in block open states --- .../lib/ui/views/learn/superblock/superblock_view.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart index fbbbaaead..9fb376a57 100644 --- a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart +++ b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart @@ -51,6 +51,11 @@ class SuperBlockView extends StatelessWidget { block.dashedName: false }; + // Set first block open + String firstBlockKey = openStates.entries.toList()[0].key; + + openStates[firstBlockKey] = true; + model.blockOpenStates = openStates; } }); From f3aae5582f26c87ebe27f98033aabef546bceddd Mon Sep 17 00:00:00 2001 From: sembauke Date: Tue, 8 Apr 2025 15:48:09 +0200 Subject: [PATCH 24/24] fix: refactor block rendering to use ListView and streamline open/close state management --- .../learn/superblock/superblock_view.dart | 57 +++++++------------ .../superblock/superblock_viewmodel.dart | 11 ++++ 2 files changed, 33 insertions(+), 35 deletions(-) diff --git a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart index 9fb376a57..accf07ab8 100644 --- a/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart +++ b/mobile-app/lib/ui/views/learn/superblock/superblock_view.dart @@ -88,44 +88,31 @@ class SuperBlockView extends StatelessWidget { notification.disallowIndicator(); return true; }, - child: CustomScrollView( - slivers: [ - SliverList( - delegate: SliverChildBuilderDelegate( - (context, block) { - return Padding( - padding: model.getPaddingBeginAndEnd( + child: ListView.builder( + itemCount: superBlock.blocks!.length, + itemBuilder: (context, block) { + return Padding( + padding: model.getPaddingBeginAndEnd( + block, + superBlock.blocks![block].challenges.length, + ), + child: Column( + children: [ + BlockTemplateView( + key: ValueKey(block), + block: superBlock.blocks![block], + isOpen: model.blockOpenStates[ + superBlock.blocks![block].dashedName] ?? + false, + isOpenFunction: () => model.setBlockOpenClosedState( + superBlock, block, - superBlock.blocks![block].challenges.length, ), - child: Column( - children: [ - BlockTemplateView( - key: ValueKey(block), - block: superBlock.blocks![block], - isOpen: model.blockOpenStates[ - superBlock.blocks![block].dashedName] ?? - false, - isOpenFunction: () { - Map local = model.blockOpenStates; - Block curr = superBlock.blocks![block]; - - if (local[curr.dashedName] != null) { - local[curr.dashedName] = !local[curr.dashedName]!; - } - - model.blockOpenStates = local; - }, - ) - ], - ), - ); - }, - childCount: superBlock.blocks!.length, - addAutomaticKeepAlives: true, + ) + ], ), - ) - ], + ); + }, ), ), ); diff --git a/mobile-app/lib/ui/views/learn/superblock/superblock_viewmodel.dart b/mobile-app/lib/ui/views/learn/superblock/superblock_viewmodel.dart index bdaf21404..dadab5818 100644 --- a/mobile-app/lib/ui/views/learn/superblock/superblock_viewmodel.dart +++ b/mobile-app/lib/ui/views/learn/superblock/superblock_viewmodel.dart @@ -46,6 +46,17 @@ class SuperBlockViewModel extends BaseViewModel { } } + setBlockOpenClosedState(SuperBlock superBlock, int block) { + Map local = blockOpenStates; + Block curr = superBlock.blocks![block]; + + if (local[curr.dashedName] != null) { + local[curr.dashedName] = !local[curr.dashedName]!; + } + + blockOpenStates = local; + } + Future getSuperBlockData( String dashedName, String name,