diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b56b05b..c076471 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,16 +21,17 @@ We appreciate your interest in contributing to Plan Sync and helping to make it ## JSON Template Copy and modify this template for your contribution
- Template + Normal Class Template -``` +```json { "meta": { "section": "b17", //replace with your section here "type": "norm-class", // default value, need not to change - "revision": "Revision 1.0", // default value, need not to change + "revision": "Revision 1.0", // default value, need not to change "effective-date": "Aug 31, 2023", - "contributor": "Legendary Contributor", //replace with your name here + "contributor": "PlanSync Wizard", //replace with your name here + "isTimetableUpdating": false, // default value, need not to change // add day-wise classroom here "room": { @@ -88,6 +89,58 @@ Copy and modify this template for your contribution
+
+ Electives Template + +```json +{ + "meta": { + "type": "electives", + "revision": "Revision 1.0", + "effective-date": "Aug 31, 2023", + "name": "Electives Configuration for B14 - B23", + "contributor": "PlanSync Wizard", //replace with your name here + "isTimetableUpdating": false + }, + "data": { + "monday": { + "***": "***" + }, + "tuesday": { + "CIE6": "Room 304", + "CIE7": "Room 305", + "CIE8": "Room 301", + "CIE9": "Room 306", + "CIE10": "Room 307", + "SST3": "Room 401", + "SOE5": "Room 302", + "SOE6": "Room 402", + "SOE7": "Room 404", + "SOE8": "Room 303" + }, + "wednesday": { + "***": "***" + }, + "thursday": { + "CIE6": "Room 404", + "CIE7": "Room 302", + "CIE8": "Room 306", + "CIE9": "Room 303", + "CIE10": "Room 304", + "SST3": "Room 307", + "SOE5": "Room 401", + "SOE6": "Room 305", + "SOE7": "Room 402", + "SOE8": "Room 403" + }, + "friday": { + "***": "***" + } + } +} +``` +
+ ## Guidelines for submission - Please use the provided JSON template to input your class schedule details. diff --git a/client-app/lib/controllers/app_preferences_controller.dart b/client-app/lib/controllers/app_preferences_controller.dart new file mode 100644 index 0000000..bc6bd71 --- /dev/null +++ b/client-app/lib/controllers/app_preferences_controller.dart @@ -0,0 +1,35 @@ +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class AppPreferencesController extends GetxController { + late SharedPreferences perfs; + + @override + Future onInit() async { + perfs = await SharedPreferences.getInstance(); + super.onInit(); + } + + String? getPrimarySectionPreference() => perfs.getString('primary-section'); + + Future savePrimarySectionPreference(String data) async => + await perfs.setString('primary-section', data); + + String? getPrimarySemesterPreference() => perfs.getString('primary-semester'); + + Future savePrimarySemesterPreference(String data) async => + await perfs.setString('primary-semester', data); + + String? getPrimaryYearPreference() => perfs.getString('primary-year'); + + Future savePrimaryYearPreference(String data) async => + await perfs.setString('primary-year', data); + + // In app tutorial + /// Returns `true` if tutorial has already been completed. + bool? getTutorialStatus() => perfs.getBool('app-tutorial-status'); + + /// Save tutorial status as `true` if completed. + Future saveTutorialStatus(bool status) async => + await perfs.setBool('app-tutorial-status', status); +} diff --git a/client-app/lib/controllers/app_tour_controller.dart b/client-app/lib/controllers/app_tour_controller.dart new file mode 100644 index 0000000..ba63757 --- /dev/null +++ b/client-app/lib/controllers/app_tour_controller.dart @@ -0,0 +1,124 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:plan_sync/controllers/app_preferences_controller.dart'; +import 'package:plan_sync/util/logger.dart'; +import 'package:plan_sync/widgets/bottom-sheets/bottom_sheets_wrapper.dart'; +import 'package:plan_sync/widgets/tutorials/app_target_focus.dart'; +import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; + +class AppTourController extends GetxController { + late AppPreferencesController appPreferencesController; + + late GlobalKey _schedulePreferencesButtonKey; + GlobalKey get schedulePreferencesButtonKey => _schedulePreferencesButtonKey; + + late GlobalKey _sectionBarKey; + GlobalKey get sectionBarKey => _sectionBarKey; + + late GlobalKey _savePreferenceSwitchKey; + GlobalKey get savePreferenceSwitchKey => _savePreferenceSwitchKey; + + @override + void onInit() { + appPreferencesController = Get.find(); + super.onInit(); + _schedulePreferencesButtonKey = GlobalKey(); + _sectionBarKey = GlobalKey(); + _savePreferenceSwitchKey = GlobalKey(); + } + + Future startAppTour(BuildContext context) async { + final colorScheme = Theme.of(context).colorScheme; + + if (await tourAlreadyCompleted()) { + return; + } else if (!context.mounted) { + return; + } + + List targets = getTutorialTargets(context); + + TutorialCoachMark tutorial = TutorialCoachMark( + targets: targets, + colorShadow: colorScheme.onBackground, + textSkip: "SKIP", + textStyleSkip: TextStyle(color: colorScheme.background), + paddingFocus: 0, + focusAnimationDuration: const Duration(milliseconds: 500), + unFocusAnimationDuration: const Duration(milliseconds: 500), + pulseAnimationDuration: const Duration(milliseconds: 750), + showSkipInLastTarget: true, + imageFilter: ImageFilter.blur(sigmaX: 4, sigmaY: 4), + initialFocus: 0, + useSafeArea: true, + onFinish: onTourComplete, + onSkip: () { + onTourComplete(); + return true; + }, + onClickTargetWithTapPosition: onClickHandler, + ); + + if (!context.mounted) { + return; + } + + tutorial.show(context: context, rootOverlay: true); + } + + Future onClickHandler( + TargetFocus target, TapDownDetails tapDownDetails) async { + if (target.identify == schedulePreferencesButtonKey.hashCode) { + Logger.i('key match with schedule button'); + + await Future.delayed(const Duration(milliseconds: 250)); + BottomSheets.changeSectionPreference( + context: schedulePreferencesButtonKey.currentContext!, + ); + } + return; + } + + Future tourAlreadyCompleted() async { + return appPreferencesController.getTutorialStatus() ?? false; + } + + List getTutorialTargets(BuildContext context) { + List targets = []; + final colorScheme = Theme.of(context).colorScheme; + + targets.add( + AppTargetFocus.schedulePreferencesButton( + colorScheme: colorScheme, + buttonKey: schedulePreferencesButtonKey, + ), + ); + targets.add( + AppTargetFocus.savePreferenceSwitch( + colorScheme: colorScheme, + buttonKey: savePreferenceSwitchKey, + ), + ); + targets.add( + AppTargetFocus.sectionBarButton( + colorScheme: colorScheme, + buttonKey: sectionBarKey, + ), + ); + + return targets; + } + + Future onTourComplete() async { + final res = await appPreferencesController.saveTutorialStatus(true); + if (res != true) { + final err = { + 'origin': 'AppTourController.onTourComplete', + 'message': 'error saving to shared preferences' + }; + return Future.error(err); + } + return; + } +} diff --git a/client-app/lib/controllers/filter_controller.dart b/client-app/lib/controllers/filter_controller.dart index 311e3d9..8f4aeec 100644 --- a/client-app/lib/controllers/filter_controller.dart +++ b/client-app/lib/controllers/filter_controller.dart @@ -1,8 +1,8 @@ import 'package:get/get.dart'; +import 'package:plan_sync/controllers/app_preferences_controller.dart'; import 'package:plan_sync/controllers/git_service.dart'; import 'package:plan_sync/util/logger.dart'; import 'package:plan_sync/util/snackbar.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:collection/collection.dart'; class FilterController extends GetxController { @@ -72,13 +72,13 @@ class FilterController extends GetxController { } late GitService service; - late SharedPreferences preferences; + late AppPreferencesController preferences; @override onInit() async { - super.onInit(); - preferences = await SharedPreferences.getInstance(); service = Get.find(); + preferences = Get.find(); + super.onInit(); } Future getShortCode() async { @@ -97,9 +97,7 @@ class FilterController extends GetxController { } /// returns primary section from shared-preferences - String? get primarySection { - return preferences.getString('primary-section'); - } + String? get primarySection => preferences.getPrimarySectionPreference(); /// saves the section code into shared-preferences Future storePrimarySection() async { @@ -112,7 +110,18 @@ class FilterController extends GetxController { return; } - await preferences.setString('primary-section', activeSectionCode!); + final res = + await preferences.savePrimarySectionPreference(activeSectionCode!); + + if (res == false) { + Logger.i("Could not save preference"); + CustomSnackbar.error( + 'Error', + 'Primary Section wasn\'t saved. Try again', + ); + return; + } + Logger.i("set ${activeSectionCode!} as primary"); update(); } @@ -120,7 +129,7 @@ class FilterController extends GetxController { /// sets the section code while runtime Future setPrimarySection() async { activeSection = null; - final String? primarySection = preferences.getString('primary-section'); + final String? primarySection = preferences.getPrimarySectionPreference(); Logger.i("primary section: $primarySection"); if (primarySection != null && @@ -131,9 +140,7 @@ class FilterController extends GetxController { } /// returns primary semester from shared-preferences - String? get primarySemester { - return preferences.getString('primary-semester'); - } + String? get primarySemester => preferences.getPrimarySemesterPreference(); /// saves the semester code into shared-preferences Future storePrimarySemester() async { @@ -145,7 +152,18 @@ class FilterController extends GetxController { Logger.i("select a semester to be set as primary."); return; } - await preferences.setString('primary-semester', activeSemester!); + final res = + await preferences.savePrimarySemesterPreference(activeSemester!); + + if (res == false) { + Logger.i("Could not save preference"); + CustomSnackbar.error( + 'Error', + 'Primary Semester wasn\'t saved. Try again', + ); + return; + } + Logger.i("set ${activeSemester!} as primary semester"); update(); } @@ -153,7 +171,7 @@ class FilterController extends GetxController { /// sets the semester code while runtime Future setPrimarySemester() async { // activeSemester = null; - final String? primarySemester = preferences.getString('primary-semester'); + final String? primarySemester = preferences.getPrimarySemesterPreference(); Logger.i("primary semester: $primarySemester"); if (service.semesters?.contains(primarySemester) != false && @@ -163,9 +181,7 @@ class FilterController extends GetxController { } /// returns primary semester from shared-preferences - String? get primaryYear { - return preferences.getString('primary-year'); - } + String? get primaryYear => preferences.getPrimaryYearPreference(); /// saves the semester code into shared-preferences Future storePrimaryYear() async { @@ -177,10 +193,19 @@ class FilterController extends GetxController { Logger.i("select a year to be set as primary."); return; } - await preferences.setString( - 'primary-year', + final res = await preferences.savePrimaryYearPreference( service.selectedYear!.toString(), ); + + if (res == false) { + Logger.i("Could not save preference"); + CustomSnackbar.error( + 'Error', + 'Primary Year wasn\'t saved. Try again', + ); + return; + } + Logger.i("set ${service.selectedYear!} as primary year"); update(); } @@ -188,7 +213,7 @@ class FilterController extends GetxController { /// sets the semester code while runtime Future setPrimaryYear() async { // activeSemester = null; - final String? primaryYear = preferences.getString('primary-year'); + final String? primaryYear = preferences.getPrimaryYearPreference(); Logger.i("primary year: $primaryYear"); if (service.years?.contains(primaryYear) != false && primaryYear != null) { diff --git a/client-app/lib/controllers/git_service.dart b/client-app/lib/controllers/git_service.dart index d83de5a..444d8e5 100644 --- a/client-app/lib/controllers/git_service.dart +++ b/client-app/lib/controllers/git_service.dart @@ -1,14 +1,20 @@ import 'dart:convert'; +import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; import 'package:plan_sync/controllers/filter_controller.dart'; import 'package:plan_sync/util/logger.dart'; import 'package:plan_sync/util/snackbar.dart'; +import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; +import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart'; +import 'package:path_provider/path_provider.dart'; class GitService extends GetxController { late String branch; - final dio = Dio(BaseOptions(connectTimeout: const Duration(seconds: 15))); + + late final CacheOptions cacheOptions; + late final Dio dio; // normal schedule years List? years; @@ -77,12 +83,46 @@ class GitService extends GetxController { super.onReady(); filterController = Get.find(); setRepositoryBranch(); + await startCachingService(); await getYears(); await getSemesters(); await getElectiveSemesters(); await getElectiveYears(); } + /// Adds an interceptor to global dio instance, which is used to + /// cache responses from requested API's. + /// + /// Implemented to enable offline functionality, to fetch schedules + /// once it's cached in temp storage. + Future startCachingService() async { + final dir = await getApplicationCacheDirectory(); + + cacheOptions = CacheOptions( + store: HiveCacheStore( + dir.path, + hiveBoxName: 'plan_sync', + ), + policy: CachePolicy.refreshForceCache, + hitCacheOnErrorExcept: [401, 403], + maxStale: const Duration(days: 7), + priority: CachePriority.high, + keyBuilder: CacheOptions.defaultCacheKeyBuilder, + allowPostMethod: false, + ); + + dio = Dio( + BaseOptions( + connectTimeout: const Duration(seconds: 15), + headers: {'Cache-Control': 'no-cache'}, + contentType: "application/json", + ), + ); + dio.interceptors.add(DioCacheInterceptor(options: cacheOptions)); + + return; + } + /// From v1.0.0, app will use main branch for release client app, /// and pre-mvp for debug/development. void setRepositoryBranch() { @@ -250,16 +290,10 @@ class GitService extends GetxController { final url = "https://gitlab.com/delwinn/plan-sync/-/raw/$branch/res/$selectedYear/$semester/$section.json"; try { - final response = await dio.get(url, - options: Options( - headers: { - "accept": "application/vnd.github+json", - 'X-GitHub-Api-Version': '2022-11-28' - }, - contentType: "application/json", - )); - if (response.statusCode != 200) { - throw DioException(requestOptions: response.requestOptions); + final response = await dio.get(url); + + if (response.statusCode! >= 400) { + return Future.error(response); } !isWorking.value ? null : isWorking.toggle(); @@ -273,6 +307,10 @@ class GitService extends GetxController { 'We couldn\'t fetch requested timetable. Please try again later.', }; + if (e.type == DioExceptionType.connectionError) { + errorDetails?['message'] = 'Please check your network connection.'; + } + !isWorking.value ? null : isWorking.toggle(); Logger.i(e.toString()); return Future.error(Exception(errorDetails)); @@ -442,16 +480,10 @@ class GitService extends GetxController { final url = "https://gitlab.com/delwinn/plan-sync/-/raw/$branch/res/$selectedElectiveYear/${filterController.activeElectiveSemester}/electives-scheme-${filterController.activeElectiveSchemeCode}.json"; try { - final response = await dio.get(url, - options: Options( - headers: { - "accept": "application/vnd.github+json", - 'X-GitHub-Api-Version': '2022-11-28' - }, - contentType: "application/json", - )); - if (response.statusCode != 200) { - throw DioException(requestOptions: response.requestOptions); + final response = await dio.get(url); + + if (response.statusCode! >= 400) { + return Future.error(response); } if (response.data == "") { return null; diff --git a/client-app/lib/controllers/version_controller.dart b/client-app/lib/controllers/version_controller.dart index e670238..9e0a95c 100644 --- a/client-app/lib/controllers/version_controller.dart +++ b/client-app/lib/controllers/version_controller.dart @@ -53,20 +53,39 @@ class VersionController extends GetxController { return result.updateAvailability == UpdateAvailability.updateAvailable; } catch (e) { isError = true; - throw Exception("DioException, $e"); + throw Exception("VersionController.checkForUpdate Exception, $e"); } } Future triggerPlayUpdate() async { final updateAvail = await InAppUpdate.checkForUpdate(); - if (updateAvail.flexibleUpdateAllowed) { + if (updateAvail.immediateUpdateAllowed && + immediateUpdateCondition(updateAvail)) { + Logger.i('starting immediate upadte'); + await InAppUpdate.performImmediateUpdate(); + Logger.i('flex update package installed'); + } else if (updateAvail.flexibleUpdateAllowed) { Logger.i('starting flex upadte'); - await InAppUpdate.startFlexibleUpdate(); + AppUpdateResult appUpdateResult = await InAppUpdate.startFlexibleUpdate(); Logger.i('flex update package downloaded'); - await InAppUpdate.completeFlexibleUpdate(); - Logger.i('flex update package installed'); + + if (appUpdateResult == AppUpdateResult.success) { + await InAppUpdate.completeFlexibleUpdate(); + Logger.i('flex update package installed'); + } } return; } + + // wanted to use updateAvail.updatePriority but it's API has been + // stale for over 4 years. using updateAvail.availableVersionCode + // and performing immediate update if difference in current buildNumber + // and incoming buildNumber is greater than 5 + bool immediateUpdateCondition(AppUpdateInfo info) { + int difference = + info.availableVersionCode! - int.parse(packageInfo.buildNumber); + + return difference > 5; + } } diff --git a/client-app/lib/main.dart b/client-app/lib/main.dart index d1c4391..0329ef5 100644 --- a/client-app/lib/main.dart +++ b/client-app/lib/main.dart @@ -5,6 +5,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:go_router/go_router.dart'; import 'package:plan_sync/controllers/analytics_controller.dart'; +import 'package:plan_sync/controllers/app_tour_controller.dart'; +import 'package:plan_sync/controllers/app_preferences_controller.dart'; import 'package:plan_sync/controllers/auth.dart'; import 'package:plan_sync/controllers/filter_controller.dart'; import 'package:plan_sync/controllers/git_service.dart'; @@ -57,10 +59,12 @@ class MainApp extends StatelessWidget { _injectDependencies() async { Get.put(Auth()); - Get.put(FilterController()); + Get.put(AppPreferencesController()); Get.put(GitService()); + Get.put(FilterController()); Get.put(VersionController()); Get.put(AnalyticsController()); + Get.put(AppTourController()); } // GoRouter configuration diff --git a/client-app/lib/views/home_screen.dart b/client-app/lib/views/home_screen.dart index d9ecdd2..4b9b2d1 100644 --- a/client-app/lib/views/home_screen.dart +++ b/client-app/lib/views/home_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:plan_sync/controllers/app_tour_controller.dart'; import 'package:plan_sync/controllers/filter_controller.dart'; import 'package:plan_sync/widgets/buttons/schedule_preferences_button.dart'; import '../widgets/time_table.dart'; @@ -14,6 +15,7 @@ class HomeScreen extends StatefulWidget { class _HomeScreenState extends State { FilterController filterController = Get.find(); + AppTourController appTourController = Get.find(); String? sectionSemesterShortCode; @override @@ -24,6 +26,11 @@ class _HomeScreenState extends State { sectionSemesterShortCode = code; }), ); + WidgetsBinding.instance.addPostFrameCallback((_) { + Future.delayed(const Duration(milliseconds: 500), () { + appTourController.startAppTour(context); + }); + }); } @override @@ -48,9 +55,11 @@ class _HomeScreenState extends State { letterSpacing: 0.2, ), ), - actions: const [ - SchedulePreferenceButton(), - SizedBox(width: 16), + actions: [ + SchedulePreferenceButton( + key: appTourController.schedulePreferencesButtonKey, + ), + const SizedBox(width: 16), ], ), body: Padding( @@ -63,6 +72,7 @@ class _HomeScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ const VersionCheckWidget(), + const SizedBox(height: 16), Text( "Time Sheet", style: TextStyle( diff --git a/client-app/lib/widgets/bottom-sheets/schedule_preference.dart b/client-app/lib/widgets/bottom-sheets/schedule_preference.dart index 3f05a19..63f565e 100644 --- a/client-app/lib/widgets/bottom-sheets/schedule_preference.dart +++ b/client-app/lib/widgets/bottom-sheets/schedule_preference.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:go_router/go_router.dart'; +import 'package:plan_sync/controllers/app_tour_controller.dart'; import 'package:plan_sync/controllers/filter_controller.dart'; import 'package:plan_sync/util/snackbar.dart'; import 'package:plan_sync/widgets/dropdowns/sections_bar.dart'; @@ -45,6 +46,7 @@ class _SchedulePreferenceBottomSheetState Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final size = MediaQuery.of(context).size; + AppTourController appTourController = Get.find(); return SingleChildScrollView( physics: const BouncingScrollPhysics(), @@ -72,6 +74,7 @@ class _SchedulePreferenceBottomSheetState // preference switch ListTile( + key: appTourController.savePreferenceSwitchKey, enableFeedback: true, leading: Icon( Icons.downloading_rounded, @@ -166,6 +169,7 @@ class _SchedulePreferenceBottomSheetState // section selection ListTile( + key: appTourController.sectionBarKey, enableFeedback: true, leading: Icon( Icons.book_rounded, diff --git a/client-app/lib/widgets/tutorials/app_target_focus.dart b/client-app/lib/widgets/tutorials/app_target_focus.dart new file mode 100644 index 0000000..947e9d2 --- /dev/null +++ b/client-app/lib/widgets/tutorials/app_target_focus.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; + +class AppTargetFocus { + AppTargetFocus(); + + static schedulePreferencesButton({ + required ColorScheme colorScheme, + required GlobalKey buttonKey, + }) { + return TargetFocus( + identify: buttonKey.hashCode, + keyTarget: buttonKey, + contents: [ + TargetContent( + align: ContentAlign.left, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 64), + Text( + "Select your section here", + style: TextStyle( + fontWeight: FontWeight.bold, + color: colorScheme.background, + fontSize: 20.0, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text( + "View your schedule efficiently and save your preferences here", + style: TextStyle( + color: colorScheme.background, + ), + ), + ) + ], + ), + ), + ], + ); + } + + static sectionBarButton({ + required ColorScheme colorScheme, + required GlobalKey buttonKey, + }) { + return TargetFocus( + identify: buttonKey.hashCode, + keyTarget: buttonKey, + shape: ShapeLightFocus.RRect, + radius: 24, + contents: [ + TargetContent( + align: ContentAlign.top, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Select your section", + style: TextStyle( + fontWeight: FontWeight.bold, + color: colorScheme.background, + fontSize: 20.0, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text( + "We have all the schedules ready to go!", + style: TextStyle( + color: colorScheme.background, + ), + ), + ), + const SizedBox(height: 48), + ], + ), + ), + ], + ); + } + + static savePreferenceSwitch({ + required ColorScheme colorScheme, + required GlobalKey buttonKey, + }) { + return TargetFocus( + identify: buttonKey.hashCode, + keyTarget: buttonKey, + shape: ShapeLightFocus.RRect, + radius: 24, + contents: [ + TargetContent( + align: ContentAlign.top, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Save Everything", + style: TextStyle( + fontWeight: FontWeight.bold, + color: colorScheme.background, + fontSize: 20.0, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text( + "Turn this on to save your section details.\nWe'll open this up when you come back!", + style: TextStyle( + color: colorScheme.background, + ), + ), + ), + const SizedBox(height: 48), + ], + ), + ), + ], + ); + } +} diff --git a/client-app/lib/widgets/version_check.dart b/client-app/lib/widgets/version_check.dart index 2774a51..833a96e 100644 --- a/client-app/lib/widgets/version_check.dart +++ b/client-app/lib/widgets/version_check.dart @@ -41,7 +41,7 @@ class _VersionCheckWidgetState extends State { color: colorScheme.primary, ), padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16), - margin: const EdgeInsets.only(top: 16, bottom: 40), + margin: const EdgeInsets.only(top: 16, bottom: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ diff --git a/client-app/pubspec.yaml b/client-app/pubspec.yaml index 37b31e4..207357e 100644 --- a/client-app/pubspec.yaml +++ b/client-app/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+5 +version: 1.1.0+6 environment: sdk: ">=3.0.6 <4.0.0" @@ -57,6 +57,11 @@ dependencies: flutter_svg: ^2.0.9 rename: ^3.0.1 in_app_update: ^4.2.2 + dio_cache_interceptor: ^3.5.0 + dio_cache_interceptor_hive_store: ^3.2.2 + path_provider: ^2.1.1 + hive: ^2.2.3 + tutorial_coach_mark: ^1.2.11 icons_launcher: image_path: "assets/favicon.png"