diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 3046c3e4..a61261b0 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -39,6 +39,9 @@ jobs: # Get flutter dependencies. - run: flutter pub get + # Run build runner to generate dart files + - run: flutter packages pub run --no-sound-null-safety build_runner build --delete-conflicting-outputs + # Check for any formatting issues in the code. - run: flutter format --set-exit-if-changed . diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78c3b763..39e033ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - ib pull_request: jobs: @@ -27,6 +28,9 @@ jobs: # Get flutter dependencies. - run: flutter pub get + # Run build runner to generate dart files + - run: flutter packages pub run --no-sound-null-safety build_runner build --delete-conflicting-outputs + # Check for any formatting issues in the code. - run: flutter format --set-exit-if-changed . diff --git a/README.md b/README.md index 74db2d8b..5e9378f7 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,10 @@ A detailed guide for multiple platforms setup could be find [here](https://flutt - `flutter pub get` to get all the dependencies. - `flutter run` +### Generating Files using Build Runner + +`flutter packages pub run --no-sound-null-safety build_runner build` + ### Android OAuth Config This project uses flutter version 1.20.2 and hence the support for compile time variables. To use compile time variables pass them in `--dart-defines` as `flutter run --dart-define=VAR_NAME=VAR_VALUE`. Supported `dart-defines` include : diff --git a/ios/Podfile.lock b/ios/Podfile.lock index aca8cd97..408fcf5c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -4,26 +4,28 @@ PODS: - AppAuth/ExternalUserAgent (= 1.4.0) - AppAuth/Core (1.4.0) - AppAuth/ExternalUserAgent (1.4.0) - - FBSDKCoreKit (9.1.0): - - FBSDKCoreKit/Basics (= 9.1.0) - - FBSDKCoreKit/Core (= 9.1.0) - - FBSDKCoreKit/Basics (9.1.0) - - FBSDKCoreKit/Core (9.1.0): - - FBSDKCoreKit/Basics - - FBSDKLoginKit (9.1.0): - - FBSDKLoginKit/Login (= 9.1.0) - - FBSDKLoginKit/Login (9.1.0): - - FBSDKCoreKit (~> 9.1.0) + - FBSDKCoreKit (11.0.1): + - FBSDKCoreKit/Core (= 11.0.1) + - FBSDKCoreKit/Core (11.0.1): + - FBSDKCoreKit_Basics (~> 11.0.1) + - FBSDKCoreKit_Basics (11.0.1): + - FBSDKCoreKit_Basics/Basics (= 11.0.1) + - FBSDKCoreKit_Basics/Basics (11.0.1) + - FBSDKLoginKit (11.0.1): + - FBSDKLoginKit/Login (= 11.0.1) + - FBSDKLoginKit/Login (11.0.1): + - FBSDKCoreKit (~> 11.0.1) + - FBSDKCoreKit_Basics (~> 11.0.1) - Flutter (1.0.0) - flutter_facebook_auth (2.0.0): - - FBSDKCoreKit (~> 9.1.0) - - FBSDKLoginKit (~> 9.1.0) + - FBSDKCoreKit (~> 11.0.0) + - FBSDKLoginKit (~> 11.0.0) - Flutter - flutter_keyboard_visibility (0.0.1): - Flutter - flutter_secure_storage (3.3.1): - Flutter - - flutter_web_auth (0.2.4): + - flutter_web_auth (0.3.0): - Flutter - google_sign_in (0.0.1): - Flutter @@ -32,14 +34,10 @@ PODS: - AppAuth (~> 1.2) - GTMAppAuth (~> 1.0) - GTMSessionFetcher/Core (~> 1.1) - - GTMAppAuth (1.1.0): + - GTMAppAuth (1.2.2): - AppAuth/Core (~> 1.4) - - GTMSessionFetcher (~> 1.4) - - GTMSessionFetcher (1.5.0): - - GTMSessionFetcher/Full (= 1.5.0) - - GTMSessionFetcher/Core (1.5.0) - - GTMSessionFetcher/Full (1.5.0): - - GTMSessionFetcher/Core (= 1.5.0) + - GTMSessionFetcher/Core (~> 1.5) + - GTMSessionFetcher/Core (1.6.1) - image_picker (0.0.1): - Flutter - share (0.0.1): @@ -74,6 +72,7 @@ SPEC REPOS: trunk: - AppAuth - FBSDKCoreKit + - FBSDKCoreKit_Basics - FBSDKLoginKit - GoogleSignIn - GTMAppAuth @@ -109,24 +108,25 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: AppAuth: 31bcec809a638d7bd2f86ea8a52bd45f6e81e7c7 - FBSDKCoreKit: a00fe2efd780c195a5e09201bf51c56106245b40 - FBSDKLoginKit: d98498c598ec09de657385a9349a1f21119b7f86 + FBSDKCoreKit: 38c9e29a3f1436362acacff6486d3a28628c288f + FBSDKCoreKit_Basics: 06780a4a12e16596bde8406add02c6aa1bf38616 + FBSDKLoginKit: 8243e04c590c38b85c69fcf09d1234514d2475c3 Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c - flutter_facebook_auth: 4b170c07b7fce791497093fcc3f134fb215f3f07 + flutter_facebook_auth: abc044072738b7baa90d80583dabdb1211614f0d flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec - flutter_web_auth: b7465086188ce4413d7e8b23622583784b9007a7 + flutter_web_auth: ede89bf8107b021cf16e769756d267d84d24e88b google_sign_in: 6bd214b9c154f881422f5fe27b66aaa7bbd580cc GoogleSignIn: 7137d297ddc022a7e0aa4619c86d72c909fa7213 - GTMAppAuth: 197a8dabfea5d665224aa00d17f164fc2248dab9 - GTMSessionFetcher: b3503b20a988c4e20cc189aa798fd18220133f52 - image_picker: 9c3312491f862b28d21ecd8fdf0ee14e601b3f09 + GTMAppAuth: ad5c2b70b9a8689e1a04033c9369c4915bfcbe89 + GTMSessionFetcher: 36689134877faeb055b27dfa4ccc9ceaa42e029e + image_picker: e06f7a68f000bd36f552c1847e33cda96ed31f1f share: 0b2c3e82132f5888bccca3351c504d0003b3b410 shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef video_player: 9cc823b1d9da7e8427ee591e8438bfbcde500e6e - wakelock: bfc7955c418d0db797614075aabbc58a39ab5107 - webview_flutter: d2b4d6c66968ad042ad94cbb791f5b72b4678a96 + wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f + webview_flutter: 3603125dfd3bcbc9d8d418c3f80aeecf331c068b PODFILE CHECKSUM: a75497545d4391e2d394c3668e20cfb1c2bbd4aa diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index a90d86b2..2608e426 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -283,6 +283,7 @@ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", "${BUILT_PRODUCTS_DIR}/AppAuth/AppAuth.framework", "${BUILT_PRODUCTS_DIR}/FBSDKCoreKit/FBSDKCoreKit.framework", + "${BUILT_PRODUCTS_DIR}/FBSDKCoreKit_Basics/FBSDKCoreKit_Basics.framework", "${BUILT_PRODUCTS_DIR}/FBSDKLoginKit/FBSDKLoginKit.framework", "${BUILT_PRODUCTS_DIR}/GTMAppAuth/GTMAppAuth.framework", "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", @@ -302,6 +303,7 @@ outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AppAuth.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBSDKCoreKit.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBSDKCoreKit_Basics.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBSDKLoginKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMAppAuth.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", diff --git a/lib/config/environment_config.dart b/lib/config/environment_config.dart index f85ea707..5fcba99d 100644 --- a/lib/config/environment_config.dart +++ b/lib/config/environment_config.dart @@ -4,6 +4,16 @@ class EnvironmentConfig { defaultValue: 'https://circuitverse.org/api/v1', ); + static const String IB_API_BASE_URL = String.fromEnvironment( + 'IB_API_BASE_URL', + defaultValue: 'https://learn.circuitverse.org/_api/pages', + ); + + static const String IB_BASE_URL = String.fromEnvironment( + 'IB_BASE_URL', + defaultValue: 'https://learn.circuitverse.org', + ); + // GITHUB OAUTH ENV VARIABLES static const String GITHUB_OAUTH_CLIENT_ID = String.fromEnvironment( 'GITHUB_OAUTH_CLIENT_ID', diff --git a/lib/cv_theme.dart b/lib/cv_theme.dart index 91441998..5a5baae3 100644 --- a/lib/cv_theme.dart +++ b/lib/cv_theme.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; class CVTheme { CVTheme._(); + static Color textFieldLabelColor(context) { return Theme.of(context).brightness == Brightness.dark ? Colors.grey[300] @@ -70,6 +71,7 @@ class CVTheme { static const Color bgCard = Color.fromRGBO(255, 255, 255, 0.9); static const Color bgCardDark = Color.fromRGBO(97, 97, 97, 1); static const Color htmlEditorBg = Color.fromRGBO(245, 245, 245, 1); + static const OutlineInputBorder primaryDarkOutlineBorder = OutlineInputBorder( borderRadius: BorderRadius.zero, borderSide: BorderSide(color: CVTheme.primaryColorDark), diff --git a/lib/ib_theme.dart b/lib/ib_theme.dart new file mode 100644 index 00000000..a834ad6c --- /dev/null +++ b/lib/ib_theme.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; + +class IbTheme { + IbTheme._(); + + static ThemeData getThemeData(context) { + return Theme.of(context).copyWith( + primaryIconTheme: Theme.of(context).primaryIconTheme.copyWith( + color: Colors.white, + ), + accentColor: IbTheme.primaryColor, + primaryColor: IbTheme.primaryColor, + textTheme: Theme.of(context).textTheme.apply( + fontFamily: IbTheme.fontFamily, + bodyColor: IbTheme.textColor(context), + ), + primaryTextTheme: Theme.of(context).primaryTextTheme.apply( + fontFamily: IbTheme.fontFamily, + bodyColor: Colors.white, + ), + accentTextTheme: Theme.of(context).accentTextTheme.apply( + fontFamily: IbTheme.fontFamily, + ), + ); + } + + static Color textFieldLabelColor(context) { + return Theme.of(context).brightness == Brightness.dark + ? Colors.grey[300] + : Colors.grey[600]; + } + + static Color textColor(context) { + return Theme.of(context).brightness == Brightness.dark + ? Colors.white + : IbTheme.bodyTextColor; + } + + static Color primaryHeadingColor(context) { + return Theme.of(context).brightness == Brightness.dark + ? Colors.white + : IbTheme.headingTextColor; + } + + static Color boxBg(context) { + return Theme.of(context).brightness == Brightness.dark + ? bgCardDark + : bgCard; + } + + static Color boxShadow(context) { + return Theme.of(context).brightness == Brightness.dark ? bgCardDark : grey; + } + + static Color highlightText(context) { + return Theme.of(context).brightness == Brightness.dark + ? primaryColor + : primaryColorDark; + } + + static Color getPrimaryColor(context) { + return Theme.of(context).brightness == Brightness.dark + ? IbTheme.brightPrimaryColor + : IbTheme.primaryColor; + } + + static const Color primaryColor = Color.fromRGBO(2, 110, 87, 1); + static const Color brightPrimaryColor = Color.fromRGBO(0, 232, 179, 1); + static const Color primaryColorDark = Color.fromRGBO(2, 110, 87, 0.5); + static const Color bodyTextColor = Color.fromRGBO(92, 89, 98, 1); + static const Color headingTextColor = Color.fromRGBO(39, 38, 43, 1); + static const String fontFamily = 'Roboto'; + static const Color grey = Color.fromRGBO(150, 150, 150, 1); + static const Color bgCard = Color.fromRGBO(255, 255, 255, 0.9); + static const Color bgCardDark = Color.fromRGBO(97, 97, 97, 1); +} diff --git a/lib/locator.dart b/lib/locator.dart index fd04b32b..1ef4c321 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -5,11 +5,14 @@ import 'package:mobile_app/services/API/fcm_api.dart'; import 'package:mobile_app/services/API/grades_api.dart'; import 'package:mobile_app/services/API/group_members_api.dart'; import 'package:mobile_app/services/API/groups_api.dart'; +import 'package:mobile_app/services/API/ib_api.dart'; import 'package:mobile_app/services/API/projects_api.dart'; import 'package:mobile_app/services/API/users_api.dart'; import 'package:mobile_app/services/API/country_institute_api.dart'; +import 'package:mobile_app/services/database_service.dart'; import 'package:mobile_app/services/dialog_service.dart'; import 'package:mobile_app/services/API/contributors_api.dart'; +import 'package:mobile_app/services/ib_engine_service.dart'; import 'package:mobile_app/services/local_storage_service.dart'; import 'package:mobile_app/viewmodels/authentication/auth_options_viewmodel.dart'; import 'package:mobile_app/viewmodels/authentication/forgot_password_viewmodel.dart'; @@ -24,6 +27,8 @@ import 'package:mobile_app/viewmodels/groups/my_groups_viewmodel.dart'; import 'package:mobile_app/viewmodels/groups/new_group_viewmodel.dart'; import 'package:mobile_app/viewmodels/groups/update_assignment_viewmodel.dart'; import 'package:mobile_app/viewmodels/home/home_viewmodel.dart'; +import 'package:mobile_app/viewmodels/ib/ib_landing_viewmodel.dart'; +import 'package:mobile_app/viewmodels/ib/ib_page_viewmodel.dart'; import 'package:mobile_app/viewmodels/profile/edit_profile_viewmodel.dart'; import 'package:mobile_app/viewmodels/profile/profile_viewmodel.dart'; import 'package:mobile_app/viewmodels/profile/user_favourites_viewmodel.dart'; @@ -37,10 +42,16 @@ import 'package:mobile_app/viewmodels/about/about_viewmodel.dart'; GetIt locator = GetIt.instance; Future setupLocator() async { + // Dialog Service locator.registerLazySingleton(() => DialogService()); + + // Local Storage Service var localStorageService = await LocalStorageService.getInstance(); locator.registerSingleton(localStorageService); + // Database Service + locator.registerLazySingleton(() => DatabaseServiceImpl()); + // API Services locator.registerLazySingleton(() => HttpContributorsApi()); locator.registerLazySingleton(() => HttpUsersApi()); @@ -53,6 +64,10 @@ Future setupLocator() async { locator.registerLazySingleton(() => HttpFCMApi()); locator.registerLazySingleton( () => HttpCountryInstituteAPI()); + locator.registerLazySingleton(() => HttpIbApi()); + + // Interactive Book Engine Service + locator.registerLazySingleton(() => IbEngineServiceImpl()); // Startup ViewModel locator.registerFactory(() => StartUpViewModel()); @@ -89,4 +104,8 @@ Future setupLocator() async { locator.registerFactory(() => AddAssignmentViewModel()); locator.registerFactory(() => UpdateAssignmentViewModel()); locator.registerFactory(() => AssignmentDetailsViewModel()); + + // Interactive Book ViewModels + locator.registerFactory(() => IbLandingViewModel()); + locator.registerFactory(() => IbPageViewModel()); } diff --git a/lib/main.dart b/lib/main.dart index 49181867..5a06eba1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:get/get.dart'; import 'package:mobile_app/cv_theme.dart'; import 'package:mobile_app/locale/locales.dart'; import 'package:mobile_app/locator.dart'; +import 'package:mobile_app/services/database_service.dart'; import 'package:mobile_app/utils/router.dart'; import 'package:theme_provider/theme_provider.dart'; import 'ui/views/startup_view.dart'; @@ -16,6 +17,9 @@ Future main() async { // Register all the models and services before the app starts await setupLocator(); + // Init Hive + await locator().init(); + runApp(CircuitVerseMobile()); } diff --git a/lib/models/failure_model.dart b/lib/models/failure_model.dart index fe0443b9..f1f3576a 100644 --- a/lib/models/failure_model.dart +++ b/lib/models/failure_model.dart @@ -1,4 +1,4 @@ -class Failure { +class Failure implements Exception { final String message; Failure(this.message); diff --git a/lib/models/ib/ib_chapter.dart b/lib/models/ib/ib_chapter.dart new file mode 100644 index 00000000..97d22cd9 --- /dev/null +++ b/lib/models/ib/ib_chapter.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class IbChapter { + final String id; + final String value; + final String navOrder; + IbChapter prev; + IbChapter next; + final List items; + + IbChapter({ + @required this.id, + @required this.value, + @required this.navOrder, + this.prev, + this.next, + this.items, + }); + + set prevPage(IbChapter prev) => this.prev = prev; + set nextPage(IbChapter next) => this.next = next; + + @override + String toString() { + return '{id: $id, prev: ${prev?.id}, next: ${next?.id}'; + } +} diff --git a/lib/models/ib/ib_content.dart b/lib/models/ib/ib_content.dart new file mode 100644 index 00000000..565b9474 --- /dev/null +++ b/lib/models/ib/ib_content.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +abstract class IbContent { + String content; + + IbContent({@required this.content}); +} + +class IbTocItem extends IbContent { + final List items; + + IbTocItem({@required String content, this.items}) : super(content: content); +} + +class IbMd extends IbContent { + IbMd({@required String content}) : super(content: content); +} diff --git a/lib/models/ib/ib_page_data.dart b/lib/models/ib/ib_page_data.dart new file mode 100644 index 00000000..e0ad9897 --- /dev/null +++ b/lib/models/ib/ib_page_data.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:mobile_app/models/ib/ib_content.dart'; + +class IbPageData { + final String id; + final String pageUrl; + final String title; + final List content; + final List tableOfContents; + final List chapterOfContents; + + IbPageData({ + @required this.id, + @required this.pageUrl, + @required this.title, + @required this.content, + this.tableOfContents, + this.chapterOfContents, + }); +} diff --git a/lib/models/ib/ib_raw_page_data.dart b/lib/models/ib/ib_raw_page_data.dart new file mode 100644 index 00000000..6f215bb5 --- /dev/null +++ b/lib/models/ib/ib_raw_page_data.dart @@ -0,0 +1,83 @@ +import 'package:hive/hive.dart'; + +part 'ib_raw_page_data.g.dart'; + +@HiveType(typeId: 0) +class IbRawPageData { + @HiveField(0) + String id; + + @HiveField(1) + String title; + + @HiveField(2) + String name; + + @HiveField(3) + String content; + + @HiveField(4) + String rawContent; + + @HiveField(5) + String navOrder; + + @HiveField(6) + String cvibLevel; + + @HiveField(7) + String parent; + + @HiveField(8) + bool hasChildren; + + @HiveField(9) + bool hasToc; + + @HiveField(10) + bool disableComments; + + @HiveField(11) + Map frontMatter; + + @HiveField(12) + String httpUrl; + + @HiveField(13) + String apiUrl; + + IbRawPageData({ + this.id, + this.title, + this.name, + this.content, + this.rawContent, + this.navOrder, + this.cvibLevel, + this.parent, + this.hasChildren, + this.hasToc, + this.disableComments, + this.frontMatter, + this.httpUrl, + this.apiUrl, + }); + + factory IbRawPageData.fromJson(Map json) => IbRawPageData( + id: json['path'] ?? json['relative_path'], + name: json['name'], + title: json['title'], + content: json['content'], + rawContent: json['raw_content'], + navOrder: json['nav_order'].toString(), + cvibLevel: json['cvib_level'], + parent: json['parent'], + hasChildren: json['has_children'] ?? false, + hasToc: json['has_toc'] ?? (json['name'] == 'index.md' ? false : true), + disableComments: json['disable_comments'] ?? + (json['name'] == 'index.md' ? true : false), + frontMatter: json['front_matter'] ?? {}, + httpUrl: json['http_url'], + apiUrl: json['api_url'], + ); +} diff --git a/lib/models/ib/ib_raw_page_data.g.dart b/lib/models/ib/ib_raw_page_data.g.dart new file mode 100644 index 00000000..a95c1d05 --- /dev/null +++ b/lib/models/ib/ib_raw_page_data.g.dart @@ -0,0 +1,80 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'ib_raw_page_data.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class IbRawPageDataAdapter extends TypeAdapter { + @override + final int typeId = 0; + + @override + IbRawPageData read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return IbRawPageData( + id: fields[0] as String, + title: fields[1] as String, + name: fields[2] as String, + content: fields[3] as String, + rawContent: fields[4] as String, + navOrder: fields[5] as String, + cvibLevel: fields[6] as String, + parent: fields[7] as String, + hasChildren: fields[8] as bool, + hasToc: fields[9] as bool, + disableComments: fields[10] as bool, + frontMatter: (fields[11] as Map)?.cast(), + httpUrl: fields[12] as String, + apiUrl: fields[13] as String, + ); + } + + @override + void write(BinaryWriter writer, IbRawPageData obj) { + writer + ..writeByte(14) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.title) + ..writeByte(2) + ..write(obj.name) + ..writeByte(3) + ..write(obj.content) + ..writeByte(4) + ..write(obj.rawContent) + ..writeByte(5) + ..write(obj.navOrder) + ..writeByte(6) + ..write(obj.cvibLevel) + ..writeByte(7) + ..write(obj.parent) + ..writeByte(8) + ..write(obj.hasChildren) + ..writeByte(9) + ..write(obj.hasToc) + ..writeByte(10) + ..write(obj.disableComments) + ..writeByte(11) + ..write(obj.frontMatter) + ..writeByte(12) + ..write(obj.httpUrl) + ..writeByte(13) + ..write(obj.apiUrl); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is IbRawPageDataAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/services/API/ib_api.dart b/lib/services/API/ib_api.dart new file mode 100644 index 00000000..576ecffc --- /dev/null +++ b/lib/services/API/ib_api.dart @@ -0,0 +1,59 @@ +import 'package:mobile_app/config/environment_config.dart'; +import 'package:mobile_app/constants.dart'; +import 'package:mobile_app/locator.dart'; +import 'package:mobile_app/models/failure_model.dart'; +import 'package:mobile_app/models/ib/ib_raw_page_data.dart'; +import 'package:mobile_app/services/database_service.dart'; +import 'package:mobile_app/utils/api_utils.dart'; + +abstract class IbApi { + Future>> fetchApiPage({String id}); + Future fetchRawPageData({String id}); +} + +class HttpIbApi implements IbApi { + /// Database Service + final DatabaseService _db = locator(); + + @override + Future>> fetchApiPage({String id = ''}) async { + var _url = id == '' + ? '${EnvironmentConfig.IB_API_BASE_URL}.json' + : '${EnvironmentConfig.IB_API_BASE_URL}/$id.json'; + + try { + if (await _db.isExpired(_url)) { + var _jsonResponse = await ApiUtils.get(_url); + _jsonResponse = >[..._jsonResponse]; + await _db.setData( + DatabaseBox.IB, + _url, + _jsonResponse, + expireData: true, + ); + return _jsonResponse; + } else { + var data = await _db.getData>(DatabaseBox.IB, _url); + return data.map((e) => Map.from(e))?.toList(); + } + } on FormatException { + throw Failure(Constants.BAD_RESPONSE_FORMAT); + } on Exception { + throw Failure(Constants.GENERIC_FAILURE); + } + } + + @override + Future fetchRawPageData({String id = 'index.md'}) async { + var _url = '${EnvironmentConfig.IB_API_BASE_URL}/$id'; + + try { + var _jsonResponse = await ApiUtils.get(_url, utfDecoder: true); + return IbRawPageData.fromJson(_jsonResponse); + } on FormatException { + throw Failure(Constants.BAD_RESPONSE_FORMAT); + } on Exception { + throw Failure(Constants.GENERIC_FAILURE); + } + } +} diff --git a/lib/services/database_service.dart b/lib/services/database_service.dart new file mode 100644 index 00000000..7f434fdd --- /dev/null +++ b/lib/services/database_service.dart @@ -0,0 +1,85 @@ +import 'package:flutter/foundation.dart'; +import 'package:hive/hive.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:mobile_app/models/ib/ib_raw_page_data.dart'; + +enum DatabaseBox { + Metadata, + IB, +} + +List DatabaseAdapters = [ + IbRawPageDataAdapter(), +]; + +extension DatabaseBoxExt on DatabaseBox { + String get inString => describeEnum(this); +} + +abstract class DatabaseService { + Future init(); + + Future isExpired(String key); + Future getData(DatabaseBox box, String key, {T defaultValue}); + Future setData(DatabaseBox box, String key, dynamic value, + {bool expireData}); +} + +class DatabaseServiceImpl implements DatabaseService { + final int _timeoutHours = 6; + + @override + Future init() async { + // Hive DB setup + try { + await Hive.initFlutter(); + } catch (e) { + print('Hive Initialization Failed'); + } + + // Register Adapters for Hive + for (var adapter in DatabaseAdapters) { + Hive.registerAdapter(adapter); + } + } + + Future _openBox(DatabaseBox box) async { + if (Hive.isBoxOpen(box.inString)) { + return Hive.box(box.inString); + } + + return await Hive.openBox(box.inString); + } + + @override + Future isExpired(String key) async { + var data = await getData(DatabaseBox.Metadata, key); + + if (data == null || + data.isBefore( + DateTime.now().subtract(Duration(hours: _timeoutHours)))) { + return true; + } + + return false; + } + + @override + Future getData(DatabaseBox box, String key, {T defaultValue}) async { + var openedBox = await _openBox(box); + + return openedBox.get(key, defaultValue: defaultValue); + } + + @override + Future setData(DatabaseBox box, String key, dynamic value, + {bool expireData = false}) async { + var openedBox = await _openBox(box); + + if (expireData) { + await setData(DatabaseBox.Metadata, key, DateTime.now()); + } + + await openedBox.put(key, value); + } +} diff --git a/lib/services/ib_engine_service.dart b/lib/services/ib_engine_service.dart new file mode 100644 index 00000000..2e67f5b9 --- /dev/null +++ b/lib/services/ib_engine_service.dart @@ -0,0 +1,277 @@ +import 'package:html/dom.dart'; +import 'package:html/parser.dart'; +import 'package:mobile_app/config/environment_config.dart'; +import 'package:mobile_app/locator.dart'; +import 'package:mobile_app/models/failure_model.dart'; +import 'package:mobile_app/models/ib/ib_chapter.dart'; +import 'package:mobile_app/models/ib/ib_content.dart'; +import 'package:mobile_app/models/ib/ib_page_data.dart'; +import 'package:mobile_app/models/ib/ib_raw_page_data.dart'; +import 'package:mobile_app/services/API/ib_api.dart'; +import 'package:html_unescape/html_unescape.dart'; +import 'package:mobile_app/utils/api_utils.dart'; + +/// Interactive Book Parser Engine +abstract class IbEngineService { + Future> getChapters(); + Future getPageData({String id = 'index.md'}); + Future getHtmlInteraction(String id); +} + +class IbEngineServiceImpl implements IbEngineService { + /// Inject ApiService via locator + final IbApi _ibApi = locator(); + + /// Locally cache Chapters list for the session + List _ibChapters = []; + + /// module.js URL for interaction + final _intModuleJsUrl = + '${EnvironmentConfig.IB_BASE_URL}/assets/js/module.js'; + + /// Base path for interactions + final _intBaseUrl = + 'https://raw.githubusercontent.com/CircuitVerse/Interactive-Book/master/_includes'; + + /// module.js contents + String _intModuleJs; + + /// Fetches Pages inside an API Page + Future> _fetchPagesInDir({ + String id = '', + bool ignoreIndex = false, + }) async { + /// Fetch response from API for the given id + var _apiResponse; + try { + _apiResponse = await _ibApi.fetchApiPage(id: id); + } catch (_) { + throw Failure('IbApi: ${_.toString()}'); + } + + var parentPages = []; + var childPages = []; + + // Iterate over the list of pages present inside this response + for (var page in _apiResponse) { + // Recursive scan if the page is directory + if (page['type'] == 'directory') { + childPages.addAll(await _fetchPagesInDir(id: page['path'])); + } else if (page['has_children'] != null && page['has_children']) { + // If the page has children inside a directory, it's a parent page + // All child pages must be present inside parent one + // refer an example: https://learn.circuitverse.org/_api/pages/docs/binary-representation.json + parentPages.add(IbChapter( + id: page['path'], + value: page['title'], + navOrder: page['nav_order'].toString(), + items: childPages, + )); + } else { + // Ignore Index page if flag enabled or page has no title (like 404 page) + if ((page['title'] == null) || + (page['path'] == 'index.md' && ignoreIndex)) { + continue; + } + + // Add child page to the list + childPages.add(IbChapter( + id: page['path'], + value: page['title'], + navOrder: page['nav_order'].toString(), + )); + } + } + + // Sort child pages + if (parentPages.isNotEmpty) { + childPages.sort((a, b) => a.navOrder.compareTo(b.navOrder)); + } + + return parentPages.isEmpty ? childPages : parentPages; + } + + /// Builds Navigation from IbChapters by + /// Assigning prev and next ids + List _buildNav(List chapters) { + // We have to flatten the nested chapters and assign prev and next to the objects + // but return original list of chapters to keep the order intact + + var _flatten = chapters + .expand((c) => c.items != null ? [c, ...c.items] : [c]) + .toList(); + + if (_flatten.length <= 1) { + return chapters; + } + + IbChapter prev; + + for (var i = 0; i < _flatten.length; i++) { + _flatten[i].prevPage = prev; + if (i + 1 < _flatten.length) { + _flatten[i].nextPage = _flatten[i + 1]; + } + + prev = _flatten[i]; + } + + return chapters; + } + + /// Get Chapters and Build Navigation for Interactive Book + @override + Future> getChapters() async { + // Load cached chapters if present + if (_ibChapters.isNotEmpty) { + return _ibChapters; + } + + var _chapters; + try { + _chapters = await _fetchPagesInDir(ignoreIndex: true); + } on Failure catch (e) { + throw Failure(e.toString()); + } + + // Sort root pages + _chapters + .sort((a, b) => int.parse(a.navOrder).compareTo(int.parse(b.navOrder))); + + // Build Navigation + _chapters = _buildNav(_chapters); + + _ibChapters = _chapters; + return _ibChapters; + } + + /// Recursively parses list of table of contents + List _parseToc(Element element, {bool num = true}) { + var index = num ? 1 : 'a'.codeUnitAt(0); + var toc = []; + + for (var li in element.children) { + var eff_index = num ? index.toString() : String.fromCharCode(index); + if (li.getElementsByTagName('ol').isNotEmpty) { + toc.add( + IbTocItem( + content: '$eff_index. ${li.firstChild.text}', + items: _parseToc(li.children[1], num: !num), + ), + ); + } else { + toc.add( + IbTocItem( + content: '$eff_index. ${li.text}', + ), + ); + } + index += 1; + } + + return toc; + } + + /// Recursively parses list of chapter contents + List _parseChapterContents(Element element, + {bool num = true, bool root = false}) { + var index = num ? 1 : 'a'.codeUnitAt(0); + var toc = []; + + for (var li in element.children) { + var eff_index = num ? index.toString() : String.fromCharCode(index); + var sublist = []; + + for (var node in li.nodes) { + if (node is Element && node.localName == 'ul') { + sublist.addAll(_parseChapterContents(node, num: !num)); + break; + } + } + + toc.add(IbTocItem( + content: root + ? '$eff_index. ${li.nodes[0].text.trim()}' + : '$eff_index. ${li.text.trim()}', + items: sublist.isNotEmpty ? sublist : null, + )); + + index += 1; + } + + return toc; + } + + /// Fetches Table of Contents from HTML content + List _getTableOfContents(String content) { + var document = parse(content); + var mdElement = document.getElementById('markdown-toc'); + + if (mdElement != null) { + return _parseToc(mdElement); + } else { + return []; + } + } + + /// Fetches Chapter of Contents from HTML Content + List _getChapterOfContents(String content) { + var document = parse(content); + var mdElement = document.getElementById('chapter-contents-toc'); + + if (mdElement != null) { + return _parseChapterContents(mdElement, root: true); + } else { + return []; + } + } + + /// Fetches "Rich" Page Content + @override + Future getPageData({String id = 'index.md'}) async { + /// Fetch Raw Page Data from API + IbRawPageData _ibRawPageData; + try { + _ibRawPageData = await _ibApi.fetchRawPageData(id: id); + } catch (_) { + throw Failure(_.toString()); + } + + return IbPageData( + id: _ibRawPageData.id, + pageUrl: _ibRawPageData.httpUrl, + title: _ibRawPageData.title, + content: [ + IbMd(content: HtmlUnescape().convert(_ibRawPageData.rawContent)), + ], + tableOfContents: _ibRawPageData.hasToc + ? _getTableOfContents(_ibRawPageData.content) + : [], + chapterOfContents: _ibRawPageData.hasChildren + ? _getChapterOfContents(_ibRawPageData.content) + : [], + ); + } + + /// Fetches HTML Interaction from the given [id] + /// id is basically the file-name of the HTML interaction + /// Every Interaction uses module.js which also has to be used + @override + Future getHtmlInteraction(String id) async { + // Fetch JS content if not already fetched + _intModuleJs ??= await ApiUtils.get(_intModuleJsUrl, rawResponse: true); + + // Fetch Interaction HTML + String html = await ApiUtils.get('$_intBaseUrl/$id', rawResponse: true); + + // concat JS + HTML + var js = ''; + var result = '$js\n$html'; + + // Replace local URLs with absolute + result = result.replaceAll(RegExp(r'(\.\.(\/\.\.)?)?(? { if (_model.isSuccess(_model.GOOGLE_OAUTH)) { await Get.offAllNamed(CVLandingView.id); } else if (_model.isError(_model.GOOGLE_OAUTH)) { - SnackBarUtils.showDark(_model.errorMessageFor(_model.GOOGLE_OAUTH)); + SnackBarUtils.showDark( + 'Google Authentication Error', + _model.errorMessageFor(_model.GOOGLE_OAUTH), + ); } } @@ -34,7 +37,10 @@ class _AuthOptionsViewState extends State { if (_model.isSuccess(_model.FB_OAUTH)) { await Get.offAllNamed(CVLandingView.id); } else if (_model.isError(_model.FB_OAUTH)) { - SnackBarUtils.showDark(_model.errorMessageFor(_model.FB_OAUTH)); + SnackBarUtils.showDark( + 'Facebook Authentication Error', + _model.errorMessageFor(_model.FB_OAUTH), + ); } } @@ -44,7 +50,10 @@ class _AuthOptionsViewState extends State { if (_model.isSuccess(_model.GITHUB_OAUTH)) { await Get.offAllNamed(CVLandingView.id); } else if (_model.isError(_model.GITHUB_OAUTH)) { - SnackBarUtils.showDark(_model.errorMessageFor(_model.GITHUB_OAUTH)); + SnackBarUtils.showDark( + 'GitHub Authentication Error', + _model.errorMessageFor(_model.GITHUB_OAUTH), + ); } } diff --git a/lib/ui/views/authentication/forgot_password_view.dart b/lib/ui/views/authentication/forgot_password_view.dart index 6bc70f70..d59bc723 100644 --- a/lib/ui/views/authentication/forgot_password_view.dart +++ b/lib/ui/views/authentication/forgot_password_view.dart @@ -96,7 +96,10 @@ class _ForgotPasswordViewState extends State { if (_model.isSuccess(_model.SEND_RESET_INSTRUCTIONS)) { // show instructions sent snackbar - SnackBarUtils.showDark('Instructions Sent to $_email'); + SnackBarUtils.showDark( + 'Instructions Sent to $_email', + 'Please check your mail for password reset link.', + ); // route back to previous screen await Future.delayed(Duration(seconds: 1)); @@ -104,7 +107,9 @@ class _ForgotPasswordViewState extends State { } else if (_model.isError(_model.SEND_RESET_INSTRUCTIONS)) { // show failure snackbar SnackBarUtils.showDark( - _model.errorMessageFor(_model.SEND_RESET_INSTRUCTIONS)); + 'Error', + _model.errorMessageFor(_model.SEND_RESET_INSTRUCTIONS), + ); _formKey.currentState.reset(); } } diff --git a/lib/ui/views/authentication/login_view.dart b/lib/ui/views/authentication/login_view.dart index 9a71150d..d7deab06 100644 --- a/lib/ui/views/authentication/login_view.dart +++ b/lib/ui/views/authentication/login_view.dart @@ -122,14 +122,20 @@ class _LoginViewState extends State { if (_model.isSuccess(_model.LOGIN)) { // show login successful snackbar.. - SnackBarUtils.showDark('Login Successful'); + SnackBarUtils.showDark( + 'Login Successful', + 'Welcome back!', + ); // move to home view on successful login.. await Future.delayed(Duration(seconds: 1)); await Get.offAllNamed(CVLandingView.id); } else if (_model.isError(_model.LOGIN)) { // show failure snackbar.. - SnackBarUtils.showDark(_model.errorMessageFor(_model.LOGIN)); + SnackBarUtils.showDark( + 'Error', + _model.errorMessageFor(_model.LOGIN), + ); _formKey.currentState.reset(); } } diff --git a/lib/ui/views/authentication/signup_view.dart b/lib/ui/views/authentication/signup_view.dart index a601e789..b1f12290 100644 --- a/lib/ui/views/authentication/signup_view.dart +++ b/lib/ui/views/authentication/signup_view.dart @@ -123,7 +123,10 @@ class _SignupViewState extends State { if (_signUpModel.isSuccess(_signUpModel.SIGNUP)) { // show signup successful snackbar.. - SnackBarUtils.showDark('Signup Successful'); + SnackBarUtils.showDark( + 'Signup Successful', + 'Welcome to CircuitVerse!', + ); // move to home view on successful signup.. await Future.delayed( @@ -133,7 +136,9 @@ class _SignupViewState extends State { } else if (_signUpModel.isError(_signUpModel.SIGNUP)) { // show failure snackbar.. SnackBarUtils.showDark( - _signUpModel.errorMessageFor(_signUpModel.SIGNUP)); + 'Error', + _signUpModel.errorMessageFor(_signUpModel.SIGNUP), + ); _formKey.currentState.reset(); } } diff --git a/lib/ui/views/cv_landing_view.dart b/lib/ui/views/cv_landing_view.dart index 8b8e0485..5c62c611 100644 --- a/lib/ui/views/cv_landing_view.dart +++ b/lib/ui/views/cv_landing_view.dart @@ -5,12 +5,14 @@ import 'package:get/get.dart'; import 'package:mobile_app/cv_theme.dart'; import 'package:mobile_app/locator.dart'; import 'package:mobile_app/services/dialog_service.dart'; +import 'package:mobile_app/ui/components/cv_drawer_tile.dart'; import 'package:mobile_app/ui/views/about/about_view.dart'; import 'package:mobile_app/ui/views/authentication/login_view.dart'; import 'package:mobile_app/ui/views/base_view.dart'; import 'package:mobile_app/ui/views/contributors/contributors_view.dart'; import 'package:mobile_app/ui/views/groups/my_groups_view.dart'; import 'package:mobile_app/ui/views/home/home_view.dart'; +import 'package:mobile_app/ui/views/ib/ib_landing_view.dart'; import 'package:mobile_app/ui/views/profile/profile_view.dart'; import 'package:mobile_app/ui/views/projects/featured_projects_view.dart'; import 'package:mobile_app/ui/views/teachers/teachers_view.dart'; @@ -75,19 +77,6 @@ class _CVLandingViewState extends State { ); } - Widget _buildDrawerTile(String title, IconData iconData) { - return ListTile( - leading: Icon( - iconData, - color: CVTheme.drawerIcon(context), - ), - title: Text( - title, - style: Theme.of(context).textTheme.headline6, - ), - ); - } - Future onLogoutPressed() async { Get.back(); @@ -100,7 +89,10 @@ class _CVLandingViewState extends State { if (_dialogResponse.confirmed) { _model.onLogout(); setState(() => _selectedIndex = 0); - SnackBarUtils.showDark('Logged Out Successfully'); + SnackBarUtils.showDark( + 'Logged Out Successfully', + 'You have been signed out.', + ); } } @@ -120,7 +112,7 @@ class _CVLandingViewState extends State { ), InkWell( onTap: () => setSelectedIndexTo(0), - child: _buildDrawerTile('Home', Icons.home), + child: CVDrawerTile(title: 'Home', iconData: Icons.home), ), Theme( data: CVTheme.themeData(context), @@ -140,22 +132,31 @@ class _CVLandingViewState extends State { children: [ InkWell( onTap: () => setSelectedIndexTo(1), - child: _buildDrawerTile('Featured Circuits', Icons.star), + child: CVDrawerTile( + title: 'Featured Circuits', iconData: Icons.star), ), ], ), ), + InkWell( + onTap: () => Get.toNamed(IbLandingView.id), + child: CVDrawerTile( + title: 'Interactive Book', + iconData: Icons.chrome_reader_mode), + ), InkWell( onTap: () => setSelectedIndexTo(2), - child: _buildDrawerTile('About', FontAwesome5.address_card), + child: CVDrawerTile( + title: 'About', iconData: FontAwesome5.address_card), ), InkWell( onTap: () => setSelectedIndexTo(3), - child: _buildDrawerTile('Contribute', Icons.add), + child: CVDrawerTile(title: 'Contribute', iconData: Icons.add), ), InkWell( onTap: () => setSelectedIndexTo(4), - child: _buildDrawerTile('Teachers', Icons.account_balance), + child: CVDrawerTile( + title: 'Teachers', iconData: Icons.account_balance), ), _model.isLoggedIn ? Theme( @@ -171,25 +172,28 @@ class _CVLandingViewState extends State { children: [ InkWell( onTap: () => setSelectedIndexTo(5), - child: - _buildDrawerTile('Profile', FontAwesome5.user), + child: CVDrawerTile( + title: 'Profile', iconData: FontAwesome5.user), ), InkWell( onTap: () => setSelectedIndexTo(6), - child: _buildDrawerTile( - 'My Groups', FontAwesome5.object_group), + child: CVDrawerTile( + title: 'My Groups', + iconData: FontAwesome5.object_group), ), InkWell( onTap: onLogoutPressed, - child: _buildDrawerTile( - 'Log Out', Ionicons.ios_log_out), + child: CVDrawerTile( + title: 'Log Out', + iconData: Ionicons.ios_log_out), ), ], ), ) : InkWell( onTap: () => Get.offAndToNamed(LoginView.id), - child: _buildDrawerTile('Login', Ionicons.ios_log_in), + child: CVDrawerTile( + title: 'Login', iconData: Ionicons.ios_log_in), ) ], ), diff --git a/lib/ui/views/groups/add_assignment_view.dart b/lib/ui/views/groups/add_assignment_view.dart index 24a6cf42..beaa90c0 100644 --- a/lib/ui/views/groups/add_assignment_view.dart +++ b/lib/ui/views/groups/add_assignment_view.dart @@ -235,10 +235,16 @@ class _AddAssignmentViewState extends State { Get.back(result: _model.addedAssignment); // Show success snackbar.. - SnackBarUtils.showDark('Assignment Added'); + SnackBarUtils.showDark( + 'Assignment Added', + 'New assignment was successfully added.', + ); } else if (_model.isError(_model.ADD_ASSIGNMENT)) { // Show failure snackbar - SnackBarUtils.showDark(_model.errorMessageFor(_model.ADD_ASSIGNMENT)); + SnackBarUtils.showDark( + 'Error', + _model.errorMessageFor(_model.ADD_ASSIGNMENT), + ); _formKey.currentState.reset(); } } diff --git a/lib/ui/views/groups/assignment_details_view.dart b/lib/ui/views/groups/assignment_details_view.dart index c57ee07b..1ee9e6d3 100644 --- a/lib/ui/views/groups/assignment_details_view.dart +++ b/lib/ui/views/groups/assignment_details_view.dart @@ -260,9 +260,15 @@ class _AssignmentDetailsViewState extends State { _dialogService.popDialog(); if (_model.isSuccess(_model.ADD_GRADE)) { - SnackBarUtils.showDark('Project Graded Successfully'); + SnackBarUtils.showDark( + 'Project Graded Successfully', + 'You have graded the project.', + ); } else if (_model.isError(_model.ADD_GRADE)) { - SnackBarUtils.showDark(_model.errorMessageFor(_model.ADD_GRADE)); + SnackBarUtils.showDark( + 'Error', + _model.errorMessageFor(_model.ADD_GRADE), + ); } } @@ -278,9 +284,15 @@ class _AssignmentDetailsViewState extends State { _dialogService.popDialog(); if (_model.isSuccess(_model.UPDATE_GRADE)) { - SnackBarUtils.showDark('Grade updated Successfully'); + SnackBarUtils.showDark( + 'Grade updated Successfully', + 'Grade has been updated successfully.', + ); } else if (_model.isError(_model.UPDATE_GRADE)) { - SnackBarUtils.showDark(_model.errorMessageFor(_model.UPDATE_GRADE)); + SnackBarUtils.showDark( + 'Error', + _model.errorMessageFor(_model.UPDATE_GRADE), + ); } } @@ -299,11 +311,17 @@ class _AssignmentDetailsViewState extends State { _dialogService.popDialog(); if (_model.isSuccess(_model.DELETE_GRADE)) { - SnackBarUtils.showDark('Grade Deleted'); + SnackBarUtils.showDark( + 'Grade Deleted', + 'Grade has been removed successfully.', + ); _gradesController.clear(); _remarksController.clear(); } else if (_model.isError(_model.DELETE_GRADE)) { - SnackBarUtils.showDark(_model.errorMessageFor(_model.DELETE_GRADE)); + SnackBarUtils.showDark( + 'Error', + _model.errorMessageFor(_model.DELETE_GRADE), + ); } } } diff --git a/lib/ui/views/groups/edit_group_view.dart b/lib/ui/views/groups/edit_group_view.dart index 21c093c8..317f658c 100644 --- a/lib/ui/views/groups/edit_group_view.dart +++ b/lib/ui/views/groups/edit_group_view.dart @@ -41,9 +41,15 @@ class _EditGroupViewState extends State { if (_model.isSuccess(_model.UPDATE_GROUP)) { await Future.delayed(Duration(seconds: 1)); Get.back(result: _model.updatedGroup); - SnackBarUtils.showDark('Group Updated'); + SnackBarUtils.showDark( + 'Group Updated', + 'Group has been successfully updated.', + ); } else if (_model.isError(_model.UPDATE_GROUP)) { - SnackBarUtils.showDark(_model.errorMessageFor(_model.UPDATE_GROUP)); + SnackBarUtils.showDark( + 'Error', + _model.errorMessageFor(_model.UPDATE_GROUP), + ); } } } diff --git a/lib/ui/views/groups/group_details_view.dart b/lib/ui/views/groups/group_details_view.dart index 35b67624..b662a3fb 100644 --- a/lib/ui/views/groups/group_details_view.dart +++ b/lib/ui/views/groups/group_details_view.dart @@ -127,10 +127,15 @@ class _GroupDetailsViewState extends State { if (_model.isSuccess(_model.ADD_GROUP_MEMBERS) && _model.addedMembersSuccessMessage.isNotEmpty) { - SnackBarUtils.showDark(_model.addedMembersSuccessMessage); + SnackBarUtils.showDark( + 'Group Members Added', + _model.addedMembersSuccessMessage, + ); } else if (_model.isError(_model.ADD_GROUP_MEMBERS)) { SnackBarUtils.showDark( - _model.errorMessageFor(_model.ADD_GROUP_MEMBERS)); + 'Error', + _model.errorMessageFor(_model.ADD_GROUP_MEMBERS), + ); } } setState(() => _emails = null); @@ -205,10 +210,15 @@ class _GroupDetailsViewState extends State { _dialogService.popDialog(); if (_model.isSuccess(_model.DELETE_GROUP_MEMBER)) { - SnackBarUtils.showDark('Group Member Removed'); + SnackBarUtils.showDark( + 'Group Member Removed', + 'Successfully removed group member.', + ); } else if (_model.isError(_model.DELETE_GROUP_MEMBER)) { SnackBarUtils.showDark( - _model.errorMessageFor(_model.DELETE_GROUP_MEMBER)); + 'Error', + _model.errorMessageFor(_model.DELETE_GROUP_MEMBER), + ); } } } @@ -260,10 +270,15 @@ class _GroupDetailsViewState extends State { _dialogService.popDialog(); if (_model.isSuccess(_model.DELETE_ASSIGNMENT)) { - SnackBarUtils.showDark('Assignment Deleted'); + SnackBarUtils.showDark( + 'Assignment Deleted', + 'The assignment was successfully deleted.', + ); } else if (_model.isError(_model.DELETE_ASSIGNMENT)) { SnackBarUtils.showDark( - _model.errorMessageFor(_model.DELETE_ASSIGNMENT)); + 'Error', + _model.errorMessageFor(_model.DELETE_ASSIGNMENT), + ); } } } @@ -292,10 +307,15 @@ class _GroupDetailsViewState extends State { _dialogService.popDialog(); if (_model.isSuccess(_model.REOPEN_ASSIGNMENT)) { - SnackBarUtils.showDark('Assignent Reopened'); + SnackBarUtils.showDark( + 'Assignment Reopened', + 'The assignment is reopened now.', + ); } else if (_model.isError(_model.REOPEN_ASSIGNMENT)) { SnackBarUtils.showDark( - _model.errorMessageFor(_model.REOPEN_ASSIGNMENT)); + 'Error', + _model.errorMessageFor(_model.REOPEN_ASSIGNMENT), + ); } } } @@ -315,9 +335,15 @@ class _GroupDetailsViewState extends State { _dialogService.popDialog(); if (_model.isSuccess(_model.START_ASSIGNMENT)) { - SnackBarUtils.showDark('Project Created'); + SnackBarUtils.showDark( + 'Project Created', + 'Project is successfully created.', + ); } else { - SnackBarUtils.showDark(_model.errorMessageFor(_model.START_ASSIGNMENT)); + SnackBarUtils.showDark( + 'Error', + _model.errorMessageFor(_model.START_ASSIGNMENT), + ); } } } diff --git a/lib/ui/views/groups/my_groups_view.dart b/lib/ui/views/groups/my_groups_view.dart index 0730a71a..bf6e0ef7 100644 --- a/lib/ui/views/groups/my_groups_view.dart +++ b/lib/ui/views/groups/my_groups_view.dart @@ -63,9 +63,15 @@ class _MyGroupsViewState extends State { _dialogService.popDialog(); if (_model.isSuccess(_model.DELETE_GROUP)) { - SnackBarUtils.showDark('Group Deleted'); + SnackBarUtils.showDark( + 'Group Deleted', + 'Group was successfully deleted.', + ); } else if (_model.isError(_model.DELETE_GROUP)) { - SnackBarUtils.showDark(_model.errorMessageFor(_model.DELETE_GROUP)); + SnackBarUtils.showDark( + 'Error', + _model.errorMessageFor(_model.DELETE_GROUP), + ); } } } diff --git a/lib/ui/views/groups/new_group_view.dart b/lib/ui/views/groups/new_group_view.dart index 27cf596f..0a160931 100644 --- a/lib/ui/views/groups/new_group_view.dart +++ b/lib/ui/views/groups/new_group_view.dart @@ -37,9 +37,15 @@ class _NewGroupViewState extends State { if (_model.isSuccess(_model.ADD_GROUP)) { await Future.delayed(Duration(seconds: 1)); Get.back(result: _model.newGroup); - SnackBarUtils.showDark('Group Created'); + SnackBarUtils.showDark( + 'Group Created', + 'New group was created successfully.', + ); } else if (_model.isError(_model.ADD_GROUP)) { - SnackBarUtils.showDark(_model.errorMessageFor(_model.ADD_GROUP)); + SnackBarUtils.showDark( + 'Error', + _model.errorMessageFor(_model.ADD_GROUP), + ); } } } diff --git a/lib/ui/views/groups/update_assignment_view.dart b/lib/ui/views/groups/update_assignment_view.dart index ac93b055..8079a675 100644 --- a/lib/ui/views/groups/update_assignment_view.dart +++ b/lib/ui/views/groups/update_assignment_view.dart @@ -209,10 +209,15 @@ class _UpdateAssignmentViewState extends State { if (_model.isSuccess(_model.UPDATE_ASSIGNMENT)) { await Future.delayed(Duration(seconds: 1)); Get.back(result: _model.updatedAssignment); - SnackBarUtils.showDark('Assignment Updated'); + SnackBarUtils.showDark( + 'Assignment Updated', + 'Assignment was updated successfully', + ); } else if (_model.isError(_model.UPDATE_ASSIGNMENT)) { SnackBarUtils.showDark( - _model.errorMessageFor(_model.UPDATE_ASSIGNMENT)); + 'Error', + _model.errorMessageFor(_model.UPDATE_ASSIGNMENT), + ); } } } diff --git a/lib/ui/views/ib/builders/ib_chapter_contents_builder.dart b/lib/ui/views/ib/builders/ib_chapter_contents_builder.dart new file mode 100644 index 00000000..2c3d252a --- /dev/null +++ b/lib/ui/views/ib/builders/ib_chapter_contents_builder.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:markdown/markdown.dart' as md; + +class IbChapterContentsBuilder extends MarkdownElementBuilder { + final Widget chapterContents; + + IbChapterContentsBuilder({this.chapterContents}); + + @override + Widget visitElementAfter(md.Element element, TextStyle preferredStyle) { + return chapterContents; + } +} diff --git a/lib/ui/views/ib/builders/ib_interaction_builder.dart b/lib/ui/views/ib/builders/ib_interaction_builder.dart new file mode 100644 index 00000000..7cfc66c5 --- /dev/null +++ b/lib/ui/views/ib/builders/ib_interaction_builder.dart @@ -0,0 +1,59 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:markdown/markdown.dart' as md; +import 'package:mobile_app/viewmodels/ib/ib_page_viewmodel.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class IbInteractionBuilder extends MarkdownElementBuilder { + final BuildContext context; + final IbPageViewModel model; + + IbInteractionBuilder({this.context, this.model}); + + @override + Widget visitElementAfter(md.Element element, TextStyle preferredStyle) { + var id = element.textContent; + + return FutureBuilder( + future: model.fetchHtmlInteraction(id), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (model.isError(model.IB_FETCH_INTERACTION_DATA)) { + return Text('Error Loading Interaction'); + } else if (model.isBusy(model.IB_FETCH_INTERACTION_DATA) || + !snapshot.hasData) { + return Text('Loading Interaction...'); + } + + var _textContent = snapshot.data.toString(); + var _streamController = StreamController(); + WebViewController _webViewController; + + return StreamBuilder( + initialData: 100, + stream: _streamController.stream, + builder: (context, snapshot) { + return Container( + height: snapshot.data, + child: WebView( + initialUrl: + 'data:text/html;base64,${base64Encode(const Utf8Encoder().convert(_textContent))}', + onPageFinished: (some) async { + var height = double.parse( + await _webViewController.evaluateJavascript( + 'document.documentElement.scrollHeight;')); + _streamController.add(height); + }, + onWebViewCreated: (_controller) { + _webViewController = _controller; + }, + javascriptMode: JavascriptMode.unrestricted, + ), + ); + }); + }, + ); + } +} diff --git a/lib/ui/views/ib/builders/ib_webview_builder.dart b/lib/ui/views/ib/builders/ib_webview_builder.dart new file mode 100644 index 00000000..0e16a56f --- /dev/null +++ b/lib/ui/views/ib/builders/ib_webview_builder.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_html/html_parser.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:markdown/markdown.dart' as md; + +class IbWebViewBuilder extends MarkdownElementBuilder { + final BuildContext context; + + IbWebViewBuilder({this.context}); + + @override + Widget visitElementAfter(md.Element element, TextStyle preferredStyle) { + var textContent = element.textContent; + + return Html( + data: textContent, + customRender: { + 'iframe': (RenderContext context, Widget child) { + final width = MediaQuery.of(context.buildContext).size.width; + final height = (width * 9) / 16; + + return SizedBox( + width: width, + height: height, + child: WebView( + initialUrl: context.tree.element.attributes['src'], + javascriptMode: JavascriptMode.unrestricted, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ); + }, + }, + ); + } +} diff --git a/lib/ui/views/ib/ib_landing_view.dart b/lib/ui/views/ib/ib_landing_view.dart new file mode 100644 index 00000000..a091f51c --- /dev/null +++ b/lib/ui/views/ib/ib_landing_view.dart @@ -0,0 +1,248 @@ +import 'package:animations/animations.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:mobile_app/ib_theme.dart'; +import 'package:mobile_app/models/ib/ib_chapter.dart'; +import 'package:mobile_app/ui/components/cv_drawer_tile.dart'; +import 'package:mobile_app/ui/views/base_view.dart'; +import 'package:mobile_app/ui/views/ib/ib_page_view.dart'; +import 'package:mobile_app/viewmodels/ib/ib_landing_viewmodel.dart'; +import 'package:theme_provider/theme_provider.dart'; + +class IbLandingView extends StatefulWidget { + static const String id = 'ib_landing_view'; + + @override + _IbLandingViewState createState() => _IbLandingViewState(); +} + +class _IbLandingViewState extends State { + final IbChapter _homeChapter = IbChapter( + id: 'index.md', + navOrder: '1', + value: 'Interactive Book Home', + ); + IbChapter _selectedChapter; + ValueNotifier _tocNotifier; + + @override + void initState() { + _tocNotifier = ValueNotifier(null); + _selectedChapter = _homeChapter; + super.initState(); + } + + @override + void dispose() { + _tocNotifier.dispose(); + super.dispose(); + } + + void setSelectedChapter(IbChapter chapter) { + Get.back(); + if (_selectedChapter.id != chapter.id) { + setState(() => _selectedChapter = chapter); + } + } + + Widget _buildAppBar() { + return AppBar( + title: Text( + _selectedChapter.id == _homeChapter.id + ? 'CircuitVerse' + : 'Interactive Book', + ), + actions: [ + ValueListenableBuilder( + valueListenable: _tocNotifier, + builder: (context, value, child) { + return value != null + ? IconButton( + icon: const Icon(Icons.menu_book_rounded), + tooltip: 'Show Table of Contents', + onPressed: value, + ) + : Container(); + }, + ), + ], + centerTitle: true, + brightness: Brightness.dark, + ); + } + + Widget _buildChapter(IbChapter chapter) { + return InkWell( + onTap: () => setSelectedChapter(chapter), + child: CVDrawerTile( + title: chapter.value, + color: (_selectedChapter.id == chapter.id) + ? IbTheme.getPrimaryColor(context) + : IbTheme.textColor(context), + ), + ); + } + + Widget _buildExpandableChapter(IbChapter chapter) { + var nestedPages = []; + + var hasSelectedChapter = false; + for (var nestedChapter in chapter.items) { + if (nestedChapter.id == _selectedChapter.id) { + hasSelectedChapter = true; + } + + nestedPages.add(_buildChapter(nestedChapter)); + } + + return ExpansionTile( + maintainState: true, + initiallyExpanded: + (_selectedChapter.id.startsWith(chapter.id) || hasSelectedChapter) + ? true + : false, + title: ListTile( + contentPadding: EdgeInsets.all(0), + title: GestureDetector( + onTap: () => setSelectedChapter(chapter), + child: Text( + chapter.value, + style: Theme.of(context).textTheme.headline6.copyWith( + fontFamily: 'Poppins', + color: (_selectedChapter.id.startsWith(chapter.id)) + ? IbTheme.getPrimaryColor(context) + : IbTheme.textColor(context), + ), + ), + ), + ), + children: nestedPages, + ); + } + + Widget _buildChapters(List chapters) { + var _chapters = []; + + for (var chapter in chapters) { + if (chapter.items != null && chapter.items.isNotEmpty) { + _chapters.add(_buildExpandableChapter(chapter)); + } else { + _chapters.add(_buildChapter(chapter)); + } + } + + return Column(children: _chapters); + } + + Widget _buildDrawer(IbLandingViewModel _model) { + return Drawer( + child: Stack( + children: [ + ListView( + children: [ + InkWell( + onTap: () { + Get.back(); + Get.back(); + }, + child: CVDrawerTile(title: 'Return to Home'), + ), + Divider(thickness: 1), + InkWell( + onTap: () => setSelectedChapter(_homeChapter), + child: CVDrawerTile( + title: 'Interactive Book Home', + color: (_selectedChapter.id == _homeChapter.id) + ? IbTheme.getPrimaryColor(context) + : IbTheme.textColor(context), + ), + ), + !_model.isSuccess(_model.IB_FETCH_CHAPTERS) + ? InkWell( + child: CVDrawerTile( + title: 'Loading...', + ), + ) + : _buildChapters(_model.chapters), + ], + ), + Positioned( + right: 5, + top: 27, + child: IconButton( + icon: Theme.of(context).brightness == Brightness.dark + ? const Icon(Icons.brightness_low) + : const Icon(Icons.brightness_high), + iconSize: 28.0, + onPressed: () { + if (ThemeProvider != null) { + ThemeProvider.controllerOf(context).nextTheme(); + } + }, + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return BaseView( + onModelReady: (model) => model.fetchChapters(), + builder: (context, model, child) { + // Set next page for home page + if (model.isSuccess(model.IB_FETCH_CHAPTERS) && + _homeChapter.next == null) { + _homeChapter.nextPage = model.chapters[0]; + model.chapters[0].prev = _homeChapter; + } + + return WillPopScope( + onWillPop: () { + if (_selectedChapter != _homeChapter) { + setState(() => _selectedChapter = _homeChapter); + return Future.value(false); + } + return Future.value(true); + }, + child: Theme( + data: IbTheme.getThemeData(context), + child: Scaffold( + key: Key('IbLandingScaffold'), + appBar: _buildAppBar(), + drawer: _buildDrawer(model), + body: PageTransitionSwitcher( + transitionBuilder: ( + Widget child, + Animation animation, + Animation secondaryAnimation, + ) { + return FadeThroughTransition( + animation: animation, + secondaryAnimation: secondaryAnimation, + child: child, + ); + }, + child: IbPageView( + key: Key(_selectedChapter.toString()), + tocCallback: (val) { + Future.delayed(Duration.zero, () async { + if (mounted) { + _tocNotifier.value = val; + } + }); + }, + setPage: (chapter) { + setState(() => _selectedChapter = chapter); + }, + chapter: _selectedChapter, + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/ui/views/ib/ib_page_view.dart b/lib/ui/views/ib/ib_page_view.dart new file mode 100644 index 00000000..139639f7 --- /dev/null +++ b/lib/ui/views/ib/ib_page_view.dart @@ -0,0 +1,391 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:markdown/markdown.dart' as md; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:mobile_app/config/environment_config.dart'; +import 'package:mobile_app/ib_theme.dart'; +import 'package:mobile_app/models/ib/ib_chapter.dart'; +import 'package:mobile_app/models/ib/ib_content.dart'; +import 'package:mobile_app/models/ib/ib_page_data.dart'; +import 'package:mobile_app/ui/views/base_view.dart'; +import 'package:mobile_app/ui/views/ib/builders/ib_chapter_contents_builder.dart'; +import 'package:mobile_app/ui/views/ib/builders/ib_interaction_builder.dart'; +import 'package:mobile_app/ui/views/ib/builders/ib_webview_builder.dart'; +import 'package:mobile_app/ui/views/ib/syntaxes/ib_embed_syntax.dart'; +import 'package:mobile_app/ui/views/ib/syntaxes/ib_filter_syntax.dart'; +import 'package:mobile_app/ui/views/ib/syntaxes/ib_liquid_syntax.dart'; +import 'package:mobile_app/ui/views/ib/syntaxes/ib_md_tag_syntax.dart'; +import 'package:mobile_app/utils/url_launcher.dart'; +import 'package:mobile_app/viewmodels/ib/ib_page_viewmodel.dart'; +import 'package:url_launcher/url_launcher.dart'; + +typedef TocCallback = void Function(Function); +typedef SetPageCallback = void Function(IbChapter); + +class IbPageView extends StatefulWidget { + static const String id = 'ib_page_view'; + final TocCallback tocCallback; + final SetPageCallback setPage; + final IbChapter chapter; + + IbPageView({ + @required Key key, + @required this.tocCallback, + @required this.chapter, + @required this.setPage, + }) : super(key: key); + + @override + _IbPageViewState createState() => _IbPageViewState(); +} + +class _IbPageViewState extends State { + IbPageViewModel _model; + ScrollController _hideButtonController; + bool _isFabsVisible = true; + + @override + void initState() { + super.initState(); + _isFabsVisible = true; + _hideButtonController = ScrollController(); + _hideButtonController.addListener(() { + if (_hideButtonController.position.userScrollDirection == + ScrollDirection.reverse) { + setState(() => _isFabsVisible = false); + } + if (_hideButtonController.position.userScrollDirection == + ScrollDirection.forward) { + setState(() => _isFabsVisible = true); + } + }); + } + + Widget _buildDivider() { + return Padding( + padding: EdgeInsets.symmetric(vertical: 10), + child: Divider( + thickness: 1.5, + ), + ); + } + + void _onTapLink(String text, String href, String title) async { + // Confirm if it's a valid URL + if (!(await canLaunch(href))) { + print('[IB]: $href is not a valid link'); + return; + } + + // If Interactive Book link + if (href.startsWith(EnvironmentConfig.IB_BASE_URL)) { + // If URI is same as the current page + if (_model.pageData.pageUrl.startsWith(href)) { + // It's local link + // (TODO) Scroll to that local widget + return; + } else { + // Try to navigate to another page using url + // (TODO) We need [IbLandingViewModel] to be able to get Chapter using [httpUrl] + return; + } + } + + launchURL(href); + } + + Widget _buildMarkdown(IbMd data) { + return MarkdownBody( + data: data.content, + selectable: true, + imageDirectory: EnvironmentConfig.IB_BASE_URL, + onTapLink: _onTapLink, + blockBuilders: { + 'chapter_contents': IbChapterContentsBuilder( + chapterContents: _model.pageData?.chapterOfContents?.isNotEmpty ?? + false + ? _buildTOC(_model.pageData.chapterOfContents, padding: false) + : Container()), + 'iframe': IbWebViewBuilder(context: context), + 'interaction': IbInteractionBuilder(model: _model), + }, + extensionSet: md.ExtensionSet( + [ + IbEmbedSyntax(), + IbFilterSyntax(), + IbMdTagSyntax(), + IbLiquidSyntax(), + ...md.ExtensionSet.gitHubFlavored.blockSyntaxes + ], + [md.EmojiSyntax(), ...md.ExtensionSet.gitHubFlavored.inlineSyntaxes], + ), + styleSheet: MarkdownStyleSheet( + h1: Theme.of(context).textTheme.headline4.copyWith( + color: IbTheme.primaryHeadingColor(context), + fontWeight: FontWeight.w300, + ), + h2: Theme.of(context).textTheme.headline5.copyWith( + color: IbTheme.primaryHeadingColor(context), + fontWeight: FontWeight.w600, + ), + h3: Theme.of(context).textTheme.headline6.copyWith( + color: IbTheme.primaryHeadingColor(context), + fontWeight: FontWeight.w600, + ), + h4: Theme.of(context).textTheme.subtitle1.copyWith( + color: IbTheme.primaryHeadingColor(context), + fontWeight: FontWeight.w600, + ), + h5: Theme.of(context) + .textTheme + .headline6 + .copyWith(fontWeight: FontWeight.w300), + horizontalRuleDecoration: BoxDecoration( + border: Border( + top: BorderSide( + width: 1.5, + color: Theme.of(context).dividerColor, + ), + ), + ), + ), + ); + } + + Widget _buildFooter() { + return Padding( + padding: EdgeInsets.symmetric(vertical: 4), + child: Text( + 'Copyright © 2021 Contributors to CircuitVerse. Distributed under a [CC-by-sa] license.', + style: TextStyle( + fontSize: 10, + ), + ), + ); + } + + Widget _buildTocListTile(String content, + {bool root = true, bool padding = true}) { + if (!root) { + return ListTile( + leading: Text(''), + visualDensity: !padding ? VisualDensity(vertical: -3) : null, + contentPadding: EdgeInsets.symmetric(horizontal: padding ? 16.0 : 0.0), + minLeadingWidth: 20, + title: Text(content), + ); + } + + return ListTile( + visualDensity: !padding ? VisualDensity(vertical: -3) : null, + contentPadding: EdgeInsets.symmetric(horizontal: padding ? 16.0 : 0.0), + title: Text(content), + ); + } + + List _buildTocItems(IbTocItem item, + {bool root = false, bool padding = true}) { + var items = [ + _buildTocListTile( + item.content, + root: root, + padding: padding, + ), + ]; + + if (item.items != null) { + for (var e in item.items) { + items.addAll( + _buildTocItems( + e, + padding: padding, + ), + ); + } + } + + return items; + } + + Widget _buildTOC(List toc, {bool padding = true}) { + var items = []; + + for (var item in toc) { + items.addAll( + _buildTocItems( + item, + root: true, + padding: padding, + ), + ); + } + + return Column( + children: items, + ); + } + + void _showBottomSheet() { + showModalBottomSheet( + context: context, + builder: (context) { + return Column( + children: [ + ListTile( + title: Text( + 'Table of Contents', + style: Theme.of(context) + .textTheme + .subtitle1 + .copyWith(color: Theme.of(context).colorScheme.onPrimary), + ), + tileColor: Theme.of(context).primaryColor, + ), + Expanded( + child: SingleChildScrollView( + child: _buildTOC(_model.pageData.tableOfContents), + ), + ), + ], + ); + }, + ); + } + + Widget _buildFloatingActionButtons() { + var alignment = MainAxisAlignment.spaceBetween; + var buttons = []; + + if (widget.chapter.prev != null) { + if (widget.chapter.next == null) { + alignment = MainAxisAlignment.start; + } + + buttons.add( + AnimatedOpacity( + duration: Duration(milliseconds: 500), + opacity: _isFabsVisible ? 1.0 : 0.0, + child: FloatingActionButton( + heroTag: 'previousPage', + mini: true, + backgroundColor: Theme.of(context).primaryIconTheme.color, + onPressed: () => widget.setPage(widget.chapter.prev), + child: Icon( + Icons.arrow_back_rounded, + color: IbTheme.primaryColor, + ), + ), + ), + ); + } + + if (widget.chapter.next != null) { + if (widget.chapter.prev == null) { + alignment = MainAxisAlignment.end; + } + + buttons.add( + AnimatedOpacity( + duration: Duration(milliseconds: 500), + opacity: _isFabsVisible ? 1.0 : 0.0, + child: FloatingActionButton( + heroTag: 'nextPage', + mini: true, + backgroundColor: Theme.of(context).primaryIconTheme.color, + onPressed: () => widget.setPage(widget.chapter.next), + child: Icon( + Icons.arrow_forward_rounded, + color: IbTheme.primaryColor, + ), + ), + ), + ); + } + + return Row( + mainAxisAlignment: alignment, + children: buttons, + ); + } + + List _buildPageContent(IbPageData pageData) { + if (pageData == null) { + return [ + Text( + 'Loading ...', + style: Theme.of(context).textTheme.headline6.copyWith( + color: IbTheme.primaryHeadingColor(context), + fontWeight: FontWeight.w600, + ), + ), + ]; + } + + var contents = []; + + for (var content in pageData.content) { + switch (content.runtimeType) { + case IbMd: + contents.add(_buildMarkdown(content as IbMd)); + break; + } + } + + contents.addAll([ + _buildDivider(), + _buildFooter(), + ]); + + return contents; + } + + @override + void dispose() { + _hideButtonController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BaseView( + onModelReady: (model) { + _model = model; + model.fetchPageData(id: widget.chapter.id); + }, + builder: (context, model, child) { + // Set the callback to show bottom sheet for Table of Contents + if (_model.isSuccess(_model.IB_FETCH_PAGE_DATA) && + (model.pageData?.tableOfContents?.isNotEmpty ?? false)) { + widget.tocCallback(_showBottomSheet); + } else { + widget.tocCallback(null); + } + + return Stack( + children: [ + Scrollbar( + controller: _hideButtonController, + child: SingleChildScrollView( + controller: _hideButtonController, + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildPageContent(model.pageData), + ), + ), + ), + widget.chapter.prev != null || widget.chapter.next != null + ? Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: _buildFloatingActionButtons(), + ), + ) + : Container(), + ], + ); + }, + ); + } +} diff --git a/lib/ui/views/ib/syntaxes/ib_embed_syntax.dart b/lib/ui/views/ib/syntaxes/ib_embed_syntax.dart new file mode 100644 index 00000000..7cb5ea00 --- /dev/null +++ b/lib/ui/views/ib/syntaxes/ib_embed_syntax.dart @@ -0,0 +1,16 @@ +import 'package:markdown/markdown.dart' as md; + +class IbEmbedSyntax extends md.BlockSyntax { + IbEmbedSyntax() : super(); + + @override + md.Node parse(md.BlockParser parser) { + var text = parser.current; + parser.advance(); + + return md.Element.text('iframe', text); + } + + @override + RegExp get pattern => RegExp(r'^.+<\/iframe>'); +} diff --git a/lib/ui/views/ib/syntaxes/ib_filter_syntax.dart b/lib/ui/views/ib/syntaxes/ib_filter_syntax.dart new file mode 100644 index 00000000..8bc366cd --- /dev/null +++ b/lib/ui/views/ib/syntaxes/ib_filter_syntax.dart @@ -0,0 +1,14 @@ +import 'package:markdown/markdown.dart' as md; + +class IbFilterSyntax extends md.BlockSyntax { + IbFilterSyntax() : super(); + + @override + md.Node parse(md.BlockParser parser) { + parser.advance(); + return null; + } + + @override + RegExp get pattern => RegExp(r'^(##\sTable\s[oO]f\s[cC]ontents)|(1.\sTOC)'); +} diff --git a/lib/ui/views/ib/syntaxes/ib_liquid_syntax.dart b/lib/ui/views/ib/syntaxes/ib_liquid_syntax.dart new file mode 100644 index 00000000..2bb89a05 --- /dev/null +++ b/lib/ui/views/ib/syntaxes/ib_liquid_syntax.dart @@ -0,0 +1,39 @@ +import 'package:markdown/markdown.dart' as md; +import 'package:mobile_app/config/environment_config.dart'; + +class IbLiquidSyntax extends md.BlockSyntax { + IbLiquidSyntax() : super(); + + @override + md.Node parse(md.BlockParser parser) { + var match = pattern.firstMatch(parser.current); + var tags = match[1].split(' '); + var node; + + // Liquid include tags + if (tags[0] == 'include') { + // chapter_toc include + if (tags[1] == 'chapter_toc.html') { + node = md.Element.text('chapter_contents', ''); + } else if (tags[1] == 'image.html' && tags.length >= 3) { + // Images + var url = RegExp('url="([^"\n\r]+)"').firstMatch(match[1])[1]; + var alt = RegExp('description="([^"\n\r]+)"').firstMatch(match[1])[1]; + + node = md.Element.withTag('img'); + node.attributes['src'] = '${EnvironmentConfig.IB_BASE_URL}$url'; + node.attributes['alt'] = alt; + } else { + // Interactions using html + node = md.Element.text('interaction', tags[1]); + } + } + + parser.advance(); + + return node; + } + + @override + RegExp get pattern => RegExp(r'{%\s?(.+)\s?%}'); +} diff --git a/lib/ui/views/ib/syntaxes/ib_md_tag_syntax.dart b/lib/ui/views/ib/syntaxes/ib_md_tag_syntax.dart new file mode 100644 index 00000000..458e6e06 --- /dev/null +++ b/lib/ui/views/ib/syntaxes/ib_md_tag_syntax.dart @@ -0,0 +1,44 @@ +import 'package:markdown/markdown.dart' as md; + +class IbMdTagSyntax extends md.BlockSyntax { + final _tagsStack = []; + IbMdTagSyntax() : super(); + + @override + md.Node parse(md.BlockParser parser) { + var match = pattern.firstMatch(parser.current); + _tagsStack.addAll(match[1].split(' ')); + + // Subtitle Syntax + // This is a temporary workaround for subtitle + // Since Markdown tags after text is not supported + if (_tagsStack.contains('.fs-9')) { + parser.advance(); + parser.advance(); + var text = parser.current; + + _tagsStack.remove('.fs-9'); + parser.advance(); + return md.Element.text('h5', text); + } + + // (TODO) Fix Pop Quizes + if (_tagsStack.contains('.quiz')) { + // Ignore parsing quiz content + do { + parser.advance(); + } while (!parser.isDone); + + _tagsStack.remove('.quiz'); + + return null; + } + + parser.advance(); + + return null; + } + + @override + RegExp get pattern => RegExp(r'{:\s?(.+)\s?}'); +} diff --git a/lib/ui/views/profile/edit_profile_view.dart b/lib/ui/views/profile/edit_profile_view.dart index b8643783..2df57204 100644 --- a/lib/ui/views/profile/edit_profile_view.dart +++ b/lib/ui/views/profile/edit_profile_view.dart @@ -111,9 +111,15 @@ class _EditProfileViewState extends State { if (_model.isSuccess(_model.UPDATE_PROFILE)) { await Future.delayed(Duration(seconds: 1)); Get.back(result: _model.updatedUser); - SnackBarUtils.showDark('Profile Updated'); + SnackBarUtils.showDark( + 'Profile Updated', + 'Your profile was successfully updated.', + ); } else if (_model.isError(_model.UPDATE_PROFILE)) { - SnackBarUtils.showDark(_model.errorMessageFor(_model.UPDATE_PROFILE)); + SnackBarUtils.showDark( + 'Error', + _model.errorMessageFor(_model.UPDATE_PROFILE), + ); } } } diff --git a/lib/ui/views/projects/edit_project_view.dart b/lib/ui/views/projects/edit_project_view.dart index 0b132b38..ac959cb0 100644 --- a/lib/ui/views/projects/edit_project_view.dart +++ b/lib/ui/views/projects/edit_project_view.dart @@ -130,9 +130,15 @@ class _EditProjectViewState extends State { if (_model.isSuccess(_model.UPDATE_PROJECT)) { await Future.delayed(Duration(seconds: 1)); Get.back(result: _model.updatedProject); - SnackBarUtils.showDark('Project Updated'); + SnackBarUtils.showDark( + 'Project Updated', + 'The project was successfully updated.', + ); } else if (_model.isError(_model.UPDATE_PROJECT)) { - SnackBarUtils.showDark(_model.errorMessageFor(_model.UPDATE_PROJECT)); + SnackBarUtils.showDark( + 'Error', + _model.errorMessageFor(_model.UPDATE_PROJECT), + ); } } } diff --git a/lib/ui/views/projects/project_details_view.dart b/lib/ui/views/projects/project_details_view.dart index 1bfcc95d..72770e90 100644 --- a/lib/ui/views/projects/project_details_view.dart +++ b/lib/ui/views/projects/project_details_view.dart @@ -235,7 +235,10 @@ class _ProjectDetailsViewState extends State { await Get.toNamed(ProjectDetailsView.id, arguments: _model.forkedProject); } else if (_model.isError(_model.FORK_PROJECT)) { - SnackBarUtils.showDark(_model.errorMessageFor(_model.FORK_PROJECT)); + SnackBarUtils.showDark( + 'Error', + _model.errorMessageFor(_model.FORK_PROJECT), + ); } } } @@ -271,9 +274,14 @@ class _ProjectDetailsViewState extends State { if (_model.isSuccess(_model.TOGGLE_STAR)) { SnackBarUtils.showDark( - 'Project ${_model.isProjectStarred ? 'Starred' : 'Unstarred'}'); + 'Project ${_model.isProjectStarred ? 'Starred' : 'Unstarred'}', + 'You have successfully ${_model.isProjectStarred ? 'stared' : 'unstarred'} the project', + ); } else if (_model.isError(_model.TOGGLE_STAR)) { - SnackBarUtils.showDark(_model.errorMessageFor(_model.TOGGLE_STAR)); + SnackBarUtils.showDark( + 'Error', + _model.errorMessageFor(_model.TOGGLE_STAR), + ); } } @@ -307,10 +315,15 @@ class _ProjectDetailsViewState extends State { if (_model.isSuccess(_model.ADD_COLLABORATORS) && _model.addedCollaboratorsSuccessMessage.isNotEmpty) { - SnackBarUtils.showDark(_model.addedCollaboratorsSuccessMessage); + SnackBarUtils.showDark( + 'Collaborators Added', + _model.addedCollaboratorsSuccessMessage, + ); } else if (_model.isError(_model.ADD_COLLABORATORS)) { SnackBarUtils.showDark( - _model.errorMessageFor(_model.ADD_COLLABORATORS)); + 'Error', + _model.errorMessageFor(_model.ADD_COLLABORATORS), + ); } } } @@ -433,9 +446,15 @@ class _ProjectDetailsViewState extends State { if (_model.isSuccess(_model.DELETE_PROJECT)) { Get.back(result: true); - SnackBarUtils.showDark('Project Deleted'); + SnackBarUtils.showDark( + 'Project Deleted', + 'Project is successfully deleted.', + ); } else if (_model.isError(_model.DELETE_PROJECT)) { - SnackBarUtils.showDark(_model.errorMessageFor(_model.DELETE_PROJECT)); + SnackBarUtils.showDark( + 'Error', + _model.errorMessageFor(_model.DELETE_PROJECT), + ); } } } @@ -476,10 +495,15 @@ class _ProjectDetailsViewState extends State { _dialogService.popDialog(); if (_model.isSuccess(_model.DELETE_COLLABORATORS)) { - SnackBarUtils.showDark('Collaborator Deleted'); + SnackBarUtils.showDark( + 'Collaborator Deleted', + 'Collaborator was successfully deleted.', + ); } else if (_model.isError(_model.DELETE_COLLABORATORS)) { SnackBarUtils.showDark( - _model.errorMessageFor(_model.DELETE_COLLABORATORS)); + 'Error', + _model.errorMessageFor(_model.DELETE_COLLABORATORS), + ); } } } diff --git a/lib/utils/api_utils.dart b/lib/utils/api_utils.dart index 643902df..4de52920 100644 --- a/lib/utils/api_utils.dart +++ b/lib/utils/api_utils.dart @@ -12,10 +12,19 @@ class ApiUtils { static http.Client client = http.Client(); /// Returns JSON GET response - static Future get(String uri, {Map headers}) async { + static Future get(String uri, + {Map headers, + bool utfDecoder = false, + bool rawResponse = false}) async { try { - final response = await client.get(uri, headers: headers); - final jsonResponse = ApiUtils.jsonResponse(response); + final response = await client.get(Uri.parse(uri), headers: headers); + + if (rawResponse) { + return response.body; + } + + final jsonResponse = + ApiUtils.jsonResponse(response, utfDecoder: utfDecoder); return jsonResponse; } on SocketException { throw Failure(Constants.NO_INTERNET_CONNECTION); @@ -28,8 +37,8 @@ class ApiUtils { static Future post(String uri, {Map headers, dynamic body}) async { try { - final response = - await client.post(uri, headers: headers, body: jsonEncode(body)); + final response = await client.post(Uri.parse(uri), + headers: headers, body: jsonEncode(body)); final jsonResponse = ApiUtils.jsonResponse(response); return jsonResponse; } on SocketException { @@ -44,7 +53,7 @@ class ApiUtils { {Map headers, dynamic body}) async { try { final response = await client.put( - uri, + Uri.parse(uri), headers: headers, body: jsonEncode(body), ); @@ -61,8 +70,11 @@ class ApiUtils { static Future patch(String uri, {Map headers, dynamic body}) async { try { - final response = - await client.patch(uri, headers: headers, body: jsonEncode(body)); + final response = await client.patch( + Uri.parse(uri), + headers: headers, + body: jsonEncode(body), + ); final jsonResponse = ApiUtils.jsonResponse(response); return jsonResponse; } on SocketException { @@ -76,7 +88,10 @@ class ApiUtils { static Future delete(String uri, {Map headers}) async { try { - final response = await client.delete(uri, headers: headers); + final response = await client.delete( + Uri.parse(uri), + headers: headers, + ); final jsonResponse = ApiUtils.jsonResponse(response); return jsonResponse; } on SocketException { @@ -86,14 +101,17 @@ class ApiUtils { } } - static dynamic jsonResponse(http.Response response) { + static dynamic jsonResponse(http.Response response, + {bool utfDecoder = false}) { switch (response.statusCode) { case 200: case 201: case 202: case 204: - var responseJson = - response.body == '' ? {} : json.decode(response.body); + var responseJson = response.body == '' + ? {} + : json.decode( + utfDecoder ? utf8.decode(response.bodyBytes) : response.body); return responseJson; case 400: throw BadRequestException(response.body); diff --git a/lib/utils/router.dart b/lib/utils/router.dart index a1dc7161..f2bcbe39 100644 --- a/lib/utils/router.dart +++ b/lib/utils/router.dart @@ -17,6 +17,7 @@ import 'package:mobile_app/ui/views/groups/my_groups_view.dart'; import 'package:mobile_app/ui/views/groups/new_group_view.dart'; import 'package:mobile_app/ui/views/groups/update_assignment_view.dart'; import 'package:mobile_app/ui/views/cv_landing_view.dart'; +import 'package:mobile_app/ui/views/ib/ib_landing_view.dart'; import 'package:mobile_app/ui/views/profile/edit_profile_view.dart'; import 'package:mobile_app/ui/views/profile/profile_view.dart'; import 'package:mobile_app/ui/views/projects/edit_project_view.dart'; @@ -107,6 +108,8 @@ class CVRouter { assignment: _assignment, ), ); + case IbLandingView.id: + return MaterialPageRoute(builder: (_) => IbLandingView()); default: return MaterialPageRoute( builder: (_) => Scaffold( diff --git a/lib/utils/snackbar_utils.dart b/lib/utils/snackbar_utils.dart index fe97e4b3..aece35c9 100644 --- a/lib/utils/snackbar_utils.dart +++ b/lib/utils/snackbar_utils.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; class SnackBarUtils { - static void showLight(String message, {String title}) { + static void showLight(String title, String message) { Get.snackbar( title, message, @@ -10,7 +10,7 @@ class SnackBarUtils { ); } - static void showDark(String message, {String title}) { + static void showDark(String title, String message) { Get.snackbar( title, message, diff --git a/lib/viewmodels/ib/ib_landing_viewmodel.dart b/lib/viewmodels/ib/ib_landing_viewmodel.dart new file mode 100644 index 00000000..6beb0a40 --- /dev/null +++ b/lib/viewmodels/ib/ib_landing_viewmodel.dart @@ -0,0 +1,27 @@ +import 'package:mobile_app/enums/view_state.dart'; +import 'package:mobile_app/locator.dart'; +import 'package:mobile_app/models/failure_model.dart'; +import 'package:mobile_app/models/ib/ib_chapter.dart'; +import 'package:mobile_app/services/ib_engine_service.dart'; +import 'package:mobile_app/viewmodels/base_viewmodel.dart'; + +class IbLandingViewModel extends BaseModel { + // ViewState Keys + String IB_FETCH_CHAPTERS = 'ib_fetch_chapters'; + + final IbEngineService _ibEngineService = locator(); + + List _chapters = []; + + List get chapters => _chapters; + + Future fetchChapters() async { + try { + _chapters = await _ibEngineService.getChapters(); + setStateFor(IB_FETCH_CHAPTERS, ViewState.Success); + } on Failure catch (f) { + setStateFor(IB_FETCH_CHAPTERS, ViewState.Error); + setErrorMessageFor(IB_FETCH_CHAPTERS, f.message); + } + } +} diff --git a/lib/viewmodels/ib/ib_page_viewmodel.dart b/lib/viewmodels/ib/ib_page_viewmodel.dart new file mode 100644 index 00000000..32378eb8 --- /dev/null +++ b/lib/viewmodels/ib/ib_page_viewmodel.dart @@ -0,0 +1,39 @@ +import 'package:mobile_app/enums/view_state.dart'; +import 'package:mobile_app/locator.dart'; +import 'package:mobile_app/models/failure_model.dart'; +import 'package:mobile_app/models/ib/ib_page_data.dart'; +import 'package:mobile_app/services/ib_engine_service.dart'; +import 'package:mobile_app/viewmodels/base_viewmodel.dart'; + +class IbPageViewModel extends BaseModel { + // ViewState Keys + String IB_FETCH_PAGE_DATA = 'ib_fetch_page_data'; + String IB_FETCH_INTERACTION_DATA = 'ib_fetch_interaction_data'; + + final IbEngineService _ibEngineService = locator(); + + IbPageData _pageData; + IbPageData get pageData => _pageData; + + Future fetchPageData({String id = 'index.md'}) async { + try { + _pageData = await _ibEngineService.getPageData(id: id); + + setStateFor(IB_FETCH_PAGE_DATA, ViewState.Success); + } on Failure catch (f) { + setStateFor(IB_FETCH_PAGE_DATA, ViewState.Error); + setErrorMessageFor(IB_FETCH_PAGE_DATA, f.message); + } + } + + Future fetchHtmlInteraction(String id) async { + try { + var result = await _ibEngineService.getHtmlInteraction(id); + setStateFor(IB_FETCH_INTERACTION_DATA, ViewState.Success); + return result; + } on Failure catch (f) { + setStateFor(IB_FETCH_INTERACTION_DATA, ViewState.Error); + setErrorMessageFor(IB_FETCH_INTERACTION_DATA, f.message); + } + } +} diff --git a/pubspec.lock b/pubspec.lock index 255b0842..7b9b7594 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,34 +1,41 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - animations: - dependency: "direct main" + _fe_analyzer_shared: + dependency: transitive description: - name: animations + name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" - archive: + version: "22.0.0" + analyzer: dependency: transitive description: - name: archive + name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "2.0.13" + version: "1.7.1" + animations: + dependency: "direct main" + description: + name: animations + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "1.6.0" + version: "2.1.1" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.5.0" boolean_selector: dependency: transitive description: @@ -36,6 +43,62 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "7.0.0" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.0" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "8.1.0" characters: dependency: transitive description: @@ -50,20 +113,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" chewie: dependency: transitive description: name: chewie url: "https://pub.dartlang.org" source: hosted - version: "0.12.2" + version: "1.0.0" chewie_audio: dependency: transitive description: name: chewie_audio url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "1.2.0" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.2" clock: dependency: transitive description: @@ -71,6 +148,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" collection: dependency: transitive description: @@ -84,35 +168,35 @@ packages: name: convert url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "3.0.1" crypto: dependency: transitive description: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" - css_colors: - dependency: transitive - description: - name: css_colors - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.2" + version: "3.0.1" csslib: dependency: transitive description: name: csslib url: "https://pub.dartlang.org" source: hosted - version: "0.16.2" + version: "0.17.0" cupertino_icons: dependency: "direct main" description: name: cupertino_icons url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.3" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" datetime_picker_formfield: dependency: "direct main" description: @@ -141,6 +225,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.1.0" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" flutter: dependency: "direct main" description: flutter @@ -152,28 +243,28 @@ packages: name: flutter_facebook_auth url: "https://pub.dartlang.org" source: hosted - version: "3.3.2" + version: "3.5.0" flutter_facebook_auth_platform_interface: dependency: transitive description: name: flutter_facebook_auth_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.4.0" + version: "2.7.0" flutter_facebook_auth_web: dependency: transitive description: name: flutter_facebook_auth_web url: "https://pub.dartlang.org" source: hosted - version: "2.4.1" + version: "2.6.0+2" flutter_html: dependency: "direct main" description: name: flutter_html url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "2.0.0" flutter_icons: dependency: "direct main" description: @@ -187,7 +278,7 @@ packages: name: flutter_keyboard_visibility url: "https://pub.dartlang.org" source: hosted - version: "5.0.0" + version: "5.0.2" flutter_keyboard_visibility_platform_interface: dependency: transitive description: @@ -202,53 +293,62 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" - flutter_launcher_icons: - dependency: "direct dev" - description: - name: flutter_launcher_icons - url: "https://pub.dartlang.org" - source: hosted - version: "0.8.1" flutter_layout_grid: dependency: transitive description: name: flutter_layout_grid url: "https://pub.dartlang.org" source: hosted - version: "0.10.5" + version: "1.0.3" flutter_localizations: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_markdown: + dependency: "direct main" + description: + path: "packages/flutter_markdown" + ref: HEAD + resolved-ref: "29bb87fd8fa952de825ea1149bc6f615341dbfc5" + url: "git://github.com/CircuitVerse/packages.git" + source: git + version: "0.6.2" + flutter_math_fork: + dependency: transitive + description: + name: flutter_math_fork + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3+1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle url: "https://pub.dartlang.org" source: hosted - version: "1.0.8" + version: "2.0.2" flutter_secure_storage: dependency: transitive description: name: flutter_secure_storage url: "https://pub.dartlang.org" source: hosted - version: "3.3.3" + version: "4.2.0" flutter_summernote: dependency: "direct main" description: name: flutter_summernote url: "https://pub.dartlang.org" source: hosted - version: "0.2.3" + version: "1.0.0" flutter_svg: dependency: "direct main" description: name: flutter_svg url: "https://pub.dartlang.org" source: hosted - version: "0.19.3" + version: "0.22.0" flutter_test: dependency: "direct dev" description: flutter @@ -260,103 +360,152 @@ packages: name: flutter_typeahead url: "https://pub.dartlang.org" source: hosted - version: "3.0.0-nullsafety.0" + version: "3.1.3" flutter_web_auth: dependency: transitive description: name: flutter_web_auth url: "https://pub.dartlang.org" source: hosted - version: "0.2.4" + version: "0.3.0" flutter_web_plugins: dependency: transitive description: flutter source: sdk version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" get: dependency: "direct main" description: name: get url: "https://pub.dartlang.org" source: hosted - version: "3.26.0" + version: "4.1.4" get_it: dependency: "direct main" description: name: get_it url: "https://pub.dartlang.org" source: hosted - version: "6.0.0" + version: "7.1.3" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" google_sign_in: dependency: "direct main" description: name: google_sign_in url: "https://pub.dartlang.org" source: hosted - version: "4.5.1" + version: "5.0.4" google_sign_in_platform_interface: dependency: transitive description: name: google_sign_in_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "2.0.1" google_sign_in_web: dependency: transitive description: name: google_sign_in_web url: "https://pub.dartlang.org" source: hosted - version: "0.9.1+1" + version: "0.10.0" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + hive: + dependency: "direct main" + description: + name: hive + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + hive_flutter: + dependency: "direct main" + description: + name: hive_flutter + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" html: dependency: "direct main" description: name: html url: "https://pub.dartlang.org" source: hosted - version: "0.14.0+3" + version: "0.15.0" + html_unescape: + dependency: "direct main" + description: + name: html_unescape + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" http: dependency: "direct main" description: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.12.2" - http_parser: + version: "0.13.3" + http_multi_server: dependency: transitive description: - name: http_parser + name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "3.1.4" - image: + version: "3.0.1" + http_parser: dependency: transitive description: - name: image + name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "2.1.14" + version: "4.0.0" image_picker: dependency: transitive description: name: image_picker url: "https://pub.dartlang.org" source: hosted - version: "0.6.7+22" - image_picker_platform_interface: + version: "0.8.1+3" + image_picker_for_web: dependency: transitive description: - name: image_picker_platform_interface + name: image_picker_for_web url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" - import_js_library: + version: "2.0.0" + image_picker_platform_interface: dependency: transitive description: - name: import_js_library + name: image_picker_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "2.1.0" intl: dependency: "direct main" description: @@ -364,6 +513,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.17.0" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" js: dependency: transitive description: @@ -371,6 +527,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.3" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + markdown: + dependency: transitive + description: + name: markdown + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" matcher: dependency: transitive description: @@ -398,7 +575,7 @@ packages: name: mockito url: "https://pub.dartlang.org" source: hosted - version: "4.1.1+1" + version: "5.0.10" nested: dependency: transitive description: @@ -412,7 +589,14 @@ packages: name: oauth2_client url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "2.2.2" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" path: dependency: transitive description: @@ -426,14 +610,21 @@ packages: name: path_drawing url: "https://pub.dartlang.org" source: hosted - version: "0.4.1+1" + version: "0.5.1" path_parsing: dependency: transitive description: name: path_parsing url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.2.1" + path_provider: + dependency: transitive + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" path_provider_linux: dependency: transitive description: @@ -441,6 +632,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" path_provider_platform_interface: dependency: transitive description: @@ -461,14 +659,14 @@ packages: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.9.0" + version: "1.11.1" petitparser: dependency: transitive description: name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "3.0.4" + version: "4.1.0" photo_view: dependency: "direct main" description: @@ -490,6 +688,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.0" process: dependency: transitive description: @@ -504,34 +709,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "5.0.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "3.0.1" random_string: dependency: transitive description: name: random_string url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.2.0-nullsafety" share: dependency: "direct main" description: name: share url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.4" shared_preferences: dependency: "direct main" description: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.6" shared_preferences_linux: dependency: transitive description: @@ -567,18 +786,46 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.4" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + source_helper: + dependency: transitive + description: + name: source_helper + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" source_span: dependency: transitive description: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.0" stack_trace: dependency: transitive description: @@ -593,6 +840,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" string_scanner: dependency: transitive description: @@ -613,7 +867,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.2.19" theme_provider: dependency: "direct main" description: @@ -627,14 +881,28 @@ packages: name: timeago url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.1.0" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" transparent_image: dependency: "direct main" description: name: transparent_image url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "2.0.0" + tuple: + dependency: transitive + description: + name: tuple + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" typed_data: dependency: transitive description: @@ -648,7 +916,7 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.0.2" + version: "6.0.9" url_launcher_linux: dependency: transitive description: @@ -669,7 +937,7 @@ packages: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.4" url_launcher_web: dependency: transitive description: @@ -697,49 +965,70 @@ packages: name: video_player url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "2.1.6" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "4.1.0" video_player_web: dependency: transitive description: name: video_player_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.4+1" + version: "2.0.1" wakelock: dependency: transitive description: name: wakelock url: "https://pub.dartlang.org" source: hosted - version: "0.2.1+1" + version: "0.4.0" + wakelock_macos: + dependency: transitive + description: + name: wakelock_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0+1" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "0.1.0+1" + version: "0.2.1+1" wakelock_web: dependency: transitive description: name: wakelock_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.0+3" + version: "0.2.0+1" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" webview_flutter: dependency: transitive description: name: webview_flutter url: "https://pub.dartlang.org" source: hosted - version: "1.0.7" + version: "2.0.9" win32: dependency: transitive description: @@ -760,14 +1049,14 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "4.2.0" + version: "5.1.2" yaml: dependency: transitive description: name: yaml url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "3.1.0" sdks: dart: ">=2.12.0 <3.0.0" flutter: ">=2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index f0059318..01641277 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: none version: 1.0.0+4 environment: - sdk: ">=2.2.2 <3.0.0" + sdk: ">=2.7.0 <3.0.0" dependencies: flutter: @@ -23,37 +23,45 @@ dependencies: flutter_localizations: sdk: flutter animations: ^2.0.0 - cupertino_icons: ^1.0.2 + cupertino_icons: ^1.0.3 datetime_picker_formfield: ^2.0.0 - flutter_facebook_auth: ^3.3.2 - flutter_html: ^1.0.2 + flutter_facebook_auth: ^3.5.0 + flutter_html: ^2.0.0 flutter_icons: ^1.1.0 - flutter_keyboard_visibility: ^5.0.0 - flutter_summernote: ^0.2.3 - flutter_svg: ^0.19.3 - flutter_typeahead: ^3.0.0-nullsafety.0 - get: ^3.26.0 - get_it: ^6.0.0 - google_sign_in: ^4.5.1 - html: ^0.14.0+3 - http: ^0.12.1 + flutter_keyboard_visibility: ^5.0.2 + flutter_summernote: ^1.0.0 + flutter_svg: ^0.22.0 + flutter_typeahead: ^3.1.3 + get: ^4.1.4 + get_it: ^7.1.3 + google_sign_in: ^5.0.4 + html: ^0.15.0 + http: ^0.13.3 intl: ^0.17.0 - mockito: ^4.1.1 - oauth2_client: ^1.8.0 + mockito: ^5.0.10 + oauth2_client: ^2.2.2 photo_view: ^0.11.1 provider: ^5.0.0 - share: ^2.0.1 - shared_preferences: ^2.0.4 + share: ^2.0.4 + shared_preferences: ^2.0.6 theme_provider: ^0.5.0 - timeago: ^3.0.2 - transparent_image: ^1.0.0 - url_launcher: ^6.0.2 + timeago: ^3.1.0 + transparent_image: ^2.0.0 + url_launcher: ^6.0.9 + flutter_markdown: + git: + url: git://github.com/CircuitVerse/packages.git + path: packages/flutter_markdown + html_unescape: ^2.0.0 + hive: ^2.0.4 + hive_flutter: ^1.1.0 dev_dependencies: flutter_test: sdk: flutter - flutter_launcher_icons: ^0.8.1 - pedantic: ^1.9.0 + pedantic: ^1.11.1 + hive_generator: ^1.1.0 + build_runner: ^2.0.5 dependency_overrides: mime: ^0.9.7 diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 00000000..5c6d1118 --- /dev/null +++ b/test/.gitignore @@ -0,0 +1 @@ +hive_testing_path \ No newline at end of file diff --git a/test/model_tests/ib/ib_raw_page_data_test.dart b/test/model_tests/ib/ib_raw_page_data_test.dart new file mode 100644 index 00000000..2de139b0 --- /dev/null +++ b/test/model_tests/ib/ib_raw_page_data_test.dart @@ -0,0 +1,28 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mobile_app/models/ib/ib_raw_page_data.dart'; + +import '../../setup/test_data/mock_ib_raw_page_data.dart'; + +void main() { + group('IbRawPageData Test -', () { + test('fromJson', () { + var _ibRawPageData = IbRawPageData.fromJson(mockIbRawPageData1); + + expect(_ibRawPageData.id, mockIbRawPageData1['path']); + expect(_ibRawPageData.name, mockIbRawPageData1['name']); + expect(_ibRawPageData.title, mockIbRawPageData1['title']); + expect(_ibRawPageData.parent, null); + expect( + _ibRawPageData.navOrder, mockIbRawPageData1['nav_order'].toString()); + expect(_ibRawPageData.cvibLevel, null); + expect(_ibRawPageData.hasChildren, false); + expect(_ibRawPageData.hasToc, false); + expect(_ibRawPageData.disableComments, true); + expect(_ibRawPageData.content, mockIbRawPageData1['content']); + expect(_ibRawPageData.rawContent, mockIbRawPageData1['raw_content']); + expect(_ibRawPageData.frontMatter, mockIbRawPageData1['front_matter']); + expect(_ibRawPageData.httpUrl, mockIbRawPageData1['http_url']); + expect(_ibRawPageData.apiUrl, mockIbRawPageData1['api_url']); + }); + }); +} diff --git a/test/service_tests/database_service_test.dart b/test/service_tests/database_service_test.dart new file mode 100644 index 00000000..69336b02 --- /dev/null +++ b/test/service_tests/database_service_test.dart @@ -0,0 +1,47 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:hive/hive.dart'; +import 'package:mobile_app/locator.dart'; +import 'package:mobile_app/services/database_service.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +void main() { + group('DatabaseService Test -', () { + DatabaseService db; + + setUpAll(() async { + SharedPreferences.setMockInitialValues({}); + await setupLocator(); + + db = locator(); + await db.init(); + var path = Directory.current.path; + Hive.init(path + '/test/hive_testing_path'); + }); + + test('Set and Get data from a box', () async { + await Hive.deleteFromDisk(); + + await db.setData(DatabaseBox.IB, 'test', 'test value'); + var expectedData = await db.getData(DatabaseBox.IB, 'test'); + + expect(expectedData, 'test value'); + }); + + test('Get non-existent data from a box', () async { + await Hive.deleteFromDisk(); + + var expectedData = await db.getData(DatabaseBox.IB, 'test-2'); + expect(expectedData, null); + }); + + test('Get default value from non-existent data from a box', () async { + await Hive.deleteFromDisk(); + + var expectedData = + await db.getData(DatabaseBox.IB, 'test-3', defaultValue: 'test'); + expect(expectedData, 'test'); + }); + }); +} diff --git a/test/service_tests/ib_api_test.dart b/test/service_tests/ib_api_test.dart new file mode 100644 index 00000000..7b28b7cd --- /dev/null +++ b/test/service_tests/ib_api_test.dart @@ -0,0 +1,76 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart'; +import 'package:http/testing.dart'; +import 'package:mobile_app/locator.dart'; +import 'package:mobile_app/models/failure_model.dart'; +import 'package:mobile_app/models/ib/ib_raw_page_data.dart'; +import 'package:mobile_app/services/API/ib_api.dart'; +import 'package:mobile_app/utils/api_utils.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../setup/test_data/mock_ib_raw_page.dart'; +import '../setup/test_data/mock_ib_raw_page_data.dart'; + +void main() { + group('IbApiTest -', () { + setUpAll(() async { + SharedPreferences.setMockInitialValues({}); + await setupLocator(); + }); + + group('fetchApiPage -', () { + setUpAll(() async { + var path = Directory.current.path; + Hive.init(path + '/test/hive_testing_path'); + }); + + test('When called & http client returns succes response', () async { + await Hive.deleteFromDisk(); + + ApiUtils.client = MockClient((_) => Future.value( + Response(jsonEncode([mockIbRawPage1, mockIbRawPage2]), 200))); + var _ibApi = HttpIbApi(); + + expect((await _ibApi.fetchApiPage()).toString(), + [mockIbRawPage1, mockIbRawPage2].toString()); + }); + + test('When called & http client throws Exceptions', () async { + await Hive.deleteFromDisk(); + + var _ibApi = HttpIbApi(); + + ApiUtils.client = MockClient((_) => throw FormatException('')); + expect(_ibApi.fetchApiPage(), throwsA(isInstanceOf())); + + ApiUtils.client = MockClient((_) => throw Exception('')); + expect(_ibApi.fetchApiPage(), throwsA(isInstanceOf())); + }); + }); + + group('fetchRawPageData -', () { + test('When called & http client returns succes response', () async { + ApiUtils.client = MockClient( + (_) => Future.value(Response(jsonEncode(mockIbRawPageData1), 200))); + var _ibApi = HttpIbApi(); + + expect((await _ibApi.fetchRawPageData()).toString(), + IbRawPageData.fromJson(mockIbRawPageData1).toString()); + }); + + test('When called & http client throws Exceptions', () async { + var _ibApi = HttpIbApi(); + + ApiUtils.client = MockClient((_) => throw FormatException('')); + expect(_ibApi.fetchRawPageData(), throwsA(isInstanceOf())); + + ApiUtils.client = MockClient((_) => throw Exception('')); + expect(_ibApi.fetchRawPageData(), throwsA(isInstanceOf())); + }); + }); + }); +} diff --git a/test/service_tests/ib_engine_test.dart b/test/service_tests/ib_engine_test.dart new file mode 100644 index 00000000..2516014a --- /dev/null +++ b/test/service_tests/ib_engine_test.dart @@ -0,0 +1,255 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:http/http.dart'; +import 'package:http/testing.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mobile_app/config/environment_config.dart'; +import 'package:mobile_app/models/failure_model.dart'; +import 'package:mobile_app/models/ib/ib_chapter.dart'; +import 'package:mobile_app/models/ib/ib_content.dart'; +import 'package:mobile_app/models/ib/ib_page_data.dart'; +import 'package:mobile_app/models/ib/ib_raw_page_data.dart'; +import 'package:mobile_app/services/ib_engine_service.dart'; +import 'package:mobile_app/utils/api_utils.dart'; +import 'package:mockito/mockito.dart'; + +import '../setup/test_data/mock_ib_raw_page.dart'; +import '../setup/test_data/mock_ib_raw_page_data.dart'; +import '../setup/test_helpers.dart'; + +void main() { + group('IbEngineService Test -', () { + setUpAll(() => registerServices()); + tearDownAll(() => unregisterServices()); + + group('getChapters -', () { + test('When called and returns success response', () async { + var _ibApi = getAndRegisterIbApiMock(); + + when(_ibApi.fetchApiPage(id: '')) + .thenAnswer((_) => Future.value([mockIbRawPage2, mockIbRawPage3])); + + var ibChapter2 = IbChapter( + id: mockIbRawPage3['path'], + navOrder: mockIbRawPage3['nav_order'].toString(), + value: mockIbRawPage3['title'], + ); + + var ibChapter = IbChapter( + id: mockIbRawPage2['path'], + value: mockIbRawPage2['title'], + navOrder: mockIbRawPage2['nav_order'].toString(), + next: ibChapter2, + items: [ + ibChapter2, + ], + ); + + ibChapter2.prevPage = ibChapter; + + var _expectedResult = [ibChapter]; + + var _ibEngine = IbEngineServiceImpl(); + var _actualResult = await _ibEngine.getChapters(); + + expect(_actualResult.length, _expectedResult.length); + expect(_actualResult[0].id, _expectedResult[0].id); + expect(_actualResult[0].value, _expectedResult[0].value); + expect(_actualResult[0].prev?.id, _expectedResult[0].prev?.id); + expect(_actualResult[0].next?.id, _expectedResult[0].next?.id); + expect(_actualResult[0].items != null, true); + + expect(_actualResult[0].items.length, _expectedResult[0].items.length); + expect(_actualResult[0].items[0].id, _expectedResult[0].items[0].id); + expect( + _actualResult[0].items[0].value, _expectedResult[0].items[0].value); + expect(_actualResult[0].items[0].prev?.id, + _expectedResult[0].items[0].prev?.id); + expect(_actualResult[0].items[0].next?.id, + _expectedResult[0].items[0].next?.id); + expect(_actualResult[0].items[0].items, null); + }); + + test('When called and throws Failure', () async { + var _ibApi = getAndRegisterIbApiMock(); + + when(_ibApi.fetchApiPage(id: '')) + .thenAnswer((_) => throw Exception('Service Unavailable')); + + var _ibEngine = IbEngineServiceImpl(); + + expect(() async => await _ibEngine.getChapters(), + throwsA(isInstanceOf())); + }); + }); + + group('getPageData -', () { + test('When Home page called and returns success response', () async { + var _expectedResult = IbPageData( + id: mockIbRawPageData1['name'], + pageUrl: mockIbRawPageData1['http_url'], + title: mockIbRawPageData1['title'], + content: [IbMd(content: mockIbRawPageData1['raw_content'])], + tableOfContents: [], + ); + + var _ibApi = getAndRegisterIbApiMock(); + + when(_ibApi.fetchRawPageData(id: mockIbRawPageData1['name'])) + .thenAnswer((_) => + Future.value(IbRawPageData.fromJson(mockIbRawPageData1))); + + var _ibEngine = IbEngineServiceImpl(); + var _actualResult = await _ibEngine.getPageData(); + + expect(_actualResult.id, _expectedResult.id); + expect(_actualResult.title, _expectedResult.title); + expect(_actualResult.tableOfContents, _expectedResult.tableOfContents); + expect(_actualResult.content != null, true); + + expect(_actualResult.content[0].content, + _expectedResult.content[0].content); + }); + + test('When a regular page called and returns success response', () async { + var mockDataFile = + File('test/setup/test_data/contributing_guidelines.json'); + Map mockIbRawPageData2 = + jsonDecode(await mockDataFile.readAsString()); + + var _expectedResult = IbPageData( + id: mockIbRawPageData2['path'], + pageUrl: mockIbRawPageData2['http_url'], + title: mockIbRawPageData2['title'], + content: [], + tableOfContents: [ + IbTocItem( + content: '1. About this guidelines', + items: [ + IbTocItem(content: 'a. Revision history'), + IbTocItem(content: 'b. Purpose of the guidelines'), + IbTocItem(content: 'c. Acknowledgements'), + ], + ), + IbTocItem(content: '2. Workflow'), + IbTocItem( + content: '3. Licensing', + items: [ + IbTocItem( + content: + 'a. Non-free materials and special requirements'), + IbTocItem(content: 'b. Linking to copyrighted works'), + ], + ), + IbTocItem(content: '4. Proposing a contribution'), + IbTocItem(content: '5. Editing existing content'), + IbTocItem(content: '6. Writing content'), + IbTocItem(content: '7. Style manual'), + IbTocItem(content: '8. Templates and examples'), + IbTocItem( + content: + '9. Code of conduct, interacting with the community / etiquette'), + IbTocItem(content: '10. Tools'), + ]); + + var _ibApi = getAndRegisterIbApiMock(); + + when(_ibApi.fetchRawPageData(id: mockIbRawPageData2['path'])) + .thenAnswer((_) => + Future.value(IbRawPageData.fromJson(mockIbRawPageData2))); + + var _ibEngine = IbEngineServiceImpl(); + var _actualResult = + await _ibEngine.getPageData(id: mockIbRawPageData2['path']); + + expect(_actualResult.id, _expectedResult.id); + expect(_actualResult.title, _expectedResult.title); + expect(_actualResult.tableOfContents.length, + _expectedResult.tableOfContents.length); + + // [TODO] Tests for Content + + expect(_actualResult.tableOfContents[0].content, + _expectedResult.tableOfContents[0].content); + expect(_actualResult.tableOfContents[0].items.length, + _expectedResult.tableOfContents[0].items.length); + expect(_actualResult.tableOfContents[0].items[0].content, + _expectedResult.tableOfContents[0].items[0].content); + expect(_actualResult.tableOfContents[0].items[1].content, + _expectedResult.tableOfContents[0].items[1].content); + expect(_actualResult.tableOfContents[0].items[2].content, + _expectedResult.tableOfContents[0].items[2].content); + + expect(_actualResult.tableOfContents[1].content, + _expectedResult.tableOfContents[1].content); + + expect(_actualResult.tableOfContents[2].content, + _expectedResult.tableOfContents[2].content); + expect(_actualResult.tableOfContents[2].items.length, + _expectedResult.tableOfContents[2].items.length); + expect(_actualResult.tableOfContents[2].items[0].content, + _expectedResult.tableOfContents[2].items[0].content); + expect(_actualResult.tableOfContents[2].items[1].content, + _expectedResult.tableOfContents[2].items[1].content); + + expect(_actualResult.tableOfContents[3].content, + _expectedResult.tableOfContents[3].content); + expect(_actualResult.tableOfContents[4].content, + _expectedResult.tableOfContents[4].content); + expect(_actualResult.tableOfContents[5].content, + _expectedResult.tableOfContents[5].content); + expect(_actualResult.tableOfContents[6].content, + _expectedResult.tableOfContents[6].content); + expect(_actualResult.tableOfContents[7].content, + _expectedResult.tableOfContents[7].content); + expect(_actualResult.tableOfContents[8].content, + _expectedResult.tableOfContents[8].content); + expect(_actualResult.tableOfContents[9].content, + _expectedResult.tableOfContents[9].content); + }); + + test('When called and throws Failure', () async { + var _ibApi = getAndRegisterIbApiMock(); + + when(_ibApi.fetchRawPageData(id: 'index.md')) + .thenAnswer((_) => throw Exception('Service Unavailable')); + + var _ibEngine = IbEngineServiceImpl(); + + expect(() async => await _ibEngine.getPageData(), + throwsA(isInstanceOf())); + }); + }); + + group('getHtmlInteraction -', () { + test('When binary embed is called and returns success response', + () async { + var _mockJsFile = File('test/setup/test_data/mock_module.js'); + var _mockJs = await _mockJsFile.readAsString(); + + var _mockHtmlFile = File('test/setup/test_data/mock_binary.html'); + var _mockHtml = await _mockHtmlFile.readAsString(); + + var _expectedResult = + '\n$_mockHtml'; + _expectedResult = _expectedResult.replaceAll( + RegExp(r'(\.\.(\/\.\.)?)?(?Contributing Guidelines\n\n

