Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions mobile-app/lib/models/learn/challenge_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,23 @@ enum HelpCategory {
const HelpCategory(this.value);
}

enum DemoType {
onLoad('onLoad'),
onClick('onClick');

final String value;
const DemoType(this.value);

static DemoType? fromValue(String? value) {
if (value == null) return null;
try {
return DemoType.values.firstWhere((type) => type.value == value);
} catch (_) {
return null;
}
}
}

class Challenge {
final String id;
final String block;
Expand Down Expand Up @@ -73,6 +90,9 @@ class Challenge {
// Challenge Type 15 - Odin
final List<String>? assignments;

final List<List<SolutionFile>>? solutions;
final DemoType? demoType;

Challenge({
required this.id,
required this.block,
Expand All @@ -94,6 +114,8 @@ class Challenge {
this.audio,
this.scene,
required this.hooks,
this.solutions,
this.demoType,
});

factory Challenge.fromJson(Map<String, dynamic> data) {
Expand Down Expand Up @@ -134,6 +156,14 @@ class Challenge {
hooks: Hooks.fromJson(
data['hooks'] ?? {'beforeAll': ''},
),
solutions: data['solutions'] != null
? (data['solutions'] as List)
.map<List<SolutionFile>>((solutionList) => (solutionList as List)
.map<SolutionFile>((file) => SolutionFile.fromJson(file))
.toList())
.toList()
: null,
demoType: DemoType.fromValue(data['demoType']),
);
}

Expand Down Expand Up @@ -180,6 +210,11 @@ class Challenge {
'solution': question.solution,
})
.toList(),
'solutions': challenge.solutions
?.map((solutionList) =>
solutionList.map((file) => file.toJson()).toList())
.toList(),
'demoType': challenge.demoType?.value,
};
}
}
Expand Down Expand Up @@ -494,3 +529,61 @@ class EnglishAudio {
);
}
}

class SolutionFile {
final String head;
final String tail;
final String id;
final List<String> history;
final String name;
final String ext;
final String path;
final String fileKey;
final String contents;
final String seed;
final String? error;

SolutionFile({
required this.head,
required this.tail,
required this.id,
required this.history,
required this.name,
required this.ext,
required this.path,
required this.fileKey,
required this.contents,
required this.seed,
this.error,
});

factory SolutionFile.fromJson(Map<String, dynamic> data) {
return SolutionFile(
head: data['head'] ?? '',
tail: data['tail'] ?? '',
id: data['id'] ?? '',
history: ((data['history'] ?? []) as List).cast<String>(),
name: data['name'] ?? '',
ext: data['ext'] ?? '',
path: data['path'] ?? '',
fileKey: data['fileKey'] ?? '',
contents: data['contents'] ?? '',
seed: data['seed'] ?? '',
error: data['error'],
);
}

Map<String, dynamic> toJson() => {
'head': head,
'tail': tail,
'id': id,
'history': history,
'name': name,
'ext': ext,
'path': path,
'fileKey': fileKey,
'contents': contents,
'seed': seed,
'error': error,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,68 @@ class ChallengeViewModel extends BaseViewModel {
return document;
}

String writeDemoDocument(String doc, List<List<SolutionFile>>? solutions) {
if (solutions == null || solutions.isEmpty) {
return parse(doc).outerHtml;
}

List<SolutionFile> solutionFiles = solutions[0];
List<SolutionFile> cssFiles =
solutionFiles.where((file) => file.ext == 'css').toList();
List<SolutionFile> jsFiles =
solutionFiles.where((file) => file.ext == 'js').toList();
List<SolutionFile> htmlFiles =
solutionFiles.where((file) => file.ext == 'html').toList();

String text = htmlFiles.isNotEmpty ? htmlFiles[0].contents : doc;
Document document = parse(text);

if (cssFiles.isNotEmpty) {
// Insert CSS as <style> tags into <head>
StringBuffer cssBuffer = StringBuffer();
for (var css in cssFiles) {
cssBuffer.writeln('<style>${css.contents}</style>');
}
if (document.head != null) {
document.head!.append(parseFragment(cssBuffer.toString()));
}
}

if (jsFiles.isNotEmpty) {
// Insert JS as <script> tags before </body>
StringBuffer jsBuffer = StringBuffer();
for (var js in jsFiles) {
jsBuffer.writeln('<script>${js.contents}</script>');
}
if (document.body != null) {
document.body!.append(parseFragment(jsBuffer.toString()));
}
}

String viewPort = '''<meta content="width=device-width,
initial-scale=1.0, maximum-scale=1.0,
user-scalable=no" name="viewport">
<meta>''';
Document viewPortParsed = parse(viewPort);
Node meta = viewPortParsed.getElementsByTagName('META')[0];
document.getElementsByTagName('HEAD')[0].append(meta);

return document.outerHtml;
}

String? provideDemo(List<List<SolutionFile>>? solutions) {
if (solutions == null || solutions.isEmpty) {
return null;
}

List<SolutionFile> htmlFiles =
solutions[0].where((file) => file.ext == 'html').toList();
String doc = htmlFiles.isNotEmpty ? htmlFiles[0].contents : '';
String document = writeDemoDocument(doc, solutions);

return document;
}

String parseUsersConsoleMessages(String string) {
if (!string.startsWith('testMSG')) {
return '<p>$string</p>';
Expand Down Expand Up @@ -731,6 +793,8 @@ class ChallengeViewModel extends BaseViewModel {
return DescriptionView(
description: challenge.description,
instructions: challenge.instructions,
solutions: challenge.solutions,
demoType: challenge.demoType,
challengeModel: model,
maxChallenges: maxChallenges,
title: challenge.title,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:freecodecamp/extensions/i18n_extension.dart';
import 'package:freecodecamp/models/learn/challenge_model.dart';
import 'package:freecodecamp/ui/views/learn/challenge/challenge_viewmodel.dart';

class ProjectDemo extends StatelessWidget {
const ProjectDemo({
super.key,
required this.solutions,
required this.model,
});

final List<List<SolutionFile>>? solutions;
final ChallengeViewModel model;

@override
Widget build(BuildContext context) {
return Dialog(
insetPadding: EdgeInsets.zero,
backgroundColor: Colors.transparent,
child: Container(
width: double.infinity,
height: double.infinity,
color: Theme.of(context).scaffoldBackgroundColor,
child: Column(
children: [
AppBar(
automaticallyImplyLeading: false,
title: Text('Demo'),
actions: [
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
),
],
),
Expanded(
child: Builder(
builder: (context) {
final html = model.provideDemo(solutions);
if (html != null) {
return InAppWebView(
initialData: InAppWebViewInitialData(
data: html,
mimeType: 'text/html',
),
onWebViewCreated: (controller) {
model.setWebviewController = controller;
},
initialSettings: InAppWebViewSettings(
// TODO: Set this to true only in dev mode
isInspectable: true,
),
);
}
return const Center(
child: Text('No demo available'),
);
},
),
),
],
),
),
);
}
}
Loading