diff --git a/mobile-app/lib/models/learn/curriculum_model.dart b/mobile-app/lib/models/learn/curriculum_model.dart index 3f38e6e46..32cdaad8c 100644 --- a/mobile-app/lib/models/learn/curriculum_model.dart +++ b/mobile-app/lib/models/learn/curriculum_model.dart @@ -46,12 +46,23 @@ class SuperBlock { } } +enum BlockType { lecture, workshop, lab, review, quiz, exam, legacy } + +enum BlockLayout { + challengeList, + challengeGrid, + challengeDialogue, + challengeLink, + project, +} + 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; @@ -59,25 +70,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, @@ -90,18 +92,36 @@ class Block { data['challengeTiles'] = []; + BlockLayout handleLayout(String? layout) { + switch (layout) { + case 'project-list': + 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; + 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( @@ -125,22 +145,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_template_view.dart b/mobile-app/lib/ui/views/learn/block/block_template_view.dart new file mode 100644 index 000000000..ffa9dd9db --- /dev/null +++ b/mobile-app/lib/ui/views/learn/block/block_template_view.dart @@ -0,0 +1,90 @@ +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: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 + Widget build(BuildContext context) { + return ViewModelBuilder.reactive( + onViewModelReady: (model) async { + model.init(block.challengeTiles); + model.setIsDev = await model.developerService.developmentMode(); + }, + viewModelBuilder: () => BlockTemplateViewModel(), + builder: ( + context, + model, + child, + ) { + 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: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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: [ + 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 new file mode 100644 index 000000000..75575af18 --- /dev/null +++ b/mobile-app/lib/ui/views/learn/block/block_template_viewmodel.dart @@ -0,0 +1,140 @@ +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'; +import 'package:freecodecamp/models/learn/curriculum_model.dart'; +import 'package:freecodecamp/models/main/user_model.dart'; +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:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; + +class BlockTemplateViewModel extends BaseViewModel { + final NavigationService _navigationService = locator(); + final AuthenticationService _auth = locator(); + + FccUserModel? user; + + bool _isDev = false; + bool get isDev => _isDev; + + int _challengesCompleted = 0; + int get challengesCompleted => _challengesCompleted; + + final learnOfflineService = locator(); + + final developerService = locator(); + + final learnService = locator(); + + set setIsDev(bool value) { + _isDev = value; + notifyListeners(); + } + + void routeToChallengeView(Block block, String challengeId) { + _navigationService.navigateTo( + Routes.challengeTemplateView, + arguments: ChallengeTemplateViewArguments( + challengeId: challengeId, + block: block, + challengesCompleted: _challengesCompleted, + ), + ); + } + + Future routeToCertification(Block block) async { + String challengeId = block.challengeTiles[0].id; + + routeToChallengeView( + block, + challengeId, + ); + } + + void init(List challengeBatch) async { + user = await _auth.userModel; + setNumberOfCompletedChallenges(challengeBatch); + notifyListeners(); + } + + void setNumberOfCompletedChallenges(List challengeBatch) { + int count = 0; + if (user != null) { + Iterable completedChallengeIds = user!.completedChallenges.map( + (e) => e.id, + ); + + for (ChallengeListTile challenge in challengeBatch) { + if (completedChallengeIds.contains(challenge.id)) { + count++; + } + } + _challengesCompleted = count; + notifyListeners(); + } + } + + bool completedChallenge(String incomingId) { + if (user != null) { + for (CompletedChallenge challenge in user!.completedChallenges) { + if (challenge.id == incomingId) { + return true; + } + } + } + + return false; + } + + Widget getLayout( + 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( + block: block, + model: model, + ); + default: + return BlockGridView( + block: block, + model: model, + isOpen: isOpen, + isOpenFunction: isOpenFunction, + ); + } + } +} diff --git a/mobile-app/lib/ui/views/learn/block/block_view.dart b/mobile-app/lib/ui/views/learn/block/block_view.dart deleted file mode 100644 index 9fcd8cb21..000000000 --- a/mobile-app/lib/ui/views/learn/block/block_view.dart +++ /dev/null @@ -1,397 +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'; -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'; - -class BlockView extends StatelessWidget { - final Block block; - final bool isOpen; - final bool isStepBased; - - const BlockView({ - Key? key, - required this.block, - required this.isOpen, - required this.isStepBased, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - 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(), - builder: ( - context, - 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, - ), - 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( - fontSize: 18, - fontWeight: FontWeight.w600, - height: 1.2, - fontFamily: 'Lato', - color: Colors.white.withValues(alpha: 0.87), - ), - ), - ), - if (model.isDev && !isCert) - DownloadButton( - model: model, - block: block, - ), - 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, - ) - ], - ), - ), - ), - ], - ); - }, - ); - } - - 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, - ), - ); - }, - ), - ), - ], - ); - }) - ], - ); - } - - 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: [ - 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 { - 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 BlockViewModel model; - final int step; - final bool isDowloaded; - final String challengeId; - - @override - 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, - ), - ), - ), - ), - ), - ); - } -} diff --git a/mobile-app/lib/ui/views/learn/block/block_viewmodel.dart b/mobile-app/lib/ui/views/learn/block/block_viewmodel.dart deleted file mode 100644 index 710b8f8bc..000000000 --- a/mobile-app/lib/ui/views/learn/block/block_viewmodel.dart +++ /dev/null @@ -1,231 +0,0 @@ -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'; -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:shared_preferences/shared_preferences.dart'; -import 'package:stacked/stacked.dart'; -import 'package:stacked_services/stacked_services.dart'; - -class BlockViewModel extends BaseViewModel { - final NavigationService _navigationService = locator(); - final AuthenticationService _auth = locator(); - - FccUserModel? user; - - bool _isDev = false; - bool get isDev => _isDev; - - 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; - - int _challengesCompleted = 0; - int get challengesCompleted => _challengesCompleted; - - final learnOfflineService = locator(); - - final developerService = locator(); - - final learnService = locator(); - - set setIsOpen(bool widgetIsOpened) { - _isOpen = widgetIsOpened; - notifyListeners(); - } - - set setIsDownloading(bool value) { - _isDownloading = value; - notifyListeners(); - } - - set setIsDownloaded(bool value) { - _isDownloaded = value; - 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, - arguments: ChallengeTemplateViewArguments( - challengeId: challengeId, - block: block, - challengesCompleted: _challengesCompleted, - ), - ); - } - - Future routeToCertification(Block block) async { - String challengeId = block.challengeTiles[0].id; - - routeToChallengeView( - block, - challengeId, - ); - } - - void init(List challengeBatch) async { - 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) { - int count = 0; - if (user != null) { - Iterable completedChallengeIds = user!.completedChallenges.map( - (e) => e.id, - ); - - for (ChallengeListTile challenge in challengeBatch) { - if (completedChallengeIds.contains(challenge.id)) { - count++; - } - } - _challengesCompleted = count; - notifyListeners(); - } - } - - bool completedChallenge(String incomingId) { - if (user != null) { - for (CompletedChallenge challenge in user!.completedChallenges) { - if (challenge.id == incomingId) { - return true; - } - } - } - - return false; - } - - Icon getIcon(bool completed) { - if (completed) { - return const Icon(Icons.check_circle); - } - 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); - } -} 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..6751ef58c --- /dev/null +++ b/mobile-app/lib/ui/views/learn/block/templates/dialogue/dialogue_view.dart @@ -0,0 +1,172 @@ +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_tile.dart'; +import 'package:stacked/stacked.dart'; + +class BlockDialogueView extends StatelessWidget { + 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) { + 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]); + } + } + + 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( + physics: const ClampingScrollPhysics(), + shrinkWrap: true, + itemCount: structure.length, + itemBuilder: (context, dialogueBlock) { + return Column( + children: [ + Row( + children: [ + Expanded( + child: Padding( + padding: EdgeInsets.only( + bottom: isOpen ? 0 : 8, + left: 8, + right: 8, + ), + 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, + ) + ? 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, + ), + ), + ), + ), + ), + ), + ), + ], + ), + 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, + ); + }, + ), + ), + ), + ) + ], + ); + }, + ), + ), + ], + ), + ); + } +} 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 {} 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..7c07c90c7 --- /dev/null +++ b/mobile-app/lib/ui/views/learn/block/templates/grid/grid_view.dart @@ -0,0 +1,110 @@ +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_tile.dart'; +import 'package:stacked/stacked.dart'; + +class BlockGridView extends StatelessWidget { + 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) { + // 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, childModel, child) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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.9, + 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.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 Steps' : 'Show Steps', + ), + ), + ), + ], + ), + if (isOpen) + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + height: 195, + width: MediaQuery.of(context).size.width - 34, + 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, + ); + }, + ), + ), + ), + ], + ), + ], + ), + ); + } +} 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..f2ece42f2 --- /dev/null +++ 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..8aa3aa1f7 --- /dev/null +++ b/mobile-app/lib/ui/views/learn/block/templates/link/link_view.dart @@ -0,0 +1,57 @@ +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/link/link_viewmodel.dart'; +import 'package:stacked/stacked.dart'; + +class BlockLinkView extends StatelessWidget { + 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, childModel, child) { + return Expanded( + 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(0x19, 0x8e, 0xee, 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/link/link_viewmodel.dart b/mobile-app/lib/ui/views/learn/block/templates/link/link_viewmodel.dart new file mode 100644 index 000000000..d89e88b48 --- /dev/null +++ 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 new file mode 100644 index 000000000..9d4dc0771 --- /dev/null +++ b/mobile-app/lib/ui/views/learn/block/templates/list/list_view.dart @@ -0,0 +1,121 @@ +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'; +import 'package:stacked/stacked.dart'; + +class BlockListView extends StatelessWidget { + const BlockListView({ + 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) { + double progress = model.challengesCompleted == 0 + ? 0.01 + : model.challengesCompleted / block.challenges.length; + return ViewModelBuilder.reactive( + viewModelBuilder: () => BlockGridViewModel(), + builder: (context, childModel, child) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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.9, + 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.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' : 'Show', + ), + ), + ), + ], + ), + if (isOpen) + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + width: MediaQuery.of(context).size.width - 34, + 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/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..663792ee9 --- /dev/null +++ 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/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, 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..accf07ab8 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'; @@ -22,23 +22,44 @@ 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) { 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 + }; + + // Set first block open + String firstBlockKey = openStates.entries.toList()[0].key; + + openStates[firstBlockKey] = true; + + model.blockOpenStates = openStates; + } + }); + return blockTemplate(model, superBlock); } } @@ -67,40 +88,31 @@ class SuperBlockView extends StatelessWidget { notification.disallowIndicator(); return true; }, - child: ListView.separated( - separatorBuilder: (context, int i) => Divider( - height: model.getPaddingBetweenBlocks(superBlock.blocks![i]), - color: Colors.transparent, - ), - shrinkWrap: true, + child: ListView.builder( itemCount: superBlock.blocks!.length, - physics: const ClampingScrollPhysics(), - itemBuilder: (context, i) => Padding( - padding: model.getPaddingBeginAndEnd( - i, - superBlock.blocks![i].challenges.length, - ), - 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(); - }, - ) - ], - ), - ), + 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, + ), + ) + ], + ), + ); + }, ), ), ); 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..dadab5818 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 { @@ -21,16 +20,20 @@ class SuperBlockViewModel extends BaseViewModel { final _dio = DioService.dio; - double getPaddingBetweenBlocks(Block block) { - if (block.isStepBased) { - return 3.0; - } + Map _blockOpenStates = {}; + Map get blockOpenStates => _blockOpenStates; - if (block.dashedName == 'es6') { - return 0; - } + Future? _superBlockData; + Future? get superBlockData => _superBlockData; + + set blockOpenStates(Map openStates) { + _blockOpenStates = openStates; + notifyListeners(); + } - return 50.0; + set setSuperBlockData(Future? superBlockData) { + _superBlockData = superBlockData; + notifyListeners(); } EdgeInsets getPaddingBeginAndEnd(int index, int challenges) { @@ -43,10 +46,15 @@ class SuperBlockViewModel extends BaseViewModel { } } - Future getBlockOpenState(Block block) async { - SharedPreferences prefs = await SharedPreferences.getInstance(); + setBlockOpenClosedState(SuperBlock superBlock, int block) { + Map local = blockOpenStates; + Block curr = superBlock.blocks![block]; + + if (local[curr.dashedName] != null) { + local[curr.dashedName] = !local[curr.dashedName]!; + } - return prefs.getBool(block.name) ?? block.order == 0; + blockOpenStates = local; } Future getSuperBlockData( diff --git a/mobile-app/lib/ui/views/learn/widgets/challenge_tile.dart b/mobile-app/lib/ui/views/learn/widgets/challenge_tile.dart new file mode 100644 index 000000000..2c829f39c --- /dev/null +++ b/mobile-app/lib/ui/views/learn/widgets/challenge_tile.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(), + ), + ); + } +} 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/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, - ); - }, - ), - ], - ); - } -} 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, - ), - ), - ) - ], - ), - ), - ); - } -}