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"