Skip to content

Commit

Permalink
Add course cache and more (#5)
Browse files Browse the repository at this point in the history
Feature/add course cache
  • Loading branch information
thermitex authored Jun 11, 2024
2 parents b0b4c90 + adb5491 commit 548220f
Show file tree
Hide file tree
Showing 23 changed files with 259 additions and 89 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ All contributions are welcome from bug fixes to new features and extensions. I w

Please also let me know if you would like to be a maintainer of the repo.

Support Cuckoo's development and help Cuckoo stay on app stores:

<a href="https://www.buymeacoffee.com/jerryli" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>

## License

Cuckoo uses MIT License.
10 changes: 5 additions & 5 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ PODS:
- in_app_purchase_storekit (0.0.1):
- Flutter
- FlutterMacOS
- open_file (0.0.1):
- open_filex (0.0.2):
- Flutter
- package_info_plus (0.4.5):
- Flutter
Expand All @@ -42,7 +42,7 @@ DEPENDENCIES:
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
- open_file (from `.symlinks/plugins/open_file/ios`)
- open_filex (from `.symlinks/plugins/open_filex/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
Expand All @@ -69,8 +69,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/fluttertoast/ios"
in_app_purchase_storekit:
:path: ".symlinks/plugins/in_app_purchase_storekit/darwin"
open_file:
:path: ".symlinks/plugins/open_file/ios"
open_filex:
:path: ".symlinks/plugins/open_filex/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
Expand All @@ -92,7 +92,7 @@ SPEC CHECKSUMS:
flutter_timezone: ffb07bdad3c6276af8dada0f11978d8a1f8a20bb
fluttertoast: 9f2f8e81bb5ce18facb9748d7855bf5a756fe3db
in_app_purchase_storekit: 8c3b0b3eb1b0f04efbff401c3de6266d4258d433
open_file: 02eb5cb6b21264bd3a696876f5afbfb7ca4f4b7d
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
Expand Down
4 changes: 4 additions & 0 deletions jsons/moodleAutoLoginInfo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"key": "",
"lastRequested": 0
}
4 changes: 3 additions & 1 deletion jsons/moodleCourse.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,7 @@
"showcompletionconditions?": true,
"timemodified?": 1578359661,
"colorHex?": "",
"customFavorite?": false
"customFavorite?": false,
"cachedContents?": "$[]moodleCourseSection",
"cachedTime?": 1
}
11 changes: 11 additions & 0 deletions lib/src/common/extensions/build_context.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:io';

import 'package:cuckoo/src/common/services/moodle.dart';
import 'package:cuckoo/src/common/services/reminders.dart';
import 'package:cuckoo/src/common/services/settings.dart';
Expand All @@ -17,6 +19,15 @@ extension BuildContextExtensions on BuildContext {
}
}

// Push a new route based on the current platform.
Future<T?> platformDependentPush<T>({required WidgetBuilder builder}) {
return Navigator.of(this, rootNavigator: Platform.isAndroid)
.push(MaterialPageRoute(
fullscreenDialog: Platform.isAndroid,
builder: (context) => builder(context),
));
}

/// Shortcut for getting current theme.
ThemeData get theme => Theme.of(this);

Expand Down
2 changes: 1 addition & 1 deletion lib/src/common/services/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class Constants {
static const kUnsetCourseFavorite = 'Course Unmarked As Favorite';
static const kDownloadFileLoading = 'Downloading file...';
static const kEventsClearPrompt =
'Amazing! There is currently no upcoming events for you.';
'Amazing! There are currently no upcoming events for you.';
static const kCalendarNoEventsFound = 'No events found for the selected day.';
static const kLogOutConfirmation =
'Are you sure to sign out current Moodle account?';
Expand Down
84 changes: 61 additions & 23 deletions lib/src/common/services/moodle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ class Moodle {
/// Moodle domain.
String _domain = kHKUMoodleDomain;

/// Auto login key.
String? _autoLoginKey;
/// Auto login info.
MoodleAutoLoginInfo? _autoLoginInfo;

/// User ID of the Moodle user.
String get _userId => _siteInfo.userid.toString();
Expand Down Expand Up @@ -115,7 +115,6 @@ class Moodle {
moodle._fetchSiteInfo();
fetchEvents();
fetchCourses();
moodle._updateAutoLoginKey();
}
}

Expand All @@ -126,6 +125,7 @@ class Moodle {
moodle.loginStatusManager.status = false;
moodle._wstoken = null;
moodle._privatetoken = null;
moodle._autoLoginInfo = null;
moodle.courseManager._clearAllCourses();
moodle.eventManager
.._clearEventsExceptCustom()
Expand All @@ -136,6 +136,7 @@ class Moodle {
for (String key in [
MoodleStorageKeys.wstoken,
MoodleStorageKeys.privatetoken,
MoodleStorageKeys.autoLoginInfo,
MoodleStorageKeys.siteInfo,
MoodleStorageKeys.courses,
MoodleStorageKeys.events,
Expand Down Expand Up @@ -279,7 +280,7 @@ class Moodle {
moodle.courseManager.status = MoodleManagerStatus.error;
return false;
}
if (saveNow) moodle._save();
if (saveNow) moodle._saveCourses();
moodle.courseManager.status = MoodleManagerStatus.idle;
return true;
}
Expand Down Expand Up @@ -318,6 +319,11 @@ class Moodle {
} catch (_) {}
// Update last accessed property locally first
course.markAccess();
// Cache contents
course
..cachedContents = content
..cachedTime = DateTime.now().secondEpoch;
Moodle()._saveCourses();
return content;
}

Expand Down Expand Up @@ -367,6 +373,13 @@ class Moodle {
return null;
}

/// Clear all cached contents for all courses.
static void clearCourseCachedContents() {
final moodle = Moodle();
moodle.courseManager._clearCachedCourseContents();
moodle._saveCourses();
}

// ------------Event Interfaces------------

/// Fetch events of the logged in user.
Expand Down Expand Up @@ -419,7 +432,7 @@ class Moodle {
await moodle._fetchEventDetailsAndCompletionStatus();
moodle.eventManager._notifyManually();
moodle.eventManager._eventsLastUpdated = DateTime.now();
if (saveNow) moodle._save();
if (saveNow) moodle._saveEvents();
moodle.eventManager.status = MoodleManagerStatus.idle;
return true;
}
Expand Down Expand Up @@ -456,7 +469,7 @@ class Moodle {
}));
}
}
return Future.wait(requests).then((_) => moodle._save());
return Future.wait(requests).then((_) => moodle._saveEvents());
}

/// Get the course info associated with the event.
Expand All @@ -477,14 +490,14 @@ class Moodle {
static void addCustomEvent(MoodleEvent event) {
final moodle = Moodle();
moodle.eventManager._addCustomEvent(event);
moodle._save();
moodle._saveEvents();
}

/// Remove a custom event from the current event list.
static void removeCustomEvent(MoodleEvent event) {
final moodle = Moodle();
moodle.eventManager._removeCustomEvent(event);
moodle._save();
moodle._saveEvents();
}

// ------------Moodle Web Interfaces------------
Expand All @@ -497,20 +510,19 @@ class Moodle {
{bool internal = false}) async {
final moodle = Moodle();
if (!isUserLoggedIn || url == null) return false;
// First check if auto login key exists
if (moodle._autoLoginKey == null) {
CuckooFullScreenIndicator()
.startLoading(message: Constants.kMoodleUrlOpenLoading);
final updated = await moodle._updateAutoLoginKey();
CuckooFullScreenIndicator().stopLoading();
if (!updated) return false;
}
// Always request a new key
// If fails, fallback to existing cached key if any
CuckooFullScreenIndicator()
.startLoading(message: Constants.kMoodleUrlOpenLoading);
final hasKey = await moodle._updateAutoLoginKey();
CuckooFullScreenIndicator().stopLoading();
if (!hasKey) return false;
// Construct url
Uri finalUrl = moodle._buildMoodleUrl(
entryPoint: 'admin/tool/mobile/autologin.php',
params: {
'userid': moodle._userId,
'key': moodle._autoLoginKey!,
'key': moodle._autoLoginInfo!.key,
'urltogo': url
});
try {
Expand All @@ -534,6 +546,11 @@ class Moodle {
// Tokens and site info
_wstoken = _prefs.getString(MoodleStorageKeys.wstoken);
_privatetoken = _prefs.getString(MoodleStorageKeys.privatetoken);
final autoLoginInfo = _prefs.getString(MoodleStorageKeys.autoLoginInfo);
if (autoLoginInfo != null) {
_autoLoginInfo =
MoodleAutoLoginInfo.fromJson(jsonDecode(autoLoginInfo));
}
final siteInfo = _prefs.getString(MoodleStorageKeys.siteInfo);
if (siteInfo != null) {
_siteInfo = MoodleSiteInfo.fromJson(jsonDecode(siteInfo));
Expand Down Expand Up @@ -578,12 +595,22 @@ class Moodle {
_prefs.setString(MoodleStorageKeys.privatetoken, _privatetoken!);
}
// Courses
_saveCourses();
// Events
_saveEvents();
}

/// Save courses only to local storage.
Future<void> _saveCourses() async {
_prefs.setStringList(
MoodleStorageKeys.courses,
courseManager.courses
.map((course) => jsonEncode(course.toJson()))
.toList());
// Events
}

/// Save events only to local storage.
Future<void> _saveEvents() async {
_prefs.setStringList(
MoodleStorageKeys.events,
eventManager.events
Expand Down Expand Up @@ -803,13 +830,24 @@ class Moodle {
final response = await _callMoodleFunction(MoodleFunctions.getAutoLoginKey,
params: {'privatetoken': _privatetoken});
if (!response.fail && response.data != null) {
_autoLoginKey = response.data['key'];
// Invalidate after 360 sec
Future.delayed(const Duration(seconds: 360))
.then((_) => _autoLoginKey = null);
final info = MoodleAutoLoginInfo();
info
..key = response.data['key']
..lastRequested = DateTime.now().secondEpoch;
_autoLoginInfo = info;
_prefs.setString(
MoodleStorageKeys.autoLoginInfo, jsonEncode(info.toJson()));
return true;
}
// If fails, means that the key has not expired yet
// In that case, use the previously saved key
// Unless it has expired for sure
if (_autoLoginInfo != null) {
final secsElapsedFromLast =
DateTime.now().secondEpoch - _autoLoginInfo!.lastRequested.toInt();
if (secsElapsedFromLast > 600) return false;
return true;
}
_autoLoginKey = null;
return false;
}

Expand Down
5 changes: 3 additions & 2 deletions lib/src/common/services/moodle_extra.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ enum MoodleCourseSortingType { byCourseCode, byLastAccessed }
class MoodleStorageKeys {
static const wstoken = 'moodle_wstoken';
static const privatetoken = 'moodle_privatetoken';
static const autoLoginInfo = 'moodle_autologininfo';
static const siteInfo = 'moodle_site_info';
static const courses = 'moodle_courses';
static const events = 'moodle_events';
Expand Down Expand Up @@ -138,7 +139,7 @@ extension MoodleEventExtension on MoodleEvent {
set completionMark(bool c) {
completed = c;
Moodle().eventManager._notifyManually(flushCache: true);
Moodle()._save();
Moodle()._saveEvents();
bool shouldSync =
Settings().get<bool>(SettingsKey.syncCompletionStatus) ?? true;
if (shouldSync) Moodle.syncEventCompletion();
Expand Down Expand Up @@ -182,7 +183,7 @@ extension MoodleCourseExtension on MoodleCourse {
set favoriteMark(bool fav) {
customFavorite = fav;
Moodle().courseManager._notifyManually();
Moodle()._save();
Moodle()._saveCourses();
}

/// Mark the access to the course.
Expand Down
36 changes: 27 additions & 9 deletions lib/src/common/services/moodle_managers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,22 @@ class MoodleCourseManager with ChangeNotifier {
// Reject change
return;
}
// Check existing course map for consistent coloring
// Check existing course map for consistent coloring, cached contents,
// and favorite status as well
final currentTimestamp = DateTime.now().secondEpoch;
for (final course in courses) {
final existingCourse = _courseMap[course.id];
if (existingCourse != null) {
course.colorHex = existingCourse.colorHex;
course.customFavorite = existingCourse.customFavorite;
course
..colorHex = existingCourse.colorHex
..customFavorite = existingCourse.customFavorite
..cachedContents = existingCourse.cachedContents
..cachedTime = existingCourse.cachedTime;
// Expire cache if needed
if (course.cachedTime != null &&
currentTimestamp - course.cachedTime!.toInt() > 7 * 86400) {
course.cachedContents = null;
}
}
course.fullname = course.fullname.htmlParsed;
course.displayname = course.displayname.htmlParsed;
Expand All @@ -131,6 +140,15 @@ class MoodleCourseManager with ChangeNotifier {
_generateCourseMap();
}

/// Clear all cached course contents.
///
/// For `Moodle` use ONLY. DO NOT call it elsewhere.
void _clearCachedCourseContents() {
for (final course in courses) {
course.cachedContents = null;
}
}

/// Obtain a list containing IDs of all courses.
///
/// For `Moodle` use ONLY. DO NOT call it elsewhere.
Expand Down Expand Up @@ -194,12 +212,6 @@ class MoodleEventManager with ChangeNotifier {
/// Used for showing loading indicator / error on the page.
MoodleManagerStatus get status => _status;

/// Notify all event subscribers to rebuild their views.
///
/// Used for asking the views to call `groupedEvents` once in order to sync
/// any possible updates.
void rebuildNow() => _notifyManually(flushCache: true);

/// Grouped events given a grouping type.
///
/// If the events are grouped by course, they are first sorted by course in
Expand Down Expand Up @@ -299,6 +311,12 @@ class MoodleEventManager with ChangeNotifier {

// ----------------------Context Watch Interfaces End----------------------

/// Notify all event subscribers to rebuild their views.
///
/// Used for asking the views to call `groupedEvents` once in order to sync
/// any possible updates.
void rebuildNow() => _notifyManually(flushCache: true);

/// Timestamp where events are last updated.
/// Not preserved in storage.
DateTime? _eventsLastUpdated;
Expand Down
Loading

0 comments on commit 548220f

Please sign in to comment.