diff --git a/mobile-app/lib/ui/views/learn/challenge/challenge_view.dart b/mobile-app/lib/ui/views/learn/challenge/challenge_view.dart index 9b8058f5f..0420d7a66 100644 --- a/mobile-app/lib/ui/views/learn/challenge/challenge_view.dart +++ b/mobile-app/lib/ui/views/learn/challenge/challenge_view.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:freecodecamp/enums/panel_type.dart'; import 'package:freecodecamp/extensions/i18n_extension.dart'; @@ -96,26 +95,13 @@ class ChallengeView extends StatelessWidget { ); model.initiateFile(editor, challenge, currFile, editableRegion); + model.listenToFocusedController(editor); + model.listenToSymbolBarScrollController(); + + if (model.showPanel) { + FocusManager.instance.primaryFocus?.unfocus(); + } - SchedulerBinding.instance.addPostFrameCallback((timeStamp) { - if (keyboard && !model.showPanel) { - if (model.hideAppBar) { - model.setHideAppBar = false; - } - } else if (keyboard && model.showPanel) { - if (model.hideAppBar) { - model.setHideAppBar = false; - } - } else if (!keyboard && model.showPanel) { - if (!model.hideAppBar) { - model.setHideAppBar = true; - } - } else { - if (model.hideAppBar) { - model.setHideAppBar = false; - } - } - }); editor.onTextChange.stream.listen((text) { model.fileService.saveFileInCache( challenge, @@ -145,84 +131,84 @@ class ChallengeView extends StatelessWidget { model.learnService.updateProgressOnPop(context, block); }, child: Scaffold( - appBar: !model.hideAppBar - ? AppBar( - automaticallyImplyLeading: !model.showPreview, - title: challenge.files.length == 1 && - !model.showPreview - ? Text(context.t.editor) - : Row( - children: [ - if (model.showPreview && !onlyJs) - Expanded( - child: Container( - decoration: model.showProjectPreview - ? decoration - : null, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(0), - ), - elevation: 0, - ), - onPressed: () { - model.setShowConsole = false; - model.setShowProjectPreview = - true; - }, - child: Text( - context.t.preview, - ), + appBar: PreferredSize( + preferredSize: Size( + MediaQuery.sizeOf(context).width, + model.showPanel ? 0 : 50, + ), + child: AppBar( + automaticallyImplyLeading: !model.showPreview, + title: challenge.files.length == 1 && !model.showPreview + ? Text(context.t.editor) + : Row( + children: [ + if (model.showPreview && !onlyJs) + Expanded( + child: Container( + decoration: model.showProjectPreview + ? decoration + : null, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(0), ), + elevation: 0, + ), + onPressed: () { + model.setShowConsole = false; + model.setShowProjectPreview = true; + }, + child: Text( + context.t.preview, ), ), - if (model.showPreview) - Expanded( - child: Container( - decoration: model.showConsole - ? decoration - : null, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(0), - ), - elevation: 0, - ), - onPressed: () { - model.setShowConsole = true; - model.setShowProjectPreview = - false; - }, - child: Text( - context.t.console, - ), + ), + ), + if (model.showPreview) + Expanded( + child: Container( + decoration: + model.showConsole ? decoration : null, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(0), ), + elevation: 0, + ), + onPressed: () { + model.setShowConsole = true; + model.setShowProjectPreview = false; + }, + child: Text( + context.t.console, ), ), - if (!model.showPreview && - challenge.files.length > 1) - for (ChallengeFile file - in challenge.files) - customTabBar( - model, - challenge, - file, - editor, - ) - ], - ), - ) - : null, + ), + ), + if (!model.showPreview && + challenge.files.length > 1) + for (ChallengeFile file in challenge.files) + customTabBar( + model, + challenge, + file, + editor, + ) + ], + ), + ), + ), bottomNavigationBar: Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: customBottomBar( model, + keyboard, challenge, editor, context, @@ -351,162 +337,253 @@ class ChallengeView extends StatelessWidget { Widget customBottomBar( ChallengeViewModel model, + bool keyboard, Challenge challenge, Editor editor, BuildContext context, ) { return BottomAppBar( + height: keyboard ? 116 : 72, + padding: keyboard ? const EdgeInsets.only(bottom: 8) : null, color: const Color(0xFF0a0a23), - child: Row( + child: Column( children: [ - SizedBox( - width: 1, - height: 1, - child: InAppWebView( - onWebViewCreated: (controller) { - model.setTestController = controller; - }, - onConsoleMessage: (controller, console) { - model.handleConsoleLogMessagges(console, challenge); - }, + if (keyboard) + SymbolBar( + model: model, + editor: editor, ), - ), - Container( - margin: const EdgeInsets.symmetric(horizontal: 8), - color: model.showPanel && model.panelType == PanelType.instruction - ? Colors.white - : const Color.fromRGBO(0x3B, 0x3B, 0x4F, 1), - child: IconButton( - icon: Icon( - Icons.info_outline_rounded, - size: 32, + Row( + children: [ + SizedBox( + width: 1, + height: 1, + child: InAppWebView( + onWebViewCreated: (controller) { + model.setTestController = controller; + }, + onConsoleMessage: (controller, console) { + model.handleConsoleLogMessagges(console, challenge); + }, + ), + ), + Container( + margin: const EdgeInsets.symmetric(horizontal: 8), color: model.showPanel && model.panelType == PanelType.instruction + ? Colors.white + : const Color.fromRGBO(0x3B, 0x3B, 0x4F, 1), + child: IconButton( + icon: Icon( + Icons.info_outline_rounded, + size: 32, + color: model.showPanel && + model.panelType == PanelType.instruction ? const Color.fromRGBO(0x3B, 0x3B, 0x4F, 1) : Colors.white, - ), - onPressed: () { - if (model.showPanel && - model.panelType != PanelType.instruction) { - model.setPanelType = PanelType.instruction; - } else { - model.setPanelType = PanelType.instruction; - if (MediaQuery.of(context).viewInsets.bottom > 0) { - FocusManager.instance.primaryFocus?.unfocus(); - if (!model.showPanel) { - model.setShowPanel = true; - model.setHideAppBar = true; + ), + onPressed: () { + if (model.showPanel && + model.panelType != PanelType.instruction) { + model.setPanelType = PanelType.instruction; + } else { + model.setPanelType = PanelType.instruction; + if (MediaQuery.of(context).viewInsets.bottom > 0) { + FocusManager.instance.primaryFocus?.unfocus(); + if (!model.showPanel) { + model.setShowPanel = true; + } + } else { + model.setShowPanel = !model.showPanel; + } } - } else { - model.setHideAppBar = !model.hideAppBar; - model.setShowPanel = !model.showPanel; - } - } - }, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - ), - ), - Container( - margin: const EdgeInsets.symmetric(horizontal: 8), - color: !model.showPreview - ? const Color.fromRGBO(0x3B, 0x3B, 0x4F, 1) - : Colors.white, - child: IconButton( - icon: Icon( - Icons.remove_red_eye_outlined, - size: 32, - color: model.showPreview + }, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), + ), + Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + color: !model.showPreview ? const Color.fromRGBO(0x3B, 0x3B, 0x4F, 1) : Colors.white, + child: IconButton( + icon: Icon( + Icons.remove_red_eye_outlined, + size: 32, + color: model.showPreview + ? const Color.fromRGBO(0x3B, 0x3B, 0x4F, 1) + : Colors.white, + ), + onPressed: () async { + ChallengeFile currFile = model.currentFile(challenge); + + String currText = + await model.fileService.getExactFileFromCache( + challenge, + currFile, + ); + + model.setMounted = false; + + editor.fileTextStream.sink.add(FileIDE( + id: challenge.id + currFile.name, + ext: currFile.ext.name.toUpperCase(), + name: currFile.name, + content: currText == '' ? currFile.contents : currText, + hasRegion: currFile.editableRegionBoundaries.isNotEmpty, + region: EditorRegionOptions( + start: currFile.editableRegionBoundaries.isNotEmpty + ? currFile.editableRegionBoundaries[0] + : null, + end: currFile.editableRegionBoundaries.isNotEmpty + ? currFile.editableRegionBoundaries[1] + : null, + ), + )); + model.setEditorText = + currText == '' ? currFile.contents : currText; + model.setShowPreview = !model.showPreview; + + if (!model.showProjectPreview && !model.showConsole) { + model.setShowProjectPreview = true; + } + + model.refresh(); + }, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), ), - onPressed: () async { - ChallengeFile currFile = model.currentFile(challenge); + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + color: !model.hasTypedInEditor + ? const Color.fromARGB(255, 9, 79, 125) + : model.completedChallenge + ? const Color.fromRGBO(0x20, 0xD0, 0x32, 1) + : const Color.fromRGBO(0x1D, 0x9B, 0xF0, 1), + child: IconButton( + icon: model.runningTests + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(), + ) + : model.completedChallenge + ? const Icon(Icons.arrow_forward_rounded, + size: 30) + : const Icon(Icons.done_rounded, size: 30), + onPressed: model.hasTypedInEditor + ? () async { + model.setAfterFirstTest = false; + model.setConsoleMessages = []; + model.setUserConsoleMessages = []; + if (model.showPanel && + model.panelType == PanelType.pass) { + model.learnService.goToNextChallenge( + model.block!.challenges.length, + challengesCompleted, + challenge, + block, + ); + } - String currText = await model.fileService.getExactFileFromCache( - challenge, - currFile, - ); + model.setShowPanel = false; + model.setIsRunningTests = true; + await model.runner.setWebViewContent( + challenge, + controller: model.testController!, + ); + } + : null, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ); + } +} + +class SymbolBar extends StatelessWidget { + const SymbolBar({ + super.key, + required this.editor, + required this.model, + }); + + final Editor editor; + final ChallengeViewModel model; + + static List symbols = ['<', '/', '>', '\\', '\'', '"', '=', '{', '}']; - model.setMounted = false; - - editor.fileTextStream.sink.add(FileIDE( - id: challenge.id + currFile.name, - ext: currFile.ext.name.toUpperCase(), - name: currFile.name, - content: currText == '' ? currFile.contents : currText, - hasRegion: currFile.editableRegionBoundaries.isNotEmpty, - region: EditorRegionOptions( - start: currFile.editableRegionBoundaries.isNotEmpty - ? currFile.editableRegionBoundaries[0] - : null, - end: currFile.editableRegionBoundaries.isNotEmpty - ? currFile.editableRegionBoundaries[1] - : null, + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.only(bottom: 8), + height: 50, + color: const Color(0xFF1b1b32), + child: Stack( + children: [ + ListView.builder( + scrollDirection: Axis.horizontal, + controller: model.symbolBarScrollController, + itemCount: symbols.length, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 1, + ), + child: TextButton( + onPressed: () { + model.insertSymbol(symbols[index], editor); + }, + style: TextButton.styleFrom( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.zero), + ), ), - )); - model.setEditorText = - currText == '' ? currFile.contents : currText; - model.setShowPreview = !model.showPreview; - - if (!model.showProjectPreview && !model.showConsole) { - model.setShowProjectPreview = true; - } - - model.refresh(); - }, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - ), + child: Text(symbols[index]), + ), + ); + }, ), - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, + if (model.symbolBarIsScrollable) + Row( children: [ - Container( - margin: const EdgeInsets.symmetric(horizontal: 8), - color: !model.hasTypedInEditor - ? const Color.fromARGB(255, 9, 79, 125) - : model.completedChallenge - ? const Color.fromRGBO(0x20, 0xD0, 0x32, 1) - : const Color.fromRGBO(0x1D, 0x9B, 0xF0, 1), - child: IconButton( - icon: model.runningTests - ? const CircularProgressIndicator() - : model.completedChallenge - ? const Icon(Icons.arrow_forward_rounded, size: 30) - : const Icon(Icons.done_rounded, size: 30), - onPressed: model.hasTypedInEditor - ? () async { - model.setAfterFirstTest = false; - model.setConsoleMessages = []; - model.setUserConsoleMessages = []; - if (model.showPanel && - model.panelType == PanelType.pass) { - model.learnService.goToNextChallenge( - model.block!.challenges.length, - challengesCompleted, - challenge, - block, - ); - } - - model.setShowPanel = false; - model.setIsRunningTests = true; - await model.runner.setWebViewContent( - challenge, - controller: model.testController!, - ); - FocusManager.instance.primaryFocus?.unfocus(); - } - : null, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Container( + width: 15, + height: 66, + foregroundDecoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + Colors.white.withOpacity(0.13), + Colors.white.withOpacity(0.23), + Colors.white.withOpacity(0.33), + ], + ), + ), + ), ), ), ], ), - ), ], ), ); diff --git a/mobile-app/lib/ui/views/learn/challenge/challenge_viewmodel.dart b/mobile-app/lib/ui/views/learn/challenge/challenge_viewmodel.dart index 262867bf8..6910fa285 100644 --- a/mobile-app/lib/ui/views/learn/challenge/challenge_viewmodel.dart +++ b/mobile-app/lib/ui/views/learn/challenge/challenge_viewmodel.dart @@ -1,5 +1,4 @@ import 'dart:convert'; - import 'package:dio/dio.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; @@ -19,6 +18,8 @@ import 'package:freecodecamp/ui/views/learn/test_runner.dart'; import 'package:freecodecamp/ui/widgets/setup_dialog_ui.dart'; import 'package:html/dom.dart' as dom; import 'package:html/parser.dart'; +import 'package:phone_ide/controller/custom_text_controller.dart'; +import 'package:phone_ide/models/textfield_data.dart'; import 'package:phone_ide/phone_ide.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:stacked/stacked.dart'; @@ -40,9 +41,6 @@ class ChallengeViewModel extends BaseViewModel { bool _showConsole = false; bool get showConsole => _showConsole; - bool _hideAppBar = true; - bool get hideAppBar => _hideAppBar; - String _hint = ''; String get hint => _hint; @@ -61,6 +59,11 @@ class ChallengeViewModel extends BaseViewModel { bool _completedChallenge = false; bool get completedChallenge => _completedChallenge; + bool _symbolBarIsScrollable = true; + bool get symbolBarIsScrollable => _symbolBarIsScrollable; + + ScrollController symbolBarScrollController = ScrollController(); + PanelType _panelType = PanelType.instruction; PanelType get panelType => _panelType; @@ -95,6 +98,9 @@ class ChallengeViewModel extends BaseViewModel { EditorOptions defaultEditorOptions = EditorOptions(); + TextFieldData? _textFieldData; + TextFieldData? get textFieldData => _textFieldData; + final _dialogService = locator(); final NavigationService _navigationService = locator(); final LearnFileService fileService = locator(); @@ -123,11 +129,6 @@ class ChallengeViewModel extends BaseViewModel { notifyListeners(); } - set setHideAppBar(bool value) { - _hideAppBar = value; - notifyListeners(); - } - set setShowPanel(bool value) { _showPanel = value; notifyListeners(); @@ -213,6 +214,16 @@ class ChallengeViewModel extends BaseViewModel { notifyListeners(); } + set setTextFieldData(TextFieldData textfieldData) { + _textFieldData = textfieldData; + notifyListeners(); + } + + set setSymbolBarIsScrollable(bool value) { + _symbolBarIsScrollable = value; + notifyListeners(); + } + void init( String url, Block block, @@ -279,6 +290,44 @@ class ChallengeViewModel extends BaseViewModel { } } + void listenToFocusedController(Editor editor) { + editor.textfieldData.stream.listen((textfieldData) { + setTextFieldData = textfieldData; + setShowPanel = false; + }); + } + + void listenToSymbolBarScrollController() { + symbolBarScrollController.addListener(() { + ScrollPosition sp = symbolBarScrollController.position; + + if (sp.pixels >= sp.maxScrollExtent) { + setSymbolBarIsScrollable = false; + } else if (!symbolBarIsScrollable) { + setSymbolBarIsScrollable = true; + } + }); + } + + // This function allows the symbols to be insterted into the text controllers + void insertSymbol(String symbol, Editor editor) async { + final TextEditingControllerIDE focused = textFieldData!.controller; + final RegionPosition position = textFieldData!.position; + final String text = focused.text; + final selection = focused.selection; + final newText = text.replaceRange(selection.start, selection.end, symbol); + focused.value = TextEditingValue( + text: newText, + selection: TextSelection.collapsed( + offset: selection.start + 1, + ), + ); + + editor.textfieldData.sink.add( + TextFieldData(controller: focused, position: position), + ); + } + // This prevents the user from requesting the challenge more than once // when swichting between preview and the challenge. @@ -464,7 +513,6 @@ class ChallengeViewModel extends BaseViewModel { if (msg.startsWith('testMSG: ')) { setPanelType = PanelType.hint; setHint = msg.split('testMSG: ')[1]; - setShowPanel = true; setConsoleMessages = [newMessage, ...userConsoleMessages]; } @@ -477,9 +525,12 @@ class ChallengeViewModel extends BaseViewModel { setPanelType = PanelType.pass; setCompletedChallenge = true; - setShowPanel = true; } setIsRunningTests = false; + + if (panelType != PanelType.instruction) { + setShowPanel = true; + } } } diff --git a/mobile-app/pubspec.lock b/mobile-app/pubspec.lock index c32a7ed1f..6876cff61 100644 --- a/mobile-app/pubspec.lock +++ b/mobile-app/pubspec.lock @@ -1309,10 +1309,10 @@ packages: dependency: "direct main" description: name: phone_ide - sha256: "8483b64b6011cca538ae6ec62e04756a570fa7be76eb4f7141cc0fc39da0684c" + sha256: d462325932567e0c341a7a7e7b014dadc54e4f65cdb4b11f28e29ff4da04e180 url: "https://pub.dev" source: hosted - version: "1.2.3+1" + version: "1.3.0+1" photo_view: dependency: "direct main" description: diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index 6206e0f6c..a157b32e3 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -40,7 +40,7 @@ dependencies: just_audio: ^0.9.37 path: ^1.8.3 # TODO: upgrade after migrating to FLutter 3.19 path_provider: ^2.1.3 - phone_ide: ^1.2.3 + phone_ide: ^1.3.0 photo_view: ^0.15.0 pretty_dio_logger: ^1.2.0-beta-1 # NOTE: Do we still want this? quick_actions: ^1.0.7