Table of contents

\n\n
    \n
  1. About this guidelines
      \n
    1. Revision history
    2. \n
    3. Purpose of the guidelines
    4. \n
    5. Acknowledgements
    6. \n
    \n
  2. \n
  3. Workflow
  4. \n
  5. Licensing
      \n
    1. Non-free materials and special requirements
    2. \n
    3. Linking to copyrighted works
    4. \n
    \n
  6. \n
  7. Proposing a contribution
  8. \n
  9. Editing existing content
  10. \n
  11. Writing content
      \n
    1. Quality assurance
    2. \n
    \n
  12. \n
  13. Style manual
      \n
    1. Figures and tables
    2. \n
    3. Equations
    4. \n
    5. References
    6. \n
    \n
  14. \n
  15. Templates and examples
      \n
    1. New chapter template
    2. \n
    3. New section template
    4. \n
    5. Equations examples
    6. \n
    7. Figure examples
    8. \n
    9. Table examples
    10. \n
    11. Bibliography example
    12. \n
    \n
  16. \n
  17. Code of conduct, interacting with the community / etiquette
  18. \n
  19. Tools
      \n
    1. Git
    2. \n
    3. Jekyll
    4. \n
    \n
  20. \n
\n\n
\n\n

About this guidelines

\n\n

This guidelines documentation was started on September 2020 as part of the Goggle Season of Docs 2020 initiative.

\n\n

Revision history

\n\n

The revision history is presented in the following table

\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Revision numberDateRevised by
1.02020-11-25DVLS
\n\n

Purpose of the guidelines

\n\n

These guidelines are inteded to guide those who wish to contribute to this Interactive Book. It (tries) to cover most aspects such as the process workflow, license, style, quality, templates and tools.

\n\n

Acknowledgements

\n\n

These guidlines are heavily based on the Linux Document Project Author Guide and the Wikibooks Contributing help and Policies and guidelines.

\n\n
    \n
  • The LDP Author Guide is Copyright (C) 1999-2002 Mark F. Komarinski, David C. Merrill, Jorge Godoy. It is licensed under the GNU FDL v1.1+ (no invariant sections, no front-cover texts and no-back-cover texts)
  • \n
  • Wikibooks documents are licensed under the CC-by-sa License and copyrighted by contributors to Wikibooks
  • \n
\n\n
\n\n

Workflow

\n\n

Usually, the contribution process will include the following steps:

\n\n
    \n
  1. find an opportunity to contribute:\n
      \n
    1. add new content (new chapters, new sections, extend a section, add example circuits),
    2. \n
    3. correct existing content,
    4. \n
    5. translate content,
    6. \n
    7. \n
    \n
  2. \n
  3. contact the interactive book development coordination team and share/discuss your idea
  4. \n
  5. further discuss the idea with the community
  6. \n
  7. fork the book repository
  8. \n
  9. work on the contribution
  10. \n
  11. create a pull-request to start the reviewing process of your contribution
  12. \n
\n\n
\n\n

Licensing

\n\n

The book is licensed under the CC BY-SA 4.0 and copyrighted by contributors to CircuitVerse unless otherwise noted. The author has to make sure that the all content contributed can be licensed under the specified license. Never submit copyrighted material without permission from copyright owner. If you contribute to the book, you irrevocably agree to license it to the public under CC BY-SA 4.0.

\n\n

Non-free materials and special requirements

\n\n

CircuitVerse’s Interactive Book may also include quotations, images, or other media under the U.S. Copyright law “fair use” doctrine. In CircuitVerse, such “fair use” material should be identified as from an external source by an appropriate method (on the figure or table description, as appropriate; quotations should be denoted as a quotation block. This leads to possible restrictions on the use, outside of the Interactive Book, of such “fair use” content retrieved from the book: this “fair use” content does not fall under the CC BY-SA 4.0 license as such, but under the “fair use” (or similar/different) regulations in the country where the media are retrieved.

\n\n

More information about “Fair use”

\n\n\n\n

Linking to copyrighted works

\n\n

Since most recently-created works are copyrighted, almost all cites sources will link to copyrighted material. It is usually not necessary to obtain the permission of a copyright holder before linking to copyrighted material, just as an author of a dead-tree book does not need permission to cite someone else’s work in their bibliography. Likewise, CircuitVerse’s Interactive Book is not restricted to linking only to CC-BY-SA or open-source content.

\n\n
\n\n

Proposing a contribution

\n\n

If you have in mind new content to be added, it is important that the CircuitVerse community understand why your contribution should be included in the book. You may have already discussed your idea in some of CircuitVerse social channels, in that case, please include the discussion in your proposal.

\n\n

The proposal should clearly indicate what kind of contribution are you planning to submit (add new content, edit existing content, translate content). When proposing new content, please, also specify whether it will be a whole new chapter, a new section, specific content within a section (text, figures, equations, example circuits, etc.)

\n\n

Then, include a brief summary of the contribution content followed by the justification why it is needed.

\n\n

Finaly, submit your proposal to the CircuitVerse Interactive Book coordination team, and discuss the proposal with them in order to refine it. The coordination team will either approve or reject the proposal. In case of rejection, comments will be included explaining why it was rejected, and how could it be improved when the proposed content is useful, but the proposed “format” whas not adequate enough.

\n\n
\n\n

Editing existing content

\n\n

When editing existing content, try to contact the original author first in order to discuss your idea.

\n\n

If the original author cannot be contacted after a “good-faith” effort, fall back to discuss the topic with the CircuiVerse community through any of the social channels.

\n\n
\n\n

Writing content

\n\n

All content should be supported by valid sources. Please, research carefuly and validate all your references. Check that there are no copyright or license issues regarding the use of each of your sources.

\n\n

When using online resources, make a “hard-copy” (screen-capture, pdf-printed version) of them to avoid missing resources in the future (Error 404)

\n\n

The Interactive Book is not intended as a thorough textbook, but its main focus is on interactive content. Try to keep the content to the amount necessary to understand the topic and cite appropriate references for further study on the subject.

\n\n

To keep the consistency of the book, please follow the style indicated in these guidelines, also use the templates and existing content as example.

\n\n

Quality assurance

\n\n

Besides researching and validating your sources and adhering to the Interactive Book style, please also edit (remove unnecesary content) and check for spelling and grammar errors. Then, forward your contribution to a third party for proofreading. When you are satisfied with the quality and accuracy of your contribution ask for peer reviewers from the CircuitVerse community. Finally, submit your work to the “formal” review by creating a pull-request of your forked repository

\n\n
\n\n

Style manual

\n\n

[Work in Progress]

\n\n

While most of the formatting style is carried out automatically by the layout, templates and stylesheets in the SSG (jekyll) pipeline, it is important to also adhere to a common writing style in order to assure consistency of the book.

\n\n

Please use a neutral and simple language. Avoid slang. Also, technical jargon should be explained.

\n\n

Figures and tables

\n\n

Figures and tables should include a description or caption, and they should be commented in text. If a figure or table is not part of the description, probably it is not needed and can be removed from the book.

\n\n

See an example in section Templates and examples.

\n\n

Equations

\n\n

Equations can be typesetted in LaTeX to be rendered by MathJax. A few examples can be found in the Templates and examples section. If equations need to be referenced in the text, use numbered or labelled equations. General useful guidelines:

\n\n
    \n
  • There are many variants for the notation of negated variables, prefer the bar over the variable or the prime (complement) symbol.
  • \n
  • For binary words with many literals (high number of bits) prefer a single letter with subscript numbers for each bit. For instance a four bit word can be written as $(x_3,x_2,x_1,x_0)$ instead of a different letter for each literal $(A,B,C,D)$. The least significant bit should be subscripted with 0. In case of ambiguity or if the opposite arrangement is necessary, use a superscript mark to identify the MSB and LSB: $(x_0^{\\text{MSB}},x_1,x_2,x_3^{\\text{LSB}})$
  • \n
\n\n

References

\n\n

Information obtained from other sources should be properly cited and referenced. Check the Templates and examples section for examples.

\n\n

Currently, references are formatted according to the ieee-with-url style defined CiteProc-Ruby.

\n\n
\n\n

Templates and examples

\n\n

To help you with getting started with writing your contribution, please use the following templates and examples, accordingly:

\n\n
    \n
  • New chapter template
  • \n
  • New section template
  • \n
  • Equations example
  • \n
  • Figure example
  • \n
  • Table example
  • \n
  • Bibliography example
  • \n
\n\n

The structure of the book, as well as the navigation menu is handled by Jekyll’s Just-the-docs plugin. A two-level structure has been chosen. The top-level is for chapters and the second level is for sections in a chapter

\n\n

New chapter template

\n\n

Each chapter should be a directory under the docs directory. The index.md in the chapter directory is used for the Chapter text and table of contents.

\n\n

The index.md file should have the following contents:

\n\n
---\nlayout: circuitverse\ntitle: Chapter title\nnav_order: 10\nhas_children: true\nhas_toc: false\n---\n\n# Long chapter title\n{: .no_toc}\n\nIntroductory chapter text\n\n## Chapter contents\n{: .no_toc .text-delta}\n\n\n{% include chapter_toc.html %}\n\n
\n\n

The title and nav_order elements in the front matter should be filled-in accordingly. The other elements in the front matter block should be left as in the template above.

\n\n

After the “Long” chapter title an introductory text for the chapter can be added. It is followed by the Table of contents’ title and the include directive which create a list of links to the sections of the chapter according to their level and navigation order.

\n\n

New section template

\n\n

Files for sections within a chapter must be placed in the chapter’s directory. Currently, the contents of the book are organised in three levels of difficulty:

\n\n
    \n
  1. basic
  2. \n
  3. medium
  4. \n
  5. advanced
  6. \n
\n\n

The following code represents an empty section and can be used as a template

\n\n
---\nlayout: circuitverse\ntitle: Section title\nnav_order: l0s000\ncvib_level: advanced\nparent: Parent Chapter\nhas_children: false\n---\n\n# Long section title\n{: .no_toc}\n\n## Table of contents\n{: .no_toc .text-delta}\n\n1. TOC\n{:toc}\n\n## Title\n\nText\n
\n\n

Each section file should have content belonging only to one of these levels. Therefore, the front matter block presents the cvib_level which correspond to one of the levels.

\n\n

The parent element must be the name of the Chapter containing the section.

\n\n

The section’s title goes in the title element.

\n\n

To standardise the order of sections in the navigation bar according to the difficulty level, the nav_order element is an “encoded” string with this form: l, followed by a digit, followed by s, followed by three digits. l stands for level and is a number between 0 and 2 and represent the basic, medium and advanced level, respectively. s stands for section and the three digit number after it is a zero-padded number indicating the section position in the chapter.

\n\n

The other elements in the front matter block (layout, has_children) should be should be left as they are.

\n\n

After the front matter block comes the section’s title followed by the table of contents title and code to generate it automatically.

\n\n

After that, parts within the section can be added.

\n\n

Equations examples

\n\n

Equations are processed using MathJax provided by the jekyll-spaceship plugin.

\n\n

The following are valid mathematical formulas and their rendering:

\n\n
    \n
  1. \n

    Unnumbered equations

    \n\n
    $ f(x)=\\int_2^n \\frac{1}{2\\pi}x^2 $\n
    \n\n

    $ f(x)=\\int_2^n \\frac{1}{2\\pi}x^2 $

    \n\n
    $ F(A,B,C) = A + B \\cdot C $\n
    \n\n

    $ F(A,B,C) = A + B ⋅ C $

    \n
  2. \n
  3. \n

    Automatically numbered equations

    \n\n
    $ \\begin{equation}F(x_0, x_1) = x_0 + \\overline{x_1} \\end{equation} $\n
    \n\n

    $ \\begin{equation}F(x_0, x_1) = x_0 + \\overline{x_1} \\end{equation} $

    \n
  4. \n
  5. \n

    Manually numbered equations

    \n\n
    \\[ F(A,B,C) = A + B \\cdot C \\tag{1}\\]\n
    \n\n

    \\[ F(A,B,C) = A + B \\cdot C \\tag{1}\\]

    \n
  6. \n
\n\n

Figure examples

\n\n

Neither Jekyll nor Markdown provide facilities to add captions to images. Therefore, instead of using direct inclusion of images in MD syntax, the Jekyll include mechanism is used to generate properly captioned figures in the HTML rendering. To use it, include a figure like this:

\n\n
\n{% include image.html url=\"/assets/images/path-to-image/image.svg\" description=\"Image caption\" %}\n\n
\n\n

For instance, the following code:

\n\n
\n{% include image.html url=\"/assets/images/XnorGate.svg\" description=\"XNOR Gate\" %}\n\n
\n\n

will render like this:

\n\n
\n \"XNOR\n
XNOR Gate
\n
\n\n

Table examples

\n\n

Complex tables can be handled by the jekyll-spaceship plugin. However, there is no support for table captions. As a workaround, the Table caption can be included in the first row and styled with the table caption CSS tblcap.

\n\n
|: Table: Sample data     {: .tblcap}  :|||\n| col 1       | col 2       | col 3       |\n|:-----------:|:------------|------------:|\n| row1 col2   | row1 col2   | row1 col3   |\n| row2 col1   | row2 col2   | row2 col3   |\n
\n\n

which renders to

\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
: Table: Sample data {: .tblcap} :||  
col 1col 2col 3
row1 col2row1 col2row1 col3
row2 col1row2 col2row2 col3
\n\n

Bibliography example

\n\n

Bibliographic references are processed by the jekyll-scholar plugin. It uses BibTeX files as the source for the references. To use it, the following must be done:

\n\n
    \n
  1. Add the the BibTeX file to the _bibliography/ directory. Care must be taken to not overwrite other bib files in the directory.
  2. \n
  3. If the BibTex file is named myrefs.bib, then\n
      \n
    1. \n

      To cite a reference use this tag:

      \n\n
              \n{% cite refid --file myrefs %}\n        \n
      \n\n

      where refid is the BibTeX id of the reference and myfile is the BibTeX file without the .bib extension.

      \n
    2. \n
    3. \n

      To print the list of references cited in the section use the following code:

      \n\n
      ## References\n        \n{% bibliography --cited --file myrefs %}\n        \n
      \n
    4. \n
    \n
  4. \n
\n\n

Check the plugin page for more complex uses.

\n\n
\n\n

Code of conduct, interacting with the community / etiquette

\n\n

Refer to the Contributor Covenant Code of Conduct

\n\n
\n\n

Tools

\n\n

The following tools will be useful to work in your contribution.

\n\n

Git

\n\n

The CircuitVerse Interactive Book’s sources are hosted in a GitHub repository. Besides the web interface provided by GitHub, other tools to manipulate git repositories can be used:

\n\n\n\n

Jekyll

\n\n

The Interactive Book is rendered using the Jekyll Static Site Generator (SSG). The GitHub repository is able to create a live version of the book using the continuous integration (CI) / continuous deployment (CD) workflows.

\n\n

To work locally, you will need to setup a Jekyll development environment. It is posible to setup Jekyll natively for your OS or run it in a docker container.

\n\n
    \n
  1. \n

    Workflow with native Jekyll

    \n\n
      \n
    1. Install Jekyll following the official documentation. If you don’t have time to follow the quickstart guide, the following steps might help you:\n
        \n
      1. Install Ruby if necessary
      2. \n
      3. Install Jekyll using the command line: gem install jekyll
      4. \n
      5. Install Bundler: gem install bundle
      6. \n
      7. Change (cd) to your local repository directory and install the project dependencies: bundle install
      8. \n
      \n
    2. \n
    3. Serve a live local copy at http://0.0.0.0:4000/, running: bundle exec jekyll serve
    4. \n
    5. Point your browser to the url above and see your changes live.
    6. \n
    \n
  2. \n
  3. \n

    Workflow using docker

    \n\n
      \n
    1. Install and setup docker if you haven’t done so already
    2. \n
    3. Change (cd) to your local repository directory and Run a jekyll docker image (BretFisher’s images have been tested and work well with the book). docker run --rm -p 4000:4000 -v $(pwd):/site bretfisher/jekyll-serve
    4. \n
    5. Point your browser to the appropriate url, http://0.0.0.0:4000/ if you use the command suggested previously.
    6. \n
    \n
  4. \n
\n", + "dir": "/", + "url": "/contributing_guidelines.html", + "raw_content": "# Contributing Guidelines\n{: .no_toc}\n\n\n## Table of contents\n{: .no_toc .text-delta }\n\n1. TOC\n{:toc}\n\n---\n\n\n## About this guidelines\n\nThis guidelines documentation was started on September 2020 as part of the Goggle Season of Docs 2020 initiative.\n\n\n### Revision history\n\nThe revision history is presented in the following table\n\n| Revision number | Date | Revised by |\n|--------------- |---------- |---------- |\n| 1.0 | 2020-11-25 | DVLS |\n\n\n### Purpose of the guidelines\n\nThese guidelines are inteded to guide those who wish to contribute to this Interactive Book. It (tries) to cover most aspects such as the process workflow, license, style, quality, templates and tools.\n\n\n### Acknowledgements\n\nThese guidlines are heavily based on the [Linux Document Project Author Guide](https://tldp.org/LDP/LDP-Author-Guide/html/index.html) and the Wikibooks [Contributing help](https://en.wikibooks.org/wiki/Help:Contributing) and [Policies and guidelines](https://en.wikibooks.org/wiki/Wikibooks:Policies_and_guidelines).\n\n- The LDP Author Guide is Copyright (C) 1999-2002 Mark F. Komarinski, David C. Merrill, Jorge Godoy. It is licensed under the GNU FDL v1.1+ (no invariant sections, no front-cover texts and no-back-cover texts)\n- Wikibooks documents are licensed under the [CC-by-sa](https://en.wikibooks.org/wiki/Wikibooks:Creative_Commons_Attribution-ShareAlike_3.0_Unported_License) License and copyrighted by contributors to Wikibooks\n\n---\n\n\n## Workflow\n\nUsually, the contribution process will include the following steps:\n\n1. find an opportunity to contribute:\n 1. add new content (new chapters, new sections, extend a section, add example circuits),\n 2. correct existing content,\n 3. translate content,\n 4. …\n2. contact the interactive book development coordination team and share/discuss your idea\n3. further discuss the idea with the community\n4. fork the book repository\n5. work on the contribution\n6. create a pull-request to start the reviewing process of your contribution\n\n---\n\n\n## Licensing\n\nThe book is licensed under the [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0) and copyrighted by contributors to CircuitVerse unless otherwise noted. The author has to make sure that the all content contributed can be licensed under the specified license. Never submit copyrighted material without permission from copyright owner. If you contribute to the book, you irrevocably agree to license it to the public under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0).\n\n\n### Non-free materials and special requirements\n\nCircuitVerse's Interactive Book may also include quotations, images, or other media under the U.S. Copyright law \"fair use\" doctrine. In CircuitVerse, such \"fair use\" material should be identified as from an external source by an appropriate method (on the figure or table description, as appropriate; quotations should be denoted as a quotation block. This leads to possible restrictions on the use, outside of the Interactive Book, of such \"fair use\" content retrieved from the book: this \"fair use\" content does not fall under the [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0) license as such, but under the \"fair use\" (or similar/different) regulations in the country where the media are retrieved.\n\nMore information about \"Fair use\"\n\n- \n- \n- \n- \n- \n- \n\n\n### Linking to copyrighted works\n\nSince most recently-created works are copyrighted, almost all cites sources will link to copyrighted material. It is usually not necessary to obtain the permission of a copyright holder before linking to copyrighted material, just as an author of a dead-tree book does not need permission to cite someone else's work in their bibliography. Likewise, CircuitVerse's Interactive Book is not restricted to linking only to CC-BY-SA or open-source content.\n\n---\n\n\n## Proposing a contribution\n\nIf you have in mind new content to be added, it is important that the CircuitVerse community understand why your contribution should be included in the book. You may have already discussed your idea in some of CircuitVerse social channels, in that case, please include the discussion in your proposal.\n\nThe proposal should clearly indicate what kind of contribution are you planning to submit (add new content, edit existing content, translate content). When proposing new content, please, also specify whether it will be a whole new chapter, a new section, specific content within a section (text, figures, equations, example circuits, etc.)\n\nThen, include a brief summary of the contribution content followed by the justification why it is needed.\n\nFinaly, submit your proposal to the CircuitVerse Interactive Book coordination team, and discuss the proposal with them in order to refine it. The coordination team will either approve or reject the proposal. In case of rejection, comments will be included explaining why it was rejected, and how could it be improved when the proposed content is useful, but the proposed \"format\" whas not adequate enough.\n\n---\n\n\n## Editing existing content\n\nWhen editing existing content, try to contact the original author first in order to discuss your idea.\n\nIf the original author cannot be contacted after a \"good-faith\" effort, fall back to discuss the topic with the CircuiVerse community through any of the social channels.\n\n---\n\n\n## Writing content\n\nAll content should be supported by valid sources. Please, research carefuly and validate all your references. Check that there are no copyright or license issues regarding the use of each of your sources.\n\nWhen using online resources, make a \"hard-copy\" (screen-capture, pdf-printed version) of them to avoid missing resources in the future (Error 404)\n\nThe Interactive Book is not intended as a thorough textbook, but its main focus is on interactive content. Try to keep the content to the amount necessary to understand the topic and cite appropriate references for further study on the subject.\n\nTo keep the consistency of the book, please follow the style indicated in these guidelines, also use the templates and existing content as example.\n\n\n### Quality assurance\n\nBesides researching and validating your sources and adhering to the Interactive Book style, please also edit (remove unnecesary content) and check for spelling and grammar errors. Then, forward your contribution to a third party for proofreading. When you are satisfied with the quality and accuracy of your contribution ask for peer reviewers from the CircuitVerse community. Finally, submit your work to the \"formal\" review by creating a pull-request of your forked repository\n\n---\n\n\n## Style manual\n\n[Work in Progress]\n\nWhile most of the formatting style is carried out automatically by the layout, templates and stylesheets in the SSG (jekyll) pipeline, it is important to also adhere to a common writing style in order to assure consistency of the book.\n\nPlease use a neutral and simple language. Avoid slang. Also, technical jargon should be explained.\n\n\n### Figures and tables\n\nFigures and tables should include a description or caption, and they should be commented in text. If a figure or table is not part of the description, probably it is not needed and can be removed from the book.\n\nSee an example in section [Templates and examples](#templates-and-examples).\n\n\n### Equations\n\nEquations can be typesetted in LaTeX to be rendered by [MathJax](https://www.mathjax.org/). A few examples can be found in the [Templates and examples](#templates-and-examples) section. If equations need to be referenced in the text, use numbered or labelled equations. General useful guidelines:\n\n- There are many variants for the notation of negated variables, prefer the bar over the variable or the *prime* (complement) symbol.\n- For binary words with many literals (high number of bits) prefer a single letter with subscript numbers for each bit. For instance a four bit word can be written as $(x_3,x_2,x_1,x_0)$ instead of a different letter for each literal $(A,B,C,D)$. The least significant bit should be subscripted with 0. In case of ambiguity or if the opposite arrangement is necessary, use a superscript mark to identify the MSB and LSB: $(x_0^{\\text{MSB}},x_1,x_2,x_3^{\\text{LSB}})$\n\n\n### References\n\nInformation obtained from other sources should be properly cited and referenced. Check the [Templates and examples](#templates-and-examples) section for examples.\n\nCurrently, references are formatted according to the `ieee-with-url` style defined [CiteProc-Ruby](https://github.com/inukshuk/citeproc-ruby).\n\n---\n\n\n## Templates and examples\n\nTo help you with getting started with writing your contribution, please use the following templates and examples, accordingly:\n\n- New chapter template\n- New section template\n- Equations example\n- Figure example\n- Table example\n- Bibliography example\n\nThe structure of the book, as well as the navigation menu is handled by Jekyll's Just-the-docs plugin. A two-level structure has been chosen. The top-level is for chapters and the second level is for sections in a chapter\n\n\n### New chapter template\n\nEach chapter should be a directory under the `docs` directory. The `index.md` in the chapter directory is used for the Chapter text and table of contents.\n\nThe `index.md` file should have the following contents:\n\n```markdown\n---\nlayout: circuitverse\ntitle: Chapter title\nnav_order: 10\nhas_children: true\nhas_toc: false\n---\n\n# Long chapter title\n{: .no_toc}\n\nIntroductory chapter text\n\n## Chapter contents\n{: .no_toc .text-delta}\n\n{% raw %}\n{% include chapter_toc.html %}\n{% endraw %}\n```\n\nThe `title` and `nav_order` elements in the front matter should be filled-in accordingly. The other elements in the front matter block should be left as in the template above.\n\nAfter the \"Long\" chapter title an introductory text for the chapter can be added. It is followed by the Table of contents' title and the include directive which create a list of links to the sections of the chapter according to their level and navigation order.\n\n\n### New section template\n\nFiles for sections within a chapter must be placed in the chapter's directory. Currently, the contents of the book are organised in three levels of difficulty:\n\n1. basic\n2. medium\n3. advanced\n\nThe following code represents an empty section and can be used as a template\n\n```markdown\n---\nlayout: circuitverse\ntitle: Section title\nnav_order: l0s000\ncvib_level: advanced\nparent: Parent Chapter\nhas_children: false\n---\n\n# Long section title\n{: .no_toc}\n\n## Table of contents\n{: .no_toc .text-delta}\n\n1. TOC\n{:toc}\n\n## Title\n\nText\n```\n\nEach section file should have content belonging only to one of these levels. Therefore, the front matter block presents the `cvib_level` which correspond to one of the levels.\n\nThe `parent` element must be the name of the Chapter containing the section.\n\nThe section's title goes in the `title` element.\n\nTo standardise the order of sections in the navigation bar according to the difficulty level, the `nav_order` element is an \"encoded\" string with this form: `l`, followed by a digit, followed by `s`, followed by three digits. `l` stands for *level* and is a number between 0 and 2 and represent the basic, medium and advanced level, respectively. `s` stands for *section* and the three digit number after it is a zero-padded number indicating the section position in the chapter.\n\nThe other elements in the front matter block (`layout`, `has_children`) should be should be left as they are.\n\nAfter the front matter block comes the section's title followed by the table of contents title and code to generate it automatically.\n\nAfter that, parts within the section can be added.\n\n\n### Equations examples\n\nEquations are processed using MathJax provided by the [jekyll-spaceship](https://github.com/jeffreytse/jekyll-spaceship#2-mathjax-usage) plugin.\n\nThe following are valid mathematical formulas and their rendering:\n\n1. Unnumbered equations\n\n ```latex\n $ f(x)=\\int_2^n \\frac{1}{2\\pi}x^2 $\n ```\n \n $ f(x)=\\int_2^n \\frac{1}{2\\pi}x^2 $\n \n ```latex\n $ F(A,B,C) = A + B \\cdot C $\n ```\n \n $ F(A,B,C) = A + B ⋅ C $\n\n2. Automatically numbered equations\n\n ```latex\n $ \\begin{equation}F(x_0, x_1) = x_0 + \\overline{x_1} \\end{equation} $\n ```\n \n $ \\begin{equation}F(x_0, x_1) = x_0 + \\overline{x_1} \\end{equation} $\n\n3. Manually numbered equations\n\n ```latex\n \\\\[ F(A,B,C) = A + B \\cdot C \\tag{1}\\\\]\n ```\n \n \\\\[ F(A,B,C) = A + B \\cdot C \\tag{1}\\\\]\n\n\n### Figure examples\n\nNeither Jekyll nor Markdown provide facilities to add captions to images. Therefore, instead of using direct inclusion of images in MD syntax, the Jekyll include mechanism is used to generate properly captioned figures in the HTML rendering. To use it, include a figure like this:\n\n```markdown\n{% raw %}\n{% include image.html url=\"/assets/images/path-to-image/image.svg\" description=\"Image caption\" %}\n{% endraw %}\n```\n\nFor instance, the following code:\n\n```markdown\n{% raw %}\n{% include image.html url=\"/assets/images/XnorGate.svg\" description=\"XNOR Gate\" %}\n{% endraw %}\n```\n\nwill render like this:\n\n{% include image.html url=\"/assets/images/XnorGate.svg\" description=\"XNOR Gate\" %}\n\n\n### Table examples\n\nComplex tables can be handled by the [jekyll-spaceship](https://github.com/jeffreytse/jekyll-spaceship#1-table-usage) plugin. However, there is no support for table captions. As a workaround, the Table caption can be included in the first row and styled with the table caption CSS `tblcap`.\n\n```markdown\n|: Table: Sample data {: .tblcap} :|||\n| col 1 | col 2 | col 3 |\n|:-----------:|:------------|------------:|\n| row1 col2 | row1 col2 | row1 col3 |\n| row2 col1 | row2 col2 | row2 col3 |\n```\n\nwhich renders to\n\n|: Table: Sample data {: .tblcap} :|||\n| col 1 | col 2 | col 3 |\n|:-----------:|:------------|------------:|\n| row1 col2 | row1 col2 | row1 col3 |\n| row2 col1 | row2 col2 | row2 col3 |\n\n\n### Bibliography example\n\nBibliographic references are processed by the `jekyll-scholar` plugin. It uses BibTeX files as the source for the references. To use it, the following must be done:\n\n1. Add the the BibTeX file to the `_bibliography/` directory. Care must be taken to not overwrite other bib files in the directory.\n2. If the BibTex file is named `myrefs.bib`, then\n 1. To cite a reference use this tag:\n \n ```markdown\n {% raw %}\n {% cite refid --file myrefs %}\n {% endraw %}\n ```\n \n where `refid` is the BibTeX id of the reference and `myfile` is the BibTeX file without the `.bib` extension.\n 2. To print the list of references cited in the section use the following code:\n \n ```markdown\n ## References\n {% raw %}\n {% bibliography --cited --file myrefs %}\n {% endraw %}\n ```\n\nCheck the [plugin page](https://github.com/inukshuk/jekyll-scholar) for more complex uses.\n\n---\n\n\n## Code of conduct, interacting with the community / etiquette\n\nRefer to the Contributor Covenant Code of Conduct\n\n---\n\n\n## Tools\n\nThe following tools will be useful to work in your contribution.\n\n\n### Git\n\nThe CircuitVerse Interactive Book's sources are hosted in a GitHub repository. Besides the web interface provided by GitHub, other tools to manipulate git repositories can be used:\n\n- [Git](https://git-scm.com/)\n\n\n### Jekyll\n\nThe Interactive Book is rendered using the Jekyll Static Site Generator (SSG). The GitHub repository is able to create a live version of the book using the continuous integration (CI) / continuous deployment (CD) workflows.\n\nTo work locally, you will need to setup a Jekyll development environment. It is posible to setup Jekyll natively for your OS or run it in a docker container.\n\n1. Workflow with native Jekyll\n\n 1. Install Jekyll following the official [documentation](https://jekyllrb.com/docs/). If you don't have time to follow the quickstart guide, the following steps might help you:\n 1. Install Ruby if necessary\n 2. Install Jekyll using the command line: `gem install jekyll`\n 3. Install Bundler: `gem install bundle`\n 4. Change (`cd`) to your local repository directory and install the project dependencies: `bundle install`\n 2. Serve a live local copy at , running: `bundle exec jekyll serve`\n 3. Point your browser to the url above and see your changes live.\n\n2. Workflow using docker\n\n 1. Install and setup docker if you haven't done so already\n 2. Change (`cd`) to your local repository directory and Run a jekyll docker image ([BretFisher's images](https://github.com/BretFisher/jekyll-serve) have been tested and work well with the book). `docker run --rm -p 4000:4000 -v $(pwd):/site bretfisher/jekyll-serve`\n 3. Point your browser to the appropriate url, if you use the command suggested previously.\n", + "front_matter": { + "layout": "default", + "nav_order": 1000, + "title": "Guidelines", + "disable_comments": true, + "description": "" + }, + "front_matter_defaults": { + }, + "http_url": "https://learn.circuitverse.org/contributing_guidelines.html", + "api_url": "https://learn.circuitverse.org/_api/pages/contributing_guidelines.md" +} \ No newline at end of file diff --git a/test/setup/test_data/mock_binary.html b/test/setup/test_data/mock_binary.html new file mode 100644 index 00000000..0f30a306 --- /dev/null +++ b/test/setup/test_data/mock_binary.html @@ -0,0 +1,79 @@ +
+
+
Binary
+
128
0
+
64
0
+
32
0
+
16
0
+
8
0
+
4
0
+
2
0
+
1
0
+
Decimal
0
+
+
+ \ No newline at end of file diff --git a/test/setup/test_data/mock_ib_raw_page.dart b/test/setup/test_data/mock_ib_raw_page.dart new file mode 100644 index 00000000..9d811e9a --- /dev/null +++ b/test/setup/test_data/mock_ib_raw_page.dart @@ -0,0 +1,48 @@ +Map mockIbRawPage1 = { + 'name': 'index.md', + 'path': 'index.md', + 'relative_path': 'index.md', + 'layout': 'home', + 'title': 'Home', + 'nav_order': 1, + 'description': '', + 'permalink': '/', + 'dir': '/', + 'url': '/', + 'http_url': 'https://learn.circuitverse.org/', + 'api_url': 'https://learn.circuitverse.org/_api/pages/index.md' +}; + +Map mockIbRawPage2 = { + 'name': 'index.md', + 'path': 'docs/binary-representation/index.md', + 'relative_path': 'docs/binary-representation/index.md', + 'layout': 'circuitverse', + 'title': 'Binary representation', + 'nav_order': 10, + 'has_children': true, + 'has_toc': false, + 'dir': '/docs/binary-representation/', + 'url': '/docs/binary-representation/', + 'http_url': 'https://learn.circuitverse.org/docs/binary-representation/', + 'api_url': + 'https://learn.circuitverse.org/_api/pages/docs/binary-representation/index.md' +}; + +Map mockIbRawPage3 = { + 'name': 'binary-numbers.md', + 'path': 'docs/binary-representation/binary-numbers.md', + 'relative_path': 'docs/binary-representation/binary-numbers.md', + 'layout': 'circuitverse', + 'title': 'Binary numbers', + 'nav_order': 'l0s000', + 'cvib_level': 'basic', + 'parent': 'Binary representation', + 'has_children': false, + 'dir': '/docs/binary-representation/', + 'url': '/docs/binary-representation/binary-numbers.html', + 'http_url': + 'https://learn.circuitverse.org/docs/binary-representation/binary-numbers.html', + 'api_url': + 'https://learn.circuitverse.org/_api/pages/docs/binary-representation/binary-numbers.md' +}; diff --git a/test/setup/test_data/mock_ib_raw_page_data.dart b/test/setup/test_data/mock_ib_raw_page_data.dart new file mode 100644 index 00000000..37afecbf --- /dev/null +++ b/test/setup/test_data/mock_ib_raw_page_data.dart @@ -0,0 +1,26 @@ +Map mockIbRawPageData1 = { + 'name': 'index.md', + 'path': 'index.md', + 'relative_path': 'index.md', + 'layout': 'home', + 'title': 'Home', + 'nav_order': 1, + 'description': '', + 'permalink': '/', + 'content': + '

Interactive-Book

\n\n

Learn Digital Logic Design easily.

\n\n

The Computer Logical Organization is basically the abstraction which is below the operating system and above the digital logic level.\nNow at this point, the important points are the functional units/subsystems that refer to some hardware which is made up of lower level building blocks.

\n\n

This interactive book gives a complete understanding on Computer Logical Organization starting from basic computer overview till the advanced level.\nThis book is aimed to provide the knowledge to the reader on how to analyze the combinational and sequential circuits and implement them. You can use the combinational circuit/sequential circuit/combination of both the circuits, as per the requirement.\nAfter completing this book, you will be able to implement the type of digital circuit, which is suitable for specific application.

\n\n
\n\n

Audience

\n\n

This book is mainly prepared for the students who are interested in the concepts of digital circuits and Computer Logical Organization. Digital circuits contain a set of Logic gates and these can be operated with binary values, 0 and 1.

\n\n

Prerequisites

\n

Before you start learning from this Book, I hope that you have some basic knowledge about computers and how they work.\nA basic idea regarding the initial concepts of Digital Electronics is enough to understand the topics covered in this tutorial.

\n', + 'dir': '/', + 'url': '/', + 'raw_content': + '# Interactive-Book\n{: .fs-9 }\n\nLearn Digital Logic Design easily.\n{: .fs-6 .fw-300 }\n\nThe Computer Logical Organization is basically the abstraction which is below the operating system and above the digital logic level.\nNow at this point, the important points are the functional units/subsystems that refer to some hardware which is made up of lower level building blocks.\n\nThis interactive book gives a complete understanding on Computer Logical Organization starting from basic computer overview till the advanced level.\nThis book is aimed to provide the knowledge to the reader on how to analyze the combinational and sequential circuits and implement them. You can use the combinational circuit/sequential circuit/combination of both the circuits, as per the requirement.\nAfter completing this book, you will be able to implement the type of digital circuit, which is suitable for specific application.\n\n---\n\n## Audience\n\nThis book is mainly prepared for the students who are interested in the concepts of digital circuits and Computer Logical Organization. Digital circuits contain a set of Logic gates and these can be operated with binary values, 0 and 1.\n\n### Prerequisites\nBefore you start learning from this Book, I hope that you have some basic knowledge about computers and how they work.\nA basic idea regarding the initial concepts of Digital Electronics is enough to understand the topics covered in this tutorial.\n', + 'front_matter': { + 'layout': 'home', + 'title': 'Home', + 'nav_order': 1, + 'description': '', + 'permalink': '/' + }, + 'front_matter_defaults': {}, + 'http_url': 'https://learn.circuitverse.org/', + 'api_url': 'https://learn.circuitverse.org/_api/pages/index.md' +}; diff --git a/test/setup/test_data/mock_module.js b/test/setup/test_data/mock_module.js new file mode 100644 index 00000000..ccc8c339 --- /dev/null +++ b/test/setup/test_data/mock_module.js @@ -0,0 +1,2356 @@ +var bit1 = new Array(8); +var bit1_display = new Array(2); +bit1_display[false] = "0"; +bit1_display[true] = "1"; +var operator = "OR"; + +function toggle_bitc(column) +{ + var decimal1 = 0; + document.getElementById(column+"c").innerHTML = bit1_display[bit1[column] = !bit1[column]]; + for(var i=0; i < 8; i++) + { + if(bit1[i]) { decimal1 = decimal1 + Math.pow(2, i); } + } + document.getElementById("decimal1").innerHTML = decimal1; +} + +var bit = new Array(16); +var bit_display = new Array(2); +bit_display[false] = "0"; +bit_display[true] = "1"; +bit.fill(false); + +function set_bits() +{ + if(isNaN(document.getElementById("value_A").value) || document.getElementById("value_A").value > 255 || document.getElementById("value_A").value < 0 || isNaN(document.getElementById("value_B").value) || document.getElementById("value_B").value < 0 || document.getElementById("value_B").value > 255) + { + document.getElementById("value_A").value = 0; + document.getElementById("value_B").value = 0; + alert("Only numbers between 0 and 255 can be entered."); + set_bits(); + } + else + { + for(var i=0; i < 8; i++) + { + if((document.getElementById("value_A").value&Math.pow(2,i))>0) { bit_value = true; } else { bit_value = false; } + document.getElementById(i).innerHTML = bit_display[bit[i] = bit_value]; + if((document.getElementById("value_B").value&Math.pow(2,i))>0) { bit_value = true; } else { bit_value = false; } + document.getElementById(i+8).innerHTML = bit_display[bit[i+8] = bit_value]; + } + do_bitwise(); + } +} + +function toggle_bit(column) +{ + var decimal = 0; + document.getElementById(column).innerHTML = bit_display[bit[column] = !bit[column]]; + for(var i=0; i < 16; i++) + { + if(bit[i]) { decimal = decimal + Math.pow(2, i); } + } + document.getElementById("value_A").value = decimal&255; + document.getElementById("value_B").value = Math.floor(decimal/256); + do_bitwise(operator); +} + +function change_operator(oper, ind) +{ + var tab = document.querySelectorAll( '.oper' ); + for(var i=0; i numOf(text, ')')) + text += ")"; + let variables = []; + for (i = 0; i < text.length; i++) { + if ((text[i] >= 'A' && text[i] <= 'Z')) { + if (text.indexOf(text[i]) == i) { + variables.push(text[i]); + } + } + } + variables.sort(); + if (variables.length > 8) { + placeholder.innerHTML = "

You can only have 8 variables at a time.

"; + return; + } + let string = "minterm"; + for (i = 0; i < variables.length; i++) { + string += "" + variables[i] + ""; + } + string += "" + text + ""; + for (i = 0; i < Math.pow(2, variables.length); i++) { + string += ""+i.toString()+""; + let data = []; + for (j = 0; j < variables.length; j++) { + data[j] = Math.floor(i / Math.pow(2, variables.length - j - 1)) % 2; + string += "" + data[j] + ""; + } + let equation = text; + for (j = 0; j < variables.length; j++) { + equation = equation.replace(new RegExp(variables[j], 'g'), data[j]); + } + string += "" + solve(equation) + ""; + } + string = "" + string + "
"; + if (string.indexOf("") == -1) + placeholder.innerHTML = string; + else + placeholder.innerHTML = "

Invalid expression.

"; + + function numOf(text, search) { + let count = 0; + for (let i = 0; i < text.length; i++) + if (text[i] == search) + count++; + return count; + } + + function solve(equation) { + while (equation.indexOf("(") != -1) { + let start = equation.lastIndexOf("("); + let end = equation.indexOf(")", start); + if (start != -1) + equation = equation.substring(0, start) + + solve(equation.substring(start + 1, end)) + + equation.substring(end + 1); + } + equation = equation.replace(/''/g, ''); + equation = equation.replace(/0'/g, '1'); + equation = equation.replace(/1'/g, '0'); + for (let i = 0; i < equation.length - 1; i++) + if ((equation[i] == '0' || equation[i] == '1') && (equation[i + 1] == '0' || equation[i + 1] == '1')) + equation = equation.substring(0, i + 1) + '*' + equation.substring(i + 1, equation.length); + try { + let safeEval = eval; + let answer = safeEval(equation); + if (answer == 0) + return 0; + if (answer > 0) + return 1; + return ''; + } catch (e) { + return ''; + } + } +} + + +var subject_name = new Array("English", "Maths", "Science", "Computing", "History", "Geography", "French", "German"); + +function decode() +{ + if(isNaN(document.getElementById("homework").value) | document.getElementById("homework").value < 0 | document.getElementById("homework").value > 255) + { + alert("The code must be a number between 0 and 255"); + } + else + { + var subject_text; + if(document.getElementById("homework").value == 0) + { + subject_text = "

There is no homework today.

"; + } + else + { + subject_text = "

Today's homework:

    "; + for(var i = 0; i<=78; i++) + { + if(document.getElementById("homework").value & Math.pow(2, i)) { subject_text = subject_text + "
  • " + subject_name[i] + "
  • "; } + } + subject_text = subject_text + "
"; + } + document.getElementById("subjects").innerHTML = subject_text; + document.getElementById("subjects").style.height = "auto"; + } +} + +function update_display() +{ + var values = document.querySelectorAll('input[type="text_2"]'); + var pixels = document.querySelectorAll('.pixel'); + + for(var i = 0; i<64; i+=8) + { + if(isNaN(values[i/8].value) || values[i/8].value > 255 || values[i/8].value < 0) + { + alert("Only enter numbers between 0 and 255."); + values[i/8].select(); + } + else + { + for(var n = 0; n<8; n++) + { + if(values[i/8].value & Math.pow(2,n)) + { + pixels[i+(7-n)].style.backgroundColor = "#404040"; + } + else + { + pixels[i+(7-n)].style.backgroundColor = "#F0F0F0"; + } + } + } + } +} + + +function PetrickMethod() +{ + this.problem; + this.maxProblemSize = 100; + this.solution; + this.log = ""; + var that = this; + + this.test = function() { + var andArray = new Array(); + var orArray; + var monomA; + var monomB; + orArray = new Array(); + monomA = new Object(); // using objects ensures that (x and x) = x + monomA[1] = 1; + orArray.push(monomA); + monomB = new Object(); + monomB[2] = 2; + orArray.push(monomB); + andArray.push(orArray); + orArray = new Array(); + monomA = new Object(); + monomA[3] = 3; + orArray.push(monomA); + monomB = new Object(); + monomB[4] = 4; + orArray.push(monomB); + andArray.push(orArray); + orArray = new Array(); + monomA = new Object(); + monomA[1] = 1; + orArray.push(monomA); + monomB = new Object(); + monomB[3] = 3; + orArray.push(monomB); + andArray.push(orArray); + orArray = new Array(); + monomA = new Object(); + monomA[5] = 5; + orArray.push(monomA); + monomB = new Object(); + monomB[6] = 6; + orArray.push(monomB); + andArray.push(orArray); + orArray = new Array(); + monomA = new Object(); + monomA[2] = 2; + orArray.push(monomA); + monomB = new Object(); + monomB[5] = 5; + orArray.push(monomB); + andArray.push(orArray); + orArray = new Array(); + monomA = new Object(); + monomA[4] = 4; + orArray.push(monomA); + monomB = new Object(); + monomB[6] = 6; + orArray.push(monomB); + andArray.push(orArray); + /*orArray = new Array(); + monomA = new Object(); + monomA[4] = 4; + orArray.push(monomA); + monomB = new Object(); + monomB[4] = 4; + orArray.push(monomB); + andArray.push(orArray);*/ + + this.solve(andArray); + }; + + this.solve = function(eq) { + + this.problem = eq; + this.log = ""; + + //printEqnArray(eq); + printEqnArrayFancy(eq); + + // multiply out + var andArray = eq; + var loopCounter = 0; + while (andArray.length > 1) { + var newAndArray = new Array(); + for (var i = 1; i < andArray.length; i += 2) { + + var orTermA = andArray[i - 1]; + var orTermB = andArray[i]; + var newOrArray = new Array(); + for (var a = 0; a < orTermA.length; a++) { + for (var b = 0; b < orTermB.length; b++) { + var monom1 = orTermA[a]; + var monom2 = orTermB[b]; + var resultingMonom = new Object(); + for (var m in monom1) { + resultingMonom[monom1[m]] = monom1[m]; + } + for (var n in monom2) { + resultingMonom[monom2[n]] = monom2[n]; + } + newOrArray.push(resultingMonom); + } + } + + newAndArray.push(newOrArray); + } + // if uneven copy last and-term + if (andArray.length % 2 === 1) { + newAndArray.push(andArray[andArray.length - 1]); + } + //printEqnArray(newAndArray); + printEqnArrayFancy(newAndArray); + + andArray.length = 0; + // simplify or-term + for (var i = 0; i < newAndArray.length; i++) { + var orTerm = newAndArray[i]; + var newOrTerm = simplifyOrTerm(orTerm); + if (newOrTerm.length > 0) { + andArray.push(newOrTerm); + } + } + + var problemSize = eqnArrayProblemSize(andArray); + if (problemSize > this.maxProblemSize) { + console.log("Error: The cyclic covering problem is too large to be solved with Petrick's method (increase maxProblemSize). Size=" + problemSize); + return false; + } + + //printEqnArray(andArray); + printEqnArrayFancy(andArray); + loopCounter++; + } + this.solution = andArray; + return true; + }; + + function simplifyOrTerm(orTerm) { + // find a monom that is the same or simpler than another one + var newOrTerm = new Array(); + var markedForDeletion = new Object(); + for (var a = 0; a < orTerm.length; a++) { + var keepA = true; + var monomA = orTerm[a]; + for (var b = a + 1; b < orTerm.length && keepA; b++) { + var monomB = orTerm[b]; + var overlapBoverA = 0; + var lengthA = 0; + for (var m in monomA) { + if (monomB[m] in monomA) { + overlapBoverA++; + } + lengthA++; + } + + var overlapAoverB = 0; + var lengthB = 0; + for (var m in monomB) { + if (monomA[m] in monomB) { + overlapAoverB++; + } + lengthB++; + } + + if (overlapBoverA === lengthB) { + keepA = false; + } + + if (lengthA < lengthB && overlapAoverB === lengthA) { + markedForDeletion[b] = b; + } + + } + if (keepA) { + if (a in markedForDeletion) { + // do nothing + } else + newOrTerm.push(orTerm[a]); + } + } + return newOrTerm; + } + + + function printEqnArrayFancy(andArray) { + var str = ""; + for (var i = 0; i < andArray.length; i++) { + var first = true; + str += "("; + var orArray = andArray[i]; + for (var j = 0; j < orArray.length; j++) { + if (!first) + str += " ∨ "; + var monom = orArray[j]; + for (var k in monom) { + str += "p"+ monom[k] + ""; + } + first = false; + } + str += ")"; + } + if(that.log.length > 0) { + that.log += "

⇔ " + str + "

"; + }else{ + that.log += "

"+ str + "

"; + } + } + + function eqnArrayProblemSize(andArray) { + var monomCounter = 0; + for (var i = 0; i < andArray.length; i++) { + var orArray = andArray[i]; + monomCounter += orArray.length; + } + return monomCounter; + } + + + function printEqnArray(andArray) { + var str = ""; + for (var i = 0; i < andArray.length; i++) { + var first = true; + str += "("; + var orArray = andArray[i]; + for (var j = 0; j < orArray.length; j++) { + if (!first) + str += " or "; + var monom = orArray[j]; + for (var k in monom) { + str += monom[k]; + } + first = false; + } + str += ")"; + } + console.log(str); + } + +} + +function PrimTerm() { + this.implicant = -1; + this.termString = ""; + this.color = [0, 0, 0]; + this.coloredTermString = ""; + this.used = false; + this.neededByVar = new Object; +} + +function Implicant() { + this.imp = new Object(); + this.isPrim = false; + this.isOnlyDontCare = false; + this.bitMask = 0; +} + +function ImplicantGroup() { + this.group = new Array; + this.order = -1; +} + +function PrimTermTable(ord) { + this.essentialPrimTerms = new Array(); + this.order = ord; + this.remainingVars = new Array();; + this.remainingPrimTerms = new Array(); + this.supersededPrimTerms = new Array(); +} + +function hsvToRgb(h, s, v) { + + var r, g, b; + var i = Math.floor(h * 6); + var f = h * 6 - i; + var p = v * (1 - s); + var q = v * (1 - f * s); + var t = v * (1 - (1 - f) * s); + + switch (i % 6) { + case 0: + r = v, g = t, b = p; + break; + case 1: + r = q, g = v, b = p; + break; + case 2: + r = p, g = v, b = t; + break; + case 3: + r = p, g = q, b = v; + break; + case 4: + r = t, g = p, b = v; + break; + case 5: + r = v, g = p, b = q; + break; + } + + return [ Math.floor(r * 255), Math.floor(g * 255), Math.floor(b * 255) ]; +} + +function QuineMcCluskeyDataCtrl() { + this.noOfVars = -1; + this.funcdata = new Array; + this.primTerms = new Array; + this.implicantGroups = new Array; + this.minimalTerm = ""; + this.coloredMinimalTerm = ""; + this.minimalTermPrims = new Array; + this.primTermTables = new Array; + this.petrickSolver = new PetrickMethod(); + this.petrickTermPrims = new Array; + this.allowDontCare = false; + + this.init = function(no) { + this.noOfVars = no; + this.funcdata.length = 0; + this.primTerms.length = 0; + this.implicantGroups.length = 0; + this.minimalTerm = "0"; + this.coloredMinimalTerm = "0"; + this.minimalTermPrims.length = 0; + this.primTermTables.length = 0; + this.petrickTermPrims.length = 0; + + var noOfFuncData = Math.pow(2, this.noOfVars); + for (var i = 0; i < noOfFuncData; i++) { + this.funcdata.push(0); + } + + //this.petrickSolver.test(); + + }; + + this.setFuncData = function(i, val) { + if (i < 0 || i >= this.funcdata.length) + return; + this.funcdata[i] = val; + }; + + this.activated = function(i) { + if (i < 0 || i >= this.funcdata.length) + return; + + this.funcdata[i] += 1; + if(this.allowDontCare) { + if (this.funcdata[i] > 2) this.funcdata[i] = 0; + }else{ + if (this.funcdata[i] > 1) this.funcdata[i] = 0; + } + this.compute(); + }; + + this.random = function() { + for (var i = 0; i < this.funcdata.length; i++) { + if(this.allowDontCare) { + this.funcdata[i] = Math.floor(Math.random() * 3); + }else{ + this.funcdata[i] = Math.floor(Math.random() * 2); + } + } + this.compute(); + }; + + this.clear = function() { + for (var i = 0; i < this.funcdata.length; i++) { + this.funcdata[i] = 0; + } + this.compute(); + }; + + function bitCount(value) { + var counter = 0; + while (value > 0) { + if ((value & 1) === 1) counter++; + value >>= 1; + } + return counter; + } + + this.compute = function() { + this.primTerms.length = 0; + this.implicantGroups.length = 0; + this.minimalTerm = "0"; + this.coloredMinimalTerm = "0"; + this.minimalTermPrims.length = 0; + this.primTermTables.length = 0; + this.petrickTermPrims.length = 0; + + var counter = 0; + var lastIg = -1; + var continueLoop = true; + while(continueLoop) { + + continueLoop = false; + var ig = new ImplicantGroup(); + + if(counter === 0) { + for (var i = 0; i < this.funcdata.length; i++) { + if(this.funcdata[i] > 0) { + var impl = new Implicant(); + impl.imp[i] = i; + impl.isPrim = true; + ig.group.push(impl); + continueLoop = true; + } + } + }else{ + + for (var i = 0; i < lastIg.group.length; i++) { + for (var j = i+1; j < lastIg.group.length; j++) { + var imp1 = lastIg.group[i]; + var imp2 = lastIg.group[j]; + + if (imp1.bitMask === imp2.bitMask) { + + var found = false; + var xor = -1; + for (var m in imp1.imp) { + for (var n in imp2.imp) { + var i1 = imp1.imp[m]; + var i2 = imp2.imp[n]; + //console.log(i1 + "<->" + i2); + xor = (i1 ^ i2) & (~imp1.bitMask); + if (bitCount(xor) === 1) { + //console.log("found merge candidate" + i1 + "<->" + i2); + found = true; + } + break; + } + break; + } + if (found) { + imp1.isPrim = false; + imp2.isPrim = false; + + var impl = new Implicant(); + impl.isPrim = true; + impl.bitMask = imp1.bitMask | xor; + for (var m in imp1.imp) + impl.imp[m] = parseInt(m); + for (var n in imp2.imp) + impl.imp[n] = parseInt(n); + + var foundMatch = false; // determine if this combination is already there + for(var k=0; k < ig.group.length; k++) { + var exist = ig.group[k]; + var isTheSame = true; + for(var m in impl.imp) { + var found = false; + for (var n in exist.imp) { + if(parseInt(m) === parseInt(n)) { + found = true; + } + } + if(!found) { + isTheSame = false; + break; + } + } + if(isTheSame) { + foundMatch = true; + break; + } + } + if(!foundMatch) { + ig.group.push(impl); + continueLoop = true; + } + } + } + } + } + } + + if(continueLoop) this.implicantGroups.push(ig); + lastIg = ig; + counter++; + } + + // collect primterms + this.primTerms.length = 0; + this.minimalTermPrims.length = 0; + var color = 0.0; + for(var i= this.implicantGroups.length-1; i >=0; i--) { + var g = this.implicantGroups[i].group; + + for(var j=0; j < g.length; j++) { + if(g[j].isPrim) { + + // prim terms introduced by don't cares + // must have at least one 1 + var containsOne = false; + var allFuncPrimTerm = g[j].imp; + for(var kk in allFuncPrimTerm) { + var k = allFuncPrimTerm[kk]; + if(this.funcdata[k] === 1) { + containsOne = true; + } + } + + if(!containsOne){ + g[j].isOnlyDontCare = true; + } else { + var primTerm = new PrimTerm(); + primTerm.implicant = g[j]; + + // extract minTerm as string + for (var thisVal in primTerm.implicant.imp) { + var minTerm = ""; + var one = 1; + var needed = (~primTerm.implicant.bitMask); + for (var v = 0; v < this.noOfVars; v++) { + if ((needed & one) === one) { + if ((thisVal & one) === one) { + minTerm = "x" + v + "" + minTerm; + } else { + minTerm = "" + v + "" + minTerm; + } + } + one = one << 1; + } + minTerm = "(" + minTerm + ")"; + if (primTerm.implicant.bitMask === Math.pow(2, this.noOfVars) - 1) + minTerm = "1"; + primTerm.color = hsvToRgb(color, 1.0, 0.5); + color += 0.22; + color = color % 1.0; + + + primTerm.termString = minTerm; + var colorStr = "rgb(" + primTerm.color[0] + "," + primTerm.color[1] + "," + primTerm.color[2] + ")"; + primTerm.coloredTermString = "" + minTerm + ""; + break; + } + + this.primTerms.push(primTerm); + } + } + } + } + + + // looking for essential prime implicants + var remaining = new Object(); + for (var i = 0; i < this.funcdata.length; i++) { + if(this.funcdata[i] === 1) { + remaining[i] = i; + } + } + + this.primTermTables.length = 0; + var primTableLoop = 0; + var primTableFound = (this.primTerms.length > 0); + var cyclicCoveringFound = false; + var primTermTable; + while (primTableFound) { + + primTableFound = false; + + primTermTable = new PrimTermTable(primTableLoop); + for (var r in remaining) { + primTermTable.remainingVars.push(remaining[r]); + } + + if (primTableLoop === 0) { + for (var j = 0; j < this.primTerms.length; j++) { + primTermTable.remainingPrimTerms.push(this.primTerms[j]); + } + } else { + // remove rows + var prevTable = this.primTermTables[primTableLoop-1]; + for(var k=0; k countA) { + superseded = true; + }else{ + if(k > l) { + superseded = true; + } + } + } + + } + } + + if(!superseded) { + primTermTable.remainingPrimTerms.push(prevTable.remainingPrimTerms[k]); + }else{ + prevTable.supersededPrimTerms.push(prevTable.remainingPrimTerms[k]); + } + } + } + } + + if (primTermTable.remainingPrimTerms.length > 0) { + this.primTermTables.push(primTermTable); + var currentTerms = primTermTable.remainingPrimTerms; + + var toBeRemoved = new Object(); + + for (var r in remaining) { + var i = remaining[r]; + var count = 0; + var term = -1; + for (var j = 0; j < currentTerms.length && count < 2; j++) { + if (i in currentTerms[j].implicant.imp) { + term = j; + count++; + } + } + + if (count === 1) { + currentTerms[term].neededByVar[i] = primTableLoop; + if(!currentTerms[term].used) { + this.minimalTermPrims.push(currentTerms[term]); + currentTerms[term].used = true; + primTermTable.essentialPrimTerms.push(currentTerms[term]); + primTableFound = true; + + for (var r in remaining) { + var ii = remaining[r]; + if (ii in currentTerms[term].implicant.imp) { + toBeRemoved[ii] = ii; + } + } + } + } + } + + // remove columns + var tmpRemaining = new Object(); + for (var e in remaining){ + var ee = remaining[e]; + tmpRemaining[ee] = ee; + delete remaining[e]; + } + var remainingCount = 0; + for (var r in tmpRemaining) { + var t = tmpRemaining[r]; + if(!(t in toBeRemoved)) { + remaining [t] = t; + remainingCount++; + } + } + } + + if( remainingCount === 0 ) { + primTableFound = false; // break loop + }else{ + if(!primTableFound) { + cyclicCoveringFound = true; + } + } + + primTableLoop++; + } + + var solutionFound = true; + + // Petrick's Method + if (cyclicCoveringFound) { + //console.log("Cyclic covering found"); + + var andArray = new Array(); + + for (var r in remaining) { + var ii = remaining[r]; + var orArray = new Array(); + + for (var k = 0; k < primTermTable.remainingPrimTerms.length; k++) { + var imp = primTermTable.remainingPrimTerms[k].implicant.imp; + if(ii in imp){ + var monom = new Object(); + monom[k] = k; + orArray.push(monom); + } + } + andArray.push(orArray); + } + + solutionFound = this.petrickSolver.solve(andArray); + + if (solutionFound) { + var solutions = this.petrickSolver.solution[0]; + + var bestSolution = -1; + var bestCount = 10000000; + var bestVarCount = 10000000; + for (var i = 0; i < solutions.length; i++) { + var count = 0; + for (var j in solutions[i]) { + count++; + } + if (count <= bestCount) { // first sort accoring to monom length + + var foundBest = true; + if (count === bestCount) { + var bestVarCountNew = 0; + for (var j in solutions[i]) { + for (var v in primTermTable.remainingPrimTerms[j].implicant.imp) { + bestVarCountNew++; + } + } + if (bestVarCountNew >= bestVarCount) + foundBest = false; + } + + if (foundBest) { + bestCount = count; + bestSolution = i; + bestVarCount = 0; + for (var j in solutions[bestSolution]) { + for (var v in primTermTable.remainingPrimTerms[j].implicant.imp) { + bestVarCount++; + } + } + } + } + } + //console.log("Best solution " + bestSolution); + + var best = solutions[bestSolution]; + for (var b in best) { + var addPrimTerm = primTermTable.remainingPrimTerms[best[b]]; + this.minimalTermPrims.push(addPrimTerm); + this.petrickTermPrims.push(addPrimTerm); + } + } + } + + if (solutionFound) { + this.minimalTerm = ""; + this.coloredMinimalTerm = ""; + var firstL = true; + for (var i = 0; i < this.minimalTermPrims.length; i++) { + if (!firstL) { + this.minimalTerm += " ∨ "; + this.coloredMinimalTerm += " ∨ "; + } + this.minimalTerm += this.minimalTermPrims[i].termString; + this.coloredMinimalTerm += this.minimalTermPrims[i].coloredTermString; + firstL = false; + } + + if (this.minimalTermPrims.length === 0) { + this.minimalTerm = "0"; + this.coloredMinimalTerm = "0"; + } + }else{ + this.minimalTerm = 'Error: The cyclic covering problem is too large (increase the "maxProblemSize" parameter)'; + this.coloredMinimalTerm = 'Error: The cyclic covering problem is too large (increase the "maxProblemSize" parameter)'; + } + }; +} + + + + +function QuineMcCluskey(parentDivId, columns, language) { + var myDiv = -1; + var divId = parentDivId; + this.cols = columns + 1; + this.rows = Math.pow(2, columns); + this.data = new QuineMcCluskeyDataCtrl(); + var that = this; + + var labels; + if(language === 0) { + labels = {ttable:"Truth table", + minExp:"Minimal boolean expression", + impli:"Implicants", + order:"Order", + primChart:"Prime implicant chart", + primChartReduced:"Reduced prime implicant chart (Iteration", + extractedPrims:"Extracted essential prime implicants", + extractedMPrims:"Extracted prime implicants", + petricksM:"Petrick's method"}; + }else{ + labels = {ttable:"Wahrheitstafel", + minExp:"Minimaler boolescher Ausdruck", + impli:"Implikanten", + order:"Ordnung", + primChart:"Primimplikantentafel", + primChartReduced:"Reduzierte Primimplikantentafel (Iteration", + extractedPrims:"Extrahierte essentielle Primimplikanten", + extractedMPrims:"Extrahierte Primimplikanten", + petricksM:"Verfahren von Petrick"}; + + } + + this.init = function() { + + this.data.init(columns); + + myDiv = document.createElement('div'); + if (!myDiv) { + console.log("QuineMcCluskey error: can not create a canvas element"); + myDiv = -1; + } else { + + var parent = document.getElementById(divId); + if (!parent) { + if(divId !== "fakeDivId") { + console.log("QuineMcCluskey error: can not find an element with the given name: " + divId); + } + myDiv = -1; + } else { + document.body.appendChild(myDiv); + parent.appendChild(myDiv); + } + } + this.update(); + }; + + this.setNoOfVars = function(vars) { + var c = parseInt(vars); + if (c < 1 && c > 6) + return; + this.cols = c + 1; + this.rows = Math.pow(2, c); + this.data.init(c); + this.update(); + }; + + this.genRandom = function() { + this.data.random(); + this.update(); + }; + + this.allowDontCares = function(type) { + if(type > 0) { + this.data.allowDontCare = true; + }else{ + this.data.allowDontCare = false; + } + this.data.clear(); + this.update(); + }; + + this.drawImplicantGroup = function(g, parent, primFlag, t, drawPetrickVars) { + var primTermTable = this.data.primTermTables[t]; + var myTable = document.createElement('table'); + myTable.setAttribute('class', 'qmcTableClass'); + var myRow = document.createElement('tr'); + + var cell1h = document.createElement('td'); + cell1h.setAttribute('class', 'qmcTdNoBorder'); + cell1h.innerHTML = ""; + myRow.appendChild(cell1h); + + for (var j = 0; j < this.data.noOfVars; j++) { + var myCell = document.createElement('th'); + myCell.innerHTML = "x" + (this.data.noOfVars-1-j) + ""; + myCell.setAttribute('class', 'qmcHeaderX qmcBit'); + myRow.appendChild(myCell); + } + + if (primFlag) { + for (var i = 0; i < primTermTable.remainingVars.length; i++) { + var cellImph = document.createElement('td'); + cellImph.setAttribute('class', 'qmcTdNoBorder'); + cellImph.innerHTML = primTermTable.remainingVars[i].toString(10); + myRow.appendChild(cellImph); + } + } + + var cellImph = document.createElement('td'); + cellImph.setAttribute('class', 'qmcTdNoBorder'); + cellImph.innerHTML = ""; + myRow.appendChild(cellImph); + + + myTable.appendChild(myRow); + + var iMax = 0; + if(!primFlag) iMax = g.group.length; else iMax = primTermTable.remainingPrimTerms.length; + + for (var i = 0; i < iMax; i++) { + var impl = -1; + if(!primFlag) impl = g.group[i]; else impl = primTermTable.remainingPrimTerms[i].implicant; + var bits = 0; + var mask = impl.bitMask; + + for(var m in impl.imp) { + bits = impl.imp[m]; + break; + } + + myRow = document.createElement('tr'); + + var cell1 = document.createElement('td'); + var cell1Str = ""; + var first = true; + for(var m in impl.imp) { + if(!first) cell1Str += ", "; + cell1Str += impl.imp[m].toString(10); + first = false; + } + cell1.innerHTML = cell1Str + ":"; + cell1.setAttribute('class', 'qmcTdNoBorder'); + myRow.appendChild(cell1); + + var res = bits.toString(2); + for (var j = 0; j < this.data.noOfVars; j++) { + var myCell = document.createElement('td'); + myCell.setAttribute('class', 'qmcBit'); + var str; + + var currentBit = Math.pow(2, (this.data.noOfVars - 1)-j); + + if ((currentBit & mask) === currentBit) { + str = "-"; + myCell.innerHTML = str; + } else { + if (j >= (this.data.noOfVars) - res.length) { + str = res.charAt(j - (this.data.noOfVars - res.length)); + myCell.innerHTML = str; + } else { + str = "0"; + myCell.innerHTML = str; + } + } + myRow.appendChild(myCell); + } + + + if (!primFlag) { + var cellLast = document.createElement('td'); + cellLast.setAttribute('class', 'qmcTdNoBorder'); + if (impl.isPrim) { + cellLast.innerHTML = "✓"; //equivalent ✓ in most browsers + if(impl.isOnlyDontCare){ + cellLast.innerHTML = " (×)" + } + } else { + cellLast.innerHTML = "→"; + } + myRow.appendChild(cellLast); + }else{ + for (var v = 0; v < primTermTable.remainingVars.length; v++) { + var ii = primTermTable.remainingVars[v]; + var cellUsed = document.createElement('td'); + cellUsed.setAttribute('class', 'qmcPrimItem qmcBit'); + if (ii in impl.imp) { + cellUsed.innerHTML = "○"; + if (ii in primTermTable.remainingPrimTerms[i].neededByVar) { + if(primTermTable.remainingPrimTerms[i].neededByVar[ii] === t) { + cellUsed.innerHTML = ""; + } + } + } + + myRow.appendChild(cellUsed); + } + var cellLast = document.createElement('td'); + cellLast.setAttribute('class', 'qmcTdNoBorder'); + cellLast.innerHTML = primTermTable.remainingPrimTerms[i].coloredTermString; + if(drawPetrickVars) { + var pVars = " ≡ p" + i + ""; + cellLast.innerHTML += pVars; + } + + + myRow.appendChild(cellLast); + } + + + myTable.appendChild(myRow); + } + + parent.appendChild(myTable); + }; + + + this.update = function() { + + if(myDiv === -1) return; + + // clean up + var oldInnerDiv = document.getElementById(divId+"_innerDiv"); + if (oldInnerDiv) myDiv.removeChild(oldInnerDiv); + + var myInnerDiv = document.createElement('div'); + myInnerDiv.setAttribute('id', divId+"_innerDiv"); + + + var myTruthTableDiv = document.createElement('div'); + myTruthTableDiv.innerHTML = "
" + labels['ttable'] + ":
"; + myTruthTableDiv.setAttribute('class', 'qmcTableLabelDiv'); + + // re-generate + var myTable = document.createElement('table'); + myTable.setAttribute('class', 'qmcTableClass'); + + var myRow = document.createElement('tr'); + + var cell1h = document.createElement('td'); + cell1h.innerHTML = ""; + cell1h.setAttribute('class', 'qmcTdNoBorder'); + myRow.appendChild(cell1h); + + for (var j = 0; j < this.cols; j++) { + var myCell = document.createElement('th'); + if (j < this.cols - 1) { + myCell.innerHTML = "x" + (this.cols-2-j) + ""; + myCell.setAttribute('class', 'qmcHeaderX qmcBit'); + } else { + myCell.innerHTML = "Y"; + myCell.setAttribute('class', 'qmcHeaderY qmcBit'); + } + myRow.appendChild(myCell); + } + myTable.appendChild(myRow); + + + + for (var i = 0; i < this.rows; i++) { + myRow = document.createElement('tr'); + + var cell1 = document.createElement('td'); + cell1.innerHTML = i.toString(10) + ":"; + cell1.setAttribute('class', 'qmcTdNoBorder'); + myRow.appendChild(cell1); + + var res = i.toString(2); + for (var j = 0; j < this.cols; j++) { + var myCell = document.createElement('td'); + + if (j < this.cols - 1) { // x element + myCell.setAttribute('class', 'qmcBit'); + var str; + if (j >= (this.cols - 1) - res.length) { + str = res.charAt(j - ((this.cols - 1) - res.length)); + myCell.innerHTML = str; + } else { + str = "0"; + myCell.innerHTML = str; + } + } else { // y element + myCell.setAttribute('class', 'qmcBit qmcBitY'); + myCell.setAttribute('title', i); + myCell.onmousedown = function(event) { + myCellMouseDown(event); + }; + + if (this.data.funcdata[i] === 0) { + myCell.innerHTML = "0"; + } + if (this.data.funcdata[i] === 1) { + myCell.innerHTML = "1"; + } + if (this.data.funcdata[i] === 2) { + myCell.innerHTML = "×"; + } + } + myRow.appendChild(myCell); + } + myTable.appendChild(myRow); + } + + myTruthTableDiv.appendChild(myTable); + myInnerDiv.appendChild(myTruthTableDiv); + + + for(var i=0; i < this.data.implicantGroups.length; i++) { + var myImplicantDiv = document.createElement('div'); + myImplicantDiv.innerHTML = "
"+ labels['impli'] + " (" + labels['order'] + " "+i+"):
"; + myImplicantDiv.setAttribute('class', 'qmcTableLabelDiv'); + this.drawImplicantGroup(this.data.implicantGroups[i], myImplicantDiv, false, 0, false); + myInnerDiv.appendChild(myImplicantDiv); + } + + + for (var i = 0; i < this.data.primTermTables.length; i++) { + var resultDiv = document.createElement('div'); + if(i === 0 ) { + resultDiv.innerHTML = "

" + labels['primChart'] + ":"; + } + + resultDiv.setAttribute('class', 'qmcTableResultDiv'); + + var drawPetrickVars = false; + if(this.data.petrickTermPrims.length > 0 && i === this.data.primTermTables.length-1) { + drawPetrickVars = true; + } + + this.drawImplicantGroup(this.data.primTerms, resultDiv, true, i, drawPetrickVars); + + var essPTermsDiv = document.createElement('div'); + var essPTermsStr = ""; + var primTermTable = this.data.primTermTables[i]; + var jj = primTermTable.essentialPrimTerms.length; + for(var j=0; j < jj; j++) { + essPTermsStr += primTermTable.essentialPrimTerms[j].coloredTermString; + if(j !== (jj-1)) essPTermsStr += ", "; + } + if(jj > 0) { + essPTermsDiv.innerHTML = "

" + labels['extractedPrims'] +": " + essPTermsStr + "

"; + essPTermsDiv.setAttribute('class', 'qmcIndent'); + resultDiv.appendChild(essPTermsDiv); + } + + myInnerDiv.appendChild(resultDiv); + } + + if (this.data.petrickTermPrims.length > 0) { + var petrickDiv = document.createElement('div'); + petrickDiv.innerHTML = "

" + labels['petricksM'] + "

"; + + var petrickInnerDiv = document.createElement('div'); + petrickInnerDiv.innerHTML = "" + this.data.petrickSolver.log + ""; + petrickInnerDiv.setAttribute('class', 'qmcIndent'); + petrickDiv.appendChild(petrickInnerDiv); + + var petrickEssTermsDiv = document.createElement('div'); + var petrickEssTermsStr = ""; + var jj = this.data.petrickTermPrims.length; + for (var j = 0; j < jj; j++) { + petrickEssTermsStr += this.data.petrickTermPrims[j].coloredTermString; + if (j !== (jj - 1)) + petrickEssTermsStr += ", "; + } + if (jj > 0) { + petrickEssTermsDiv.innerHTML = "

" + labels['extractedMPrims'] + " (" + labels['petricksM'] + "): " + petrickEssTermsStr + "

"; + petrickEssTermsDiv.setAttribute('class', 'qmcIndent'); + petrickDiv.appendChild(petrickEssTermsDiv); + } + + myInnerDiv.appendChild(petrickDiv); + } + + + var termDiv = document.createElement('div'); + termDiv.innerHTML = "

" + labels['minExp']+ ":

Y = " + this.data.coloredMinimalTerm; +"

"; + myInnerDiv.appendChild(termDiv); + myDiv.appendChild(myInnerDiv); + }; + + function myCellMouseDown(e) { + + var targ; + if (e.target) { + targ = e.target; + } else { // deal with Microsoft + if (e.srcElement) + targ = e.srcElement; + } + if (targ.nodeType === 3) { // deal with Safari + targ = targ.parentNode; + } + var i = parseInt(targ.title); + that.data.activated(i); + + that.update(); + } +} + + +function UIElement(x, y, width, height, type, ref, subref, slotType) { + this.x = x; + this.y = y; + this.x2 = x + width; + this.y2 = y + height; + this.type = type; // 0 = field, 1 = slot, 2 connection + this.ref = ref; +} + +function KVField() { + this.position = [0.0, 0.0]; + this.value = 0; + this.active = false; + this.uniqueID = -1; + this.truthmapID = -1; +} + +function KVBlock() { + this.fieldID = -1; + this.dimx = -1; + this.dimy = -1; + this.used = false; + this.color = [0, 0, 0]; + this.primTerm = ""; +} + +function KarnaughMapDataCtrl(qmcRef) { + this.noOfVars = -1; + this.fieldLines = -1; + this.fieldPerLine = -1; + this.fieldBorder = -1; + this.fieldHeight = 80; + this.fieldWidth = 80; + this.qmc = qmcRef; + this.fields = new Array(); + this.blocks = new Array(); + this.allowDontCare = false; + + this.init = function (no) { + + this.noOfVars = no; + + this.qmc.setNoOfVars(no); + + var noOfEvenVars = Math.floor(this.noOfVars / 2); + var noOfOddVars = Math.floor((this.noOfVars + 1) / 2); + + this.fieldLines = Math.pow(2, noOfEvenVars); + this.fieldPerLine = Math.pow(2, noOfOddVars); + this.fieldBorder = noOfOddVars * 20; + + this.fields.length = 0; + this.blocks.length = 0; + + var id = 0; + for (var i = 0; i < this.fieldLines; i++) { + for (var j = 0; j < this.fieldPerLine; j++) { + var field = new KVField(); + field.position[0] = this.fieldBorder + j * this.fieldWidth; + field.position[1] = this.fieldBorder + i * this.fieldHeight; + field.value = 0; + field.uniqueID = id; + this.fields.push(field); + id++; + } + } + + var mapped = 0; + this.fields[0].truthmapID = 0; + this.fields[1].truthmapID = 1; + var mirrorDirection = 0; + var mirrorXCount = 2; + var mirrorYCount = 1; + var mapped = 2; + var x = 0; + var y = 1; + var loop = 0; + var direction = 0; + while (loop < this.noOfVars - 1) { + for (var xx = 0; xx < mirrorXCount; xx++) { + for (var yy = 0; yy < mirrorYCount; yy++) { + var loc = xx + yy * this.fieldPerLine; + + if (direction === 0) { + var mirrorLoc = (x + xx) + (y + (mirrorYCount - 1) - yy) * this.fieldPerLine; + this.fields[mirrorLoc].truthmapID = this.fields[loc].truthmapID + mirrorXCount * mirrorYCount; + } else { + var mirrorLoc = (x + (mirrorXCount - 1) - xx) + (y + yy) * this.fieldPerLine; + this.fields[mirrorLoc].truthmapID = this.fields[loc].truthmapID + mirrorYCount * mirrorYCount; + } + } + } + if (direction === 0) { + mirrorYCount = mirrorYCount * 2; + x = mirrorXCount; + y = 0; + direction = 1; + } else { + mirrorXCount = mirrorXCount * 2; + y = mirrorYCount; + x = 0; + direction = 0; + } + loop++; + } + + }; + + this.getKVFieldsCount = function () { + return this.fields.length; + }; + + this.getKVFieldPositionX = function (fieldId) { + return this.fields[fieldId].position[0]; + }; + + this.getKVFieldPositionY = function (fieldId) { + return this.fields[fieldId].position[1]; + }; + + this.getKVFieldTruthmapID = function (fieldId) { + return this.fields[fieldId].truthmapID; + }; + + this.getKVFieldValue = function (fieldId) { + return this.fields[fieldId].value; + }; + + this.activated = function (fieldId) { + + this.fields[fieldId].value += 1; + if (this.allowDontCare) { + if (this.fields[fieldId].value > 2) + this.fields[fieldId].value = 0; + } else { + if (this.fields[fieldId].value > 1) + this.fields[fieldId].value = 0; + } + + this.qmc.data.setFuncData(this.fields[fieldId].truthmapID, this.fields[fieldId].value); + this.qmc.data.compute(); + this.qmc.update(); + this.compute(); + }; + + this.random = function () { + for (var i in this.fields) { + if (this.allowDontCare) { + this.fields[i].value = Math.floor(Math.random() * 3); + } else { + this.fields[i].value = Math.floor(Math.random() * 2); + } + this.qmc.data.setFuncData(this.fields[i].truthmapID, this.fields[i].value); + } + this.qmc.data.compute(); + this.qmc.update(); + this.compute(); + }; + + this.clear = function () { + for (var i in this.fields) { + this.fields[i].value = 0; + this.qmc.data.setFuncData(this.fields[i].truthmapID, this.fields[i].value); + } + this.qmc.data.compute(); + this.qmc.update(); + this.compute(); + }; + + this.compute = function () { + + this.blocks.length = 0; + + var localFieldsValues = new Array(); + + for (var m = 0; m < this.qmc.data.minimalTermPrims.length; m++) { + var minPrim = this.qmc.data.minimalTermPrims[m]; + + localFieldsValues.length = 0; + for (var i in this.fields) { + if (this.fields[i].truthmapID in minPrim.implicant.imp) { + localFieldsValues.push(1); + } else { + localFieldsValues.push(0); + } + } + + var maxX = Math.floor(Math.log(this.fieldPerLine) / Math.LN2); + var maxY = Math.floor(Math.log(this.fieldLines) / Math.LN2); + + // this might be computationally expensive (computing all possible blocks) + for (var x = maxX; x >= 0; x--) { + for (var y = maxY; y >= 0; y--) { + var px = Math.pow(2, x); + var py = Math.pow(2, y); + var stepI = Math.max(Math.floor(py / 2), 1); + var stepJ = Math.max(Math.floor(px / 2), 1); + for (var i = 0; i < this.fieldLines; i += stepI) { + for (var j = 0; j < this.fieldPerLine; j += stepJ) { + + var id = i * this.fieldPerLine + j; + + if (localFieldsValues[id] === 1) { + + // search zero + var noZero = true; + for (var xx = 0; xx < px && noZero; xx++) { + for (var yy = 0; yy < py && noZero; yy++) { + var otherId = ((i + yy) % this.fieldLines) * this.fieldPerLine + ((j + xx) % this.fieldPerLine); + if (localFieldsValues[otherId] === 0) + noZero = false; + } + } + + if (noZero) { + var block = new KVBlock(); + block.fieldID = id; + block.dimx = px; + block.dimy = py; + block.color = minPrim.color; + this.blocks.push(block); + if (true) { //clearing all 1s + for (var xx = 0; xx < px; xx++) { + for (var yy = 0; yy < py; yy++) { + var otherId = ((i + yy) % this.fieldLines) * this.fieldPerLine + ((j + xx) % this.fieldPerLine); + localFieldsValues[otherId] = 0; + } + } + } + + } // end if(noZero) + } // end if (localFieldsValues[id] === 1) + } // end j + } // end i + } // end y + } // end x + } // end m + }; +} + +function KarnaughMap(parentDivId, qmcRef) { + var data = new KarnaughMapDataCtrl(qmcRef); + var qmc = qmcRef; + var svg; + var svgns = "http://www.w3.org/2000/svg"; + var divId = parentDivId; + var fieldColor = "rgba(133, 178, 255, 1.0)"; + var hooveredKVFieldColor = "#AAD7FF"; + var hooveredElement = -1; + var hooveredKVField = -1; + var uiElements = new Array(); + var that = this; + var overlays = new Array(); + var overlayStyle = 'position:absolute; font-family:"Times New Roman",Georgia,Serif; visibility:inherit;'; + var overlayStyle2 = overlayStyle + 'border: 1px solid gray; background:white; pointer-events:none;'; + var resultStyle = 'position:inline; font-family:"Times New Roman",Georgia,Serif; visibility:inherit;'; + var dontShowResult = false; + + this.init = function () { + + data.init(4); + + var width = data.fieldBorder + data.fieldPerLine * data.fieldWidth + 50; + var height = data.fieldBorder + data.fieldLines * data.fieldHeight + 50; + + svg = document.createElementNS(svgns, "svg"); + if (!svg) + console.log("KarnaughMap error: can not create a svg element"); + //svg.setAttribute('style', 'border: 1px solid black'); + svg.setAttribute('width', width.toString()); + svg.setAttribute('height', height.toString()); + svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink"); + svg.setAttribute('id', parentDivId + "_KarnaughMap"); + document.body.appendChild(svg); + + var parent = document.getElementById(divId); + if (!parent) + console.log("KarnaughMap error: can not find an element with the given name: " + divId); + parent.appendChild(svg); + + svg.onmousedown = function (event) { + canvasMouseDown(event); + }; + svg.onmousemove = function (event) { + canvasMouseMove(event); + }; + svg.onmouseup = function (event) { + canvasMouseUp(event); + }; + svg.onmouseup = function (event) { + canvasMouseUp(event); + }; + + createOverlays(); + this.update(); + }; + + this.setNoOfVars = function (no) { + + var c = parseInt(no); + if (c < 1 && c > 10) + return; + + hooveredKVField = -1; + data.init(c); + createOverlays(); + + var width = data.fieldBorder + data.fieldPerLine * data.fieldWidth + 50; + var height = data.fieldBorder + data.fieldLines * data.fieldHeight + 50; + svg.setAttribute('width', width.toString()); + svg.setAttribute('height', height.toString()); + this.update(); + }; + + this.allowDontCares = function (type) { + if (type > 0) { + data.allowDontCare = true; + } else { + data.allowDontCare = false; + } + data.clear(); + this.update(); + }; + + this.setDontShowResult = function (type) { + if (type > 0) { + dontShowResult = true; + } else { + dontShowResult = false; + } + this.update(); + }; + + this.genRandom = function () { + data.random(); + this.update(); + }; + + this.clear = function () { + data.clear(); + this.update(); + }; + + function createOverlays() { + + var parent = document.getElementById(divId); + if (!parent) + console.log("KarnaughMap error: can not find an element with the given name: " + divId); + parent.setAttribute('style', 'position:relative;'); + + // remove old ones + for (var i in overlays) { + parent.removeChild(overlays[i]); + } + overlays.length = 0; + + for (var i = 0; i < data.noOfVars + 2; i++) { + var overlay = document.createElement('div'); + overlay.setAttribute('style', 'position:absolute; top:0px; left:0px; visibility:hidden;'); + overlay.innerHTML = "overlay" + i; + document.body.appendChild(overlay); + + parent.appendChild(overlay); + overlays.push(overlay); + } + } + + + + function drawKVField(fieldId) { + + var fieldPosX = data.getKVFieldPositionX(fieldId); + var fieldPosY = data.getKVFieldPositionY(fieldId); + var truthmapID = data.getKVFieldTruthmapID(fieldId); + var value = data.getKVFieldValue(fieldId); + var dn = new UIElement(fieldPosX, fieldPosY, data.fieldWidth, data.fieldHeight, 0, fieldId, 0, 0); + + var strokeColor = "#000000"; + var fillColor = "#FFFFFF"; + if (fieldId === hooveredKVField) { + fillColor = hooveredKVFieldColor; + } + + var dx = dn.x2 - dn.x; + var dy = dn.y2 - dn.y; + + var rect = document.createElementNS(svgns, 'rect'); + rect.setAttribute('x', dn.x); + rect.setAttribute('y', dn.y); + rect.setAttribute('height', dx); + rect.setAttribute('width', dy); + rect.setAttribute('fill', fillColor); + rect.setAttribute('stroke', strokeColor); + svg.appendChild(rect); + + var text = document.createElementNS(svgns, 'text'); + + var textColor = "#000000"; + if (value >= 2) { + value = "X"; + textColor = "#C8C8C8"; + } + text.setAttribute("fill", textColor); + //text.setAttribute("style", "font-family: sans-serif; font-weight: normal; font-style: normal"); + text.setAttribute("font-family", "sans-serif"); + text.setAttribute("text-anchor", "middle"); + text.setAttribute("font-size", "20"); + var posX = dn.x + Math.floor(dx / 2); + var posY = dn.y2 - Math.floor(dx / 3); + text.setAttribute("x", posX.toString()); + text.setAttribute("y", posY.toString()); + text.textContent = value.toString(); + svg.appendChild(text); + + uiElements.push(dn); + + if (true) { + var text2 = document.createElementNS(svgns, 'text'); + text2.setAttribute("fill", "#000"); + text2.setAttribute("text-anchor", "start"); + text2.setAttribute("font-family", "sans-serif"); + text2.setAttribute("font-size", "10"); + var posX = dn.x + Math.floor(dx / 32); + var posY = dn.y2 - Math.floor(dy / 16); + text2.setAttribute("x", posX.toString()); + text2.setAttribute("y", posY.toString()); + text2.textContent = truthmapID.toString(); + svg.appendChild(text2); + } + } + + function drawRoundRect(colorStr, x, y, width, height, radius) { + var x1 = x + width; + var y1 = y + height; + + var path = document.createElementNS(svgns, 'path'); + path.setAttribute("stroke-width", "3"); + path.setAttribute("stroke", colorStr); + path.setAttribute("fill", "none"); + + var d = ""; + d += "M " + (x + radius) + "," + y; + d += " L " + (x1 - radius) + "," + y; + d += " Q " + x1 + "," + y + " " + x1 + "," + (y + radius); + d += " L " + x1 + "," + (y1 - radius); + d += " Q " + x1 + "," + y1 + " " + (x1 - radius) + "," + y1; + d += " L " + (x + radius) + "," + y1; + d += " Q " + x + "," + y1 + " " + x + "," + (y1 - radius); + d += " L " + x + "," + (y + radius); + d += " Q " + x + "," + y + " " + (x + radius) + "," + y; + d += " Z"; + path.setAttribute("d", d); + svg.appendChild(path); + } + + function drawRoundRectOpenRightLeft(colorStr, x, y, w, height, radius, offset) { + + var width = w / 2 + Math.floor(data.fieldWidth * 0.6); + + var x1 = x + width; + var y1 = y + height; + + var path = document.createElementNS(svgns, 'path'); + path.setAttribute("stroke-width", "3"); + path.setAttribute("stroke", colorStr); + path.setAttribute("fill", "none"); + + var d = ""; + d += "M " + (x1 - radius) + "," + y1; + d += " L " + (x + radius) + "," + y1; + d += " Q " + x + "," + y1 + " " + x + "," + (y1 - radius); + d += " L " + x + "," + (y + radius); + d += " Q " + x + "," + y + " " + (x + radius) + "," + y; + d += " L " + (x1 - radius) + "," + y; + path.setAttribute("d", d); + svg.appendChild(path); + + x1 = x + w + offset; + x = x + offset + w / 2 - Math.floor(data.fieldWidth * 0.6); + + var path2 = document.createElementNS(svgns, 'path'); + path2.setAttribute("stroke-width", "3"); + path2.setAttribute("stroke", colorStr); + path2.setAttribute("fill", "none"); + + var d = ""; + d += "M " + (x + radius) + "," + y; + d += " L " + (x1 - radius) + "," + y; + d += " Q " + x1 + "," + y + " " + x1 + "," + (y + radius); + d += " L " + x1 + "," + (y1 - radius); + d += " Q " + x1 + "," + y1 + " " + (x1 - radius) + "," + y1; + d += " L " + (x + radius) + "," + y1; + path2.setAttribute("d", d); + svg.appendChild(path2); + + } + + function drawRoundRectOpenTopDown(colorStr, x, y, width, h, radius, offset) { + + var height = h / 2 + Math.floor(data.fieldHeight * 0.6); + var x1 = x + width; + var y1 = y + height; + + + var path = document.createElementNS(svgns, 'path'); + path.setAttribute("stroke-width", "3"); + path.setAttribute("stroke", colorStr); + path.setAttribute("fill", "none"); + + var d = ""; + d += "M " + x1 + "," + (y1 - radius); + d += " L " + x1 + "," + (y + radius); + d += " Q " + x1 + "," + y + " " + (x1 - radius) + "," + y; + d += " L " + (x + radius) + "," + y; + d += " Q " + x + "," + y + " " + x + "," + (y + radius); + d += " L " + x + "," + (y1 - radius); + path.setAttribute("d", d); + svg.appendChild(path); + + y1 = y + h + offset; + y = y + offset + h / 2 - Math.floor(data.fieldHeight * 0.6); + + var path2 = document.createElementNS(svgns, 'path'); + path2.setAttribute("stroke-width", "3"); + path2.setAttribute("stroke", colorStr); + path2.setAttribute("fill", "none"); + + var d = ""; + d += "M " + x + "," + (y + radius); + d += " L " + x + "," + (y1 - radius); + d += " Q " + x + "," + y1 + " " + (x + radius) + "," + y1; + d += " L " + (x1 - radius) + "," + y1; + d += " Q " + x1 + "," + y1 + " " + x1 + "," + (y1 - radius); + d += " L " + x1 + "," + (y + radius); + path2.setAttribute("d", d); + svg.appendChild(path2); + + } + + + function drawRoundRectAllOpen(colorStr, xx, yy, w, h, radius, offsetX, offsetY) { + var height = h / 2 + Math.floor(data.fieldHeight * 0.6); + var width = w / 2 + Math.floor(data.fieldWidth * 0.6); + + var x = xx; + var y = yy; + var x1 = xx + width; + var y1 = yy + height; + + var path = document.createElementNS(svgns, 'path'); + path.setAttribute("stroke-width", "3"); + path.setAttribute("stroke", colorStr); + path.setAttribute("fill", "none"); + + var d = ""; + d += "M " + (x1 - radius) + "," + y; + d += " L " + (x + radius) + "," + y; + d += " Q " + x + "," + y + " " + x + "," + (y + radius); + d += " L " + x + "," + (y1 - radius); + path.setAttribute("d", d); + svg.appendChild(path); + + x1 = xx + w + offsetX; + x = xx + offsetX + w / 2 - Math.floor(data.fieldWidth * 0.6); + + var path2 = document.createElementNS(svgns, 'path'); + path2.setAttribute("stroke-width", "3"); + path2.setAttribute("stroke", colorStr); + path2.setAttribute("fill", "none"); + + var d = ""; + d += "M " + (x + radius) + "," + y; + d += " L " + (x1 - radius) + "," + y; + d += " Q " + x1 + "," + y + " " + x1 + "," + (y + radius); + d += " L " + x1 + "," + (y1 - radius); + path2.setAttribute("d", d); + svg.appendChild(path2); + + y1 = yy + h + offsetY; + y = yy + offsetY + h / 2 - Math.floor(data.fieldHeight * 0.6); + x = xx; + x1 = xx + width; + + var path3 = document.createElementNS(svgns, 'path'); + path3.setAttribute("stroke-width", "3"); + path3.setAttribute("stroke", colorStr); + path3.setAttribute("fill", "none"); + + var d = ""; + d += "M " + x + "," + (y + radius); + d += " L " + x + "," + (y1 - radius); + d += " Q " + x + "," + y1 + " " + (x + radius) + "," + y1; + d += " L " + (x1 - radius) + "," + y1; + path3.setAttribute("d", d); + svg.appendChild(path3); + + x1 = xx + w + offsetX; + x = xx + offsetX + w / 2 - Math.floor(data.fieldWidth * 0.6); + + var path4 = document.createElementNS(svgns, 'path'); + path4.setAttribute("stroke-width", "3"); + path4.setAttribute("stroke", colorStr); + path4.setAttribute("fill", "none"); + + var d = ""; + d += "M " + x1 + "," + (y + radius); + d += " L " + x1 + "," + (y1 - radius); + d += " Q " + x1 + "," + y1 + " " + (x1 - radius) + "," + y1; + d += " L " + (x + radius) + "," + y1; + path4.setAttribute("d", d); + svg.appendChild(path4); + } + + function drawKVBlock(blockId) { + var fieldId = data.blocks[blockId].fieldID; + + var x0 = data.getKVFieldPositionX(fieldId); + var y0 = data.getKVFieldPositionY(fieldId); + var dx = data.blocks[blockId].dimx * data.fieldWidth; + var dy = data.blocks[blockId].dimy * data.fieldHeight; + var colorStr = "rgb(" + data.blocks[blockId].color[0].toString() + "," + data.blocks[blockId].color[1].toString() + "," + data.blocks[blockId].color[2].toString() + ")"; + + var offsetX = (data.fieldWidth * data.fieldPerLine); + var offsetY = (data.fieldHeight * data.fieldLines); + var overX = (x0 + dx > offsetX + data.fieldBorder); + var overY = (y0 + dy > offsetY + data.fieldBorder); + if (overX && overY) { + drawRoundRectAllOpen(colorStr, x0 + 2, y0 + 2, dx - 4, dy - 4, 17, -offsetX, -offsetY); + } else { + if (overX) { + drawRoundRectOpenRightLeft(colorStr, x0 + 2, y0 + 2, dx - 4, dy - 4, 17, -offsetX); + } else { + if (overY) { + drawRoundRectOpenTopDown(colorStr, x0 + 2, y0 + 2, dx - 4, dy - 4, 17, -offsetY); + } else { + drawRoundRect(colorStr, x0 + 2, y0 + 2, dx - 4, dy - 4, 17); + } + } + } + } + + function drawKVFields() { + var count = data.getKVFieldsCount(); + for (var i = 0; i < count; i++) { + drawKVField(i); + } + } + + function drawKVBlocks() { + var count = data.blocks.length; + for (var i = 0; i < count; i++) { + drawKVBlock(i); + } + } + + + this.update = function () { + + uiElements.length = 0; + + // clear svg element + while (svg.lastChild) { + svg.removeChild(svg.lastChild); + } + + // draws all fields + drawKVFields(); + + // draws all blocks + if(!dontShowResult) drawKVBlocks(); + + // draw labels + if (overlays.length !== data.noOfVars + 2) + console.log("KarnaughMap error: overlay not available"); + + var labelNum = 1; + var labelPos = 10; + var k = 0; + while (k < data.noOfVars) { + + overlays[k].innerHTML = "x" + k + "" + + for (var x = 0; x < data.fieldPerLine; x++) { + var bits = data.getKVFieldTruthmapID(x); + + if ((bits & labelNum) === labelNum) { + var x0 = data.fieldWidth * x + data.fieldBorder; + var x1 = data.fieldWidth * (x + 1) + data.fieldBorder; + + var path = document.createElementNS(svgns, 'path'); + path.setAttribute("stroke-width", "2"); + path.setAttribute("stroke", "#000000"); + path.setAttribute("fill", "none"); + + var d = ""; + d += "M " + x0 + "," + (labelPos - 2); // start marker + d += " L " + x0 + "," + (labelPos + 2); + d += " M " + x0 + "," + labelPos; + d += " L " + x1 + "," + labelPos; + d += " M " + x1 + "," + (labelPos - 2); // end marker + d += " L " + x1 + "," + (labelPos + 2); + path.setAttribute("d", d); + svg.appendChild(path); + + var style = overlayStyle + 'top:' + (labelPos - 10) + 'px; left:' + (x1 + 5) + 'px;'; + overlays[k].setAttribute('style', style); + } + } + k++; + if (k < data.noOfVars) { + + overlays[k].innerHTML = "x" + k + ""; + + labelNum = labelNum << 1; // move bit to left + + for (var y = 0; y < data.fieldLines; y++) { + var bits = data.getKVFieldTruthmapID(y * data.fieldPerLine); + if ((bits & labelNum) === labelNum) { + var x0 = data.fieldHeight * y + data.fieldBorder; + var x1 = data.fieldHeight * (y + 1) + data.fieldBorder; + + var path = document.createElementNS(svgns, 'path'); + path.setAttribute("stroke-width", "2"); + path.setAttribute("stroke", "#000000"); + path.setAttribute("fill", "none"); + + var d = ""; + d += "M " + (labelPos - 2) + "," + x0; // start marker + d += " L " + (labelPos + 2) + "," + x0; + d += " M " + labelPos + "," + x0; + d += " L " + labelPos + "," + x1; + d += " M " + (labelPos - 2) + "," + x1; // end marker + d += " L " + (labelPos + 2) + "," + x1; + path.setAttribute("d", d); + svg.appendChild(path); + + var style = overlayStyle + 'top:' + (x1) + 'px; left:' + (labelPos - 5) + 'px;'; + overlays[k].setAttribute('style', style); + } + } + labelNum = labelNum << 1; // move bit to left + labelPos += 20; + k++; + } + } + + // draw binary value + if (hooveredKVField >= 0 && hooveredKVField < data.getKVFieldsCount()) { + var truthmapID = data.getKVFieldTruthmapID(hooveredKVField); + var binString = truthmapID.toString(2); + while (binString.length < data.noOfVars) + binString = "0" + binString; + + var valueString = ""; + for (var z = 0; z < binString.length; z++) { + valueString += binString[z]; + if (z < binString.length - 1) + valueString += ","; + } + + var value = data.getKVFieldValue(hooveredKVField); + if (value >= 2) + value = "X"; + valueString = " f(" + valueString + ") = " + value; + //valueString += " (ID: " + hooveredKVField + ")"; + var textX = Math.floor(hooveredKVField % data.fieldPerLine) * data.fieldWidth + Math.floor(data.fieldWidth * 0.8) + data.fieldBorder; + var textY = Math.floor(hooveredKVField / data.fieldPerLine) * data.fieldHeight + Math.floor(data.fieldHeight * 0.1) + data.fieldBorder; + var style = overlayStyle2 + 'top:' + textY + 'px; left:' + textX + 'px;'; + overlays[data.noOfVars].setAttribute('style', style); + overlays[data.noOfVars].innerHTML = valueString; + } else { + overlays[data.noOfVars].innerHTML = ""; + var style = 'visibility:hidden;'; + overlays[data.noOfVars].setAttribute('style', style); + } + + // draw minterm + var termX = data.fieldBorder; + var termY = data.fieldHeight * data.fieldLines + data.fieldBorder; + var termStyle = resultStyle + 'max-width:' + data.fieldPerLine * data.fieldWidth + 'px;'; + overlays[data.noOfVars + 1].setAttribute('style', termStyle); + if(!dontShowResult) { + overlays[data.noOfVars + 1].innerHTML = "Y = " + qmc.data.coloredMinimalTerm + "

"; + }else{ + overlays[data.noOfVars + 1].innerHTML = "Y = " + "hidden"+ "

"; + } + }; + + function mouseOverElement(pos) { + var selectedElement = -1; + for (var n in uiElements) { + if (uiElements[n].type !== 2) { + // not of type "connection" + if (uiElements[n].x - 1 < pos.x && + uiElements[n].x2 + 1 > pos.x && + uiElements[n].y - 1 < pos.y && + uiElements[n].y2 + 1 > pos.y) + { + selectedElement = n; + } + } + } + return selectedElement; + } + + function canvasMouseDown(event) { + var pos = getMouse(event); + + // handle selection + if (!event.altKey && event.which === 1) { + var selectedElement = mouseOverElement(pos); + if (selectedElement !== -1) { + // handle field selection + if (uiElements[selectedElement].type === 0) { + var newSelectedKVField = uiElements[selectedElement].ref; + data.activated(newSelectedKVField); + } + } + that.update(); + } + event.preventDefault(); + } + + function canvasMouseUp(event) { + } + + function canvasMouseMove(event) { + var pos = getMouse(event); + + hooveredKVField = -1; + var oldHooveredElement = hooveredElement; + hooveredElement = mouseOverElement(pos); + console.log(hooveredElement); + if (hooveredElement !== -1) { + hooveredKVField = uiElements[hooveredElement].ref; + } + if (oldHooveredElement !== hooveredElement) + that.update(); + oldPos = pos; + event.preventDefault(); + } + + function getMouse(e) { + var element = document.getElementById(divId); + var offsetX = 0, offsetY = 0, mx, my; + + // compute the total offset + if (element.offsetParent !== undefined) { + do { + offsetX += element.offsetLeft; + offsetY += element.offsetTop; + } while ((element = element.offsetParent)); + } + + mx = e.pageX - offsetX; + my = e.pageY - offsetY + document.getElementById("scrollcount").scrollTop; + console.log(mx + " " + my + " " + document.getElementById("scrollcount").scrollTop ); + return {x: mx, y: my}; + } +} \ No newline at end of file diff --git a/test/setup/test_helpers.dart b/test/setup/test_helpers.dart index ed19da4f..70bb52bb 100644 --- a/test/setup/test_helpers.dart +++ b/test/setup/test_helpers.dart @@ -7,9 +7,12 @@ import 'package:mobile_app/services/API/contributors_api.dart'; import 'package:mobile_app/services/API/grades_api.dart'; import 'package:mobile_app/services/API/group_members_api.dart'; import 'package:mobile_app/services/API/groups_api.dart'; +import 'package:mobile_app/services/API/ib_api.dart'; import 'package:mobile_app/services/API/projects_api.dart'; import 'package:mobile_app/services/API/users_api.dart'; +import 'package:mobile_app/services/database_service.dart'; import 'package:mobile_app/services/dialog_service.dart'; +import 'package:mobile_app/services/ib_engine_service.dart'; import 'package:mobile_app/services/local_storage_service.dart'; import 'package:mobile_app/viewmodels/groups/add_assignment_viewmodel.dart'; import 'package:mobile_app/viewmodels/groups/assignment_details_viewmodel.dart'; @@ -18,6 +21,8 @@ import 'package:mobile_app/viewmodels/groups/group_details_viewmodel.dart'; import 'package:mobile_app/viewmodels/groups/my_groups_viewmodel.dart'; import 'package:mobile_app/viewmodels/groups/new_group_viewmodel.dart'; import 'package:mobile_app/viewmodels/groups/update_assignment_viewmodel.dart'; +import 'package:mobile_app/viewmodels/ib/ib_landing_viewmodel.dart'; +import 'package:mobile_app/viewmodels/ib/ib_page_viewmodel.dart'; import 'package:mobile_app/viewmodels/profile/edit_profile_viewmodel.dart'; import 'package:mobile_app/viewmodels/profile/profile_viewmodel.dart'; import 'package:mobile_app/viewmodels/profile/user_favourites_viewmodel.dart'; @@ -32,6 +37,8 @@ class NavigatorObserverMock extends Mock implements NavigatorObserver {} class LocalStorageServiceMock extends Mock implements LocalStorageService {} +class DatabaseServiceMock extends Mock implements DatabaseService {} + class ContributorsApiMock extends Mock implements ContributorsApi {} class GroupsApiMock extends Mock implements GroupsApi {} @@ -48,6 +55,10 @@ class ProjectsApiMock extends Mock implements ProjectsApi {} class CollaboratorsApiMock extends Mock implements CollaboratorsApi {} +class IbApiMock extends Mock implements IbApi {} + +class IbEngineServiceMock extends Mock implements IbEngineService {} + class MockDialogService extends Mock implements DialogService {} class MockLocalStorageService extends Mock implements LocalStorageService {} @@ -84,6 +95,10 @@ class MockUpdateAssignmentViewModel extends Mock class MockAssignmentDetailsViewModel extends Mock implements AssignmentDetailsViewModel {} +class MockIbLandingViewModel extends Mock implements IbLandingViewModel {} + +class MockIbPageViewModel extends Mock implements IbPageViewModel {} + LocalStorageService getAndRegisterLocalStorageServiceMock() { _removeRegistrationIfExists(); var _localStorageService = LocalStorageServiceMock(); @@ -92,6 +107,14 @@ LocalStorageService getAndRegisterLocalStorageServiceMock() { return _localStorageService; } +DatabaseService getAndRegisterDatabaseServiceMock() { + _removeRegistrationIfExists(); + var _databaseServiceMock = DatabaseServiceMock(); + + locator.registerSingleton(_databaseServiceMock); + return _databaseServiceMock; +} + ContributorsApi getAndRegisterContributorsApiMock() { _removeRegistrationIfExists(); var _contributorsApi = ContributorsApiMock(); @@ -156,8 +179,25 @@ CollaboratorsApi getAndRegisterCollaboratorsApiMock() { return _collaboratorsApi; } +IbApi getAndRegisterIbApiMock() { + _removeRegistrationIfExists(); + var _ibApi = IbApiMock(); + + locator.registerSingleton(_ibApi); + return _ibApi; +} + +IbEngineService getAndRegisterIbEngineServiceMock() { + _removeRegistrationIfExists(); + var _ibEngineService = IbEngineServiceMock(); + + locator.registerSingleton(_ibEngineService); + return _ibEngineService; +} + void registerServices() { getAndRegisterLocalStorageServiceMock(); + getAndRegisterDatabaseServiceMock(); getAndRegisterContributorsApiMock(); getAndRegisterGroupsApiMock(); getAndRegisterGroupMembersApiMock(); @@ -166,10 +206,13 @@ void registerServices() { getAndRegisterUsersApiMock(); getAndRegisterProjectsApiMock(); getAndRegisterCollaboratorsApiMock(); + getAndRegisterIbApiMock(); + getAndRegisterIbEngineServiceMock(); } void unregisterServices() { locator.unregister(); + locator.unregister(); locator.unregister(); locator.unregister(); locator.unregister(); @@ -178,6 +221,8 @@ void unregisterServices() { locator.unregister(); locator.unregister(); locator.unregister(); + locator.unregister(); + locator.unregister(); } void _removeRegistrationIfExists() { diff --git a/test/ui_tests/ib/ib_landing_view_test.dart b/test/ui_tests/ib/ib_landing_view_test.dart new file mode 100644 index 00000000..1f5690e3 --- /dev/null +++ b/test/ui_tests/ib/ib_landing_view_test.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get/get.dart'; +import 'package:mobile_app/locator.dart'; +import 'package:mobile_app/models/ib/ib_chapter.dart'; +import 'package:mobile_app/models/ib/ib_page_data.dart'; +import 'package:mobile_app/ui/views/ib/ib_landing_view.dart'; +import 'package:mobile_app/utils/router.dart'; +import 'package:mobile_app/viewmodels/ib/ib_landing_viewmodel.dart'; +import 'package:mobile_app/viewmodels/ib/ib_page_viewmodel.dart'; +import 'package:mockito/mockito.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../setup/test_helpers.dart'; + +void main() { + group('IbLandingViewTest -', () { + NavigatorObserver mockObserver; + + setUpAll(() async { + SharedPreferences.setMockInitialValues({}); + await setupLocator(); + locator.allowReassignment = true; + }); + + setUp(() => mockObserver = NavigatorObserverMock()); + + Future _pumpHomeView(WidgetTester tester) async { + // Mock ViewModel + var model = MockIbLandingViewModel(); + locator.registerSingleton(model); + + var pageViewModel = MockIbPageViewModel(); + locator.registerSingleton(pageViewModel); + + // Mock Page View Model + when(pageViewModel.fetchPageData()).thenReturn(null); + when(pageViewModel.isSuccess(pageViewModel.IB_FETCH_PAGE_DATA)) + .thenAnswer((_) => true); + when(pageViewModel.pageData).thenReturn( + IbPageData(id: 'test', pageUrl: 'test', title: 'test', content: [])); + + // Mock Page Drawer List + when(model.fetchChapters()).thenReturn(null); + when(model.isSuccess(model.IB_FETCH_CHAPTERS)).thenAnswer((_) => true); + when(model.chapters).thenAnswer((_) => [ + IbChapter( + id: 'test', + value: 'Test Chapter', + navOrder: '1', + items: [ + IbChapter( + id: 'test-2', + value: 'Test Chapter2', + navOrder: '2', + ), + ], + ) + ]); + + await tester.pumpWidget( + GetMaterialApp( + onGenerateRoute: CVRouter.generateRoute, + navigatorObservers: [mockObserver], + home: IbLandingView(), + ), + ); + + /// The tester.pumpWidget() call above just built our app widget + /// and triggered the pushObserver method on the mockObserver once. + verify(mockObserver.didPush(any, any)); + } + + testWidgets('finds IbPageView Widgets', (WidgetTester tester) async { + await tester.runAsync(() async { + await _pumpHomeView(tester); + await tester.pumpAndSettle(); + + // Finds AppBar + expect(find.byType(AppBar), findsOneWidget); + + // Finds AppBar Text + expect(find.text('CircuitVerse'), findsOneWidget); + + // Finds Table of Contents Icon + expect(find.byType(IconButton), findsNWidgets(1)); + + // Finds Floating Action Buttons + expect(find.byType(FloatingActionButton), findsNWidgets(1)); + }); + }); + + testWidgets('finds IbPageView Drawer Widgets', (WidgetTester tester) async { + await tester.runAsync(() async { + await _pumpHomeView(tester); + await tester.pumpAndSettle(); + + // Finds Scaffold + final state = tester.firstState(find.byType(Scaffold)) as ScaffoldState; + state.openDrawer(); + await tester.pump(); + + // Finds Drawer Widgets + expect(find.text('Return to Home'), findsOneWidget); + expect(find.text('Interactive Book Home'), findsOneWidget); + expect(find.byType(ExpansionTile), findsWidgets); + + // Finds Chapter + expect(find.text('Test Chapter'), findsOneWidget); + }); + }); + }); +} diff --git a/test/ui_tests/ib/ib_page_view_test.dart b/test/ui_tests/ib/ib_page_view_test.dart new file mode 100644 index 00000000..56ad5b27 --- /dev/null +++ b/test/ui_tests/ib/ib_page_view_test.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get/get.dart'; +import 'package:mobile_app/locator.dart'; +import 'package:mobile_app/models/ib/ib_chapter.dart'; +import 'package:mobile_app/models/ib/ib_content.dart'; +import 'package:mobile_app/models/ib/ib_page_data.dart'; +import 'package:mobile_app/ui/views/ib/ib_page_view.dart'; +import 'package:mobile_app/utils/router.dart'; +import 'package:mobile_app/viewmodels/ib/ib_page_viewmodel.dart'; +import 'package:mockito/mockito.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../setup/test_data/mock_ib_raw_page_data.dart'; +import '../../setup/test_helpers.dart'; + +void main() { + group('IbPageViewTest -', () { + NavigatorObserver mockObserver; + + setUpAll(() async { + SharedPreferences.setMockInitialValues({}); + await setupLocator(); + locator.allowReassignment = true; + }); + + setUp(() => mockObserver = NavigatorObserverMock()); + + Future _pumpIbPageView(WidgetTester tester) async { + // Mock ViewModel + var model = MockIbPageViewModel(); + locator.registerSingleton(model); + + // Mock Page Data + when(model.fetchPageData()).thenReturn(null); + when(model.isSuccess(model.IB_FETCH_PAGE_DATA)).thenAnswer((_) => true); + when(model.pageData).thenAnswer( + (_) => IbPageData( + id: mockIbRawPageData1['path'], + pageUrl: mockIbRawPageData1['http_url'], + title: mockIbRawPageData1['title'], + content: [IbMd(content: mockIbRawPageData1['raw_content'])], + tableOfContents: [], + ), + ); + + // Mock Page Data + var _chapter = IbChapter( + id: mockIbRawPageData1['path'], + value: mockIbRawPageData1['title'], + navOrder: '1', + ); + + _chapter.prevPage = _chapter; + _chapter.nextPage = _chapter; + + await tester.pumpWidget( + GetMaterialApp( + onGenerateRoute: CVRouter.generateRoute, + navigatorObservers: [mockObserver], + home: IbPageView( + key: UniqueKey(), + chapter: _chapter, + tocCallback: (val) {}, + setPage: (e) {}, + ), + ), + ); + + /// The tester.pumpWidget() call above just built our app widget + /// and triggered the pushObserver method on the mockObserver once. + verify(mockObserver.didPush(any, any)); + } + + testWidgets('finds PageView Widgets', (WidgetTester tester) async { + await _pumpIbPageView(tester); + await tester.pumpAndSettle(); + + final gesture = await tester.startGesture(Offset.zero); + await gesture.moveBy(const Offset(0, -1000)); + + await tester.pumpAndSettle(); + + // Finds Text Widgets + expect(find.byType(Text), findsWidgets); + + // Finds FABs + expect(find.byType(FloatingActionButton), findsNWidgets(2)); + + // Finds IbContent Widgets + expect(find.text('Interactive-Book'), findsOneWidget); + expect(find.text('Learn Digital Logic Design easily.'), findsOneWidget); + expect(find.text('Audience'), findsOneWidget); + expect(find.byType(Divider), findsNWidgets(1)); + }); + }); +} diff --git a/test/viewmodel_tests/ib/ib_landing_viewmodel_test.dart b/test/viewmodel_tests/ib/ib_landing_viewmodel_test.dart new file mode 100644 index 00000000..fa85734b --- /dev/null +++ b/test/viewmodel_tests/ib/ib_landing_viewmodel_test.dart @@ -0,0 +1,53 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mobile_app/enums/view_state.dart'; +import 'package:mobile_app/models/failure_model.dart'; +import 'package:mobile_app/models/ib/ib_chapter.dart'; +import 'package:mobile_app/viewmodels/ib/ib_landing_viewmodel.dart'; +import 'package:mockito/mockito.dart'; + +import '../../setup/test_helpers.dart'; + +void main() { + group('IbLandingViewModel Test -', () { + setUp(() => registerServices()); + tearDown(() => unregisterServices()); + + group('fetchChapters -', () { + test('When called & service returns success response', () async { + var _mockIbEngineApi = getAndRegisterIbEngineServiceMock(); + when(_mockIbEngineApi.getChapters()).thenAnswer( + (_) => Future.value([ + IbChapter( + id: 'index.md', + value: 'Home', + navOrder: '1', + ), + ]), + ); + + var _model = IbLandingViewModel(); + await _model.fetchChapters(); + + // verify call to IbEngineService was made + verify(_mockIbEngineApi.getChapters()); + expect(_model.stateFor(_model.IB_FETCH_CHAPTERS), ViewState.Success); + + // verify returned chapters + expect(_model.chapters.length, 1); + }); + + test('When called & service returns error', () async { + var _mockIbEngineApi = getAndRegisterIbEngineServiceMock(); + when(_mockIbEngineApi.getChapters()) + .thenThrow(Failure('Some Error Occurred!')); + + var _model = IbLandingViewModel(); + await _model.fetchChapters(); + + // verify call to IbEngineService was made + verify(_mockIbEngineApi.getChapters()); + expect(_model.stateFor(_model.IB_FETCH_CHAPTERS), ViewState.Error); + }); + }); + }); +} diff --git a/test/viewmodel_tests/ib/ib_page_viewmodel_test.dart b/test/viewmodel_tests/ib/ib_page_viewmodel_test.dart new file mode 100644 index 00000000..0cfd0625 --- /dev/null +++ b/test/viewmodel_tests/ib/ib_page_viewmodel_test.dart @@ -0,0 +1,90 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mobile_app/enums/view_state.dart'; +import 'package:mobile_app/models/failure_model.dart'; +import 'package:mobile_app/models/ib/ib_page_data.dart'; +import 'package:mobile_app/viewmodels/ib/ib_page_viewmodel.dart'; +import 'package:mockito/mockito.dart'; + +import '../../setup/test_helpers.dart'; + +void main() { + group('IbPageViewModel Test -', () { + setUp(() => registerServices()); + tearDown(() => unregisterServices()); + + group('fetchPageData -', () { + test('When called & service returns success response', () async { + var _mockIbEngineApi = getAndRegisterIbEngineServiceMock(); + when(_mockIbEngineApi.getPageData(id: '')).thenAnswer( + (_) => Future.value( + IbPageData( + id: '', + pageUrl: '', + title: 'Home', + content: [], + ), + ), + ); + + var _model = IbPageViewModel(); + await _model.fetchPageData(id: ''); + + // verify call to IbEngineService was made + verify(_mockIbEngineApi.getPageData(id: '')); + expect(_model.stateFor(_model.IB_FETCH_PAGE_DATA), ViewState.Success); + + // verify returned data + expect(_model.pageData.id, ''); + expect(_model.pageData.title, 'Home'); + expect(_model.pageData.content.length, 0); + }); + + test('When called & service returns error', () async { + var _mockIbEngineApi = getAndRegisterIbEngineServiceMock(); + when(_mockIbEngineApi.getPageData(id: '')) + .thenThrow(Failure('Some Error Occurred!')); + + var _model = IbPageViewModel(); + await _model.fetchPageData(id: ''); + + // verify call to IbEngineService was made + verify(_mockIbEngineApi.getPageData(id: '')); + expect(_model.stateFor(_model.IB_FETCH_PAGE_DATA), ViewState.Error); + }); + }); + + group('fetchHtmlInteraction -', () { + test('When called & service returns success response', () async { + var _mockIbEngineApi = getAndRegisterIbEngineServiceMock(); + when(_mockIbEngineApi.getHtmlInteraction('')).thenAnswer( + (_) => Future.value('test-data'), + ); + + var _model = IbPageViewModel(); + var _result = await _model.fetchHtmlInteraction(''); + + // verify call to IbEngineService was made + verify(_mockIbEngineApi.getHtmlInteraction('')); + expect(_model.stateFor(_model.IB_FETCH_INTERACTION_DATA), + ViewState.Success); + + // verify returned data + expect(_result, 'test-data'); + }); + + test('When called & service returns error', () async { + var _mockIbEngineApi = getAndRegisterIbEngineServiceMock(); + when(_mockIbEngineApi.getHtmlInteraction('')) + .thenThrow(Failure('Some Error Occurred!')); + + var _model = IbPageViewModel(); + await _model.fetchHtmlInteraction(''); + + // verify call to IbEngineService was made + verify(_mockIbEngineApi.getHtmlInteraction('')); + expect( + _model.stateFor(_model.IB_FETCH_INTERACTION_DATA), ViewState.Error); + }); + }); + }); +}