diff --git a/upgrade.txt b/upgrade.txt index 05707ffed43..37ed5bd02c2 100644 --- a/upgrade.txt +++ b/upgrade.txt @@ -1,6 +1,10 @@ This files describes API changes in the Moodle Mobile app, information provided here is intended especially for developers. +=== 3.4.1 === + + * Some modules implemented a siteId param in the getDownloadSize of their prefetch handler. This param was never sent, so it has been replaced by a "single" param. + === 3.4 === * For performance reasons, $mmCoursesDelegate has changed a bit: diff --git a/www/addons/competency/services/handlers.js b/www/addons/competency/services/handlers.js index c1b971d6afb..f587fc46f30 100644 --- a/www/addons/competency/services/handlers.js +++ b/www/addons/competency/services/handlers.js @@ -228,6 +228,21 @@ angular.module('mm.addons.competency') }); }; + /** + * Prefetch the addon for a certain course. + * + * @param {Object} course Course to prefetch. + * @return {Promise} Promise resolved when the prefetch is finished. + */ + self.prefetch = function(course) { + // Invalidate data to be sure to get the latest info. + return $mmaCompetency.invalidateCourseCompetencies(course.id).catch(function() { + // Ignore errors. + }).then(function() { + return $mmaCompetency.getCourseCompetencies(course.id); + }); + }; + return self; }; diff --git a/www/addons/coursecompletion/services/handlers.js b/www/addons/coursecompletion/services/handlers.js index 775cd9121aa..471e9fab6a6 100644 --- a/www/addons/coursecompletion/services/handlers.js +++ b/www/addons/coursecompletion/services/handlers.js @@ -260,6 +260,21 @@ angular.module('mm.addons.coursecompletion') }); }; + /** + * Prefetch the addon for a certain course. + * + * @param {Object} course Course to prefetch. + * @return {Promise} Promise resolved when the prefetch is finished. + */ + self.prefetch = function(course) { + // Invalidate data to be sure to get the latest info. + return $mmaCourseCompletion.invalidateCourseCompletion(course.id).catch(function() { + // Ignore errors. + }).then(function() { + return $mmaCourseCompletion.getCompletion(course.id); + }); + }; + return self; }; diff --git a/www/addons/grades/services/handlers.js b/www/addons/grades/services/handlers.js index 1f1a676b3bc..8e1715a6cd1 100644 --- a/www/addons/grades/services/handlers.js +++ b/www/addons/grades/services/handlers.js @@ -128,6 +128,21 @@ angular.module('mm.addons.grades') }; }; + /** + * Prefetch the addon for a certain course. + * + * @param {Object} course Course to prefetch. + * @return {Promise} Promise resolved when the prefetch is finished. + */ + self.prefetch = function(course) { + // Invalidate data to be sure to get the latest info. + return $mmGrades.invalidateGradesTableData(course.id).catch(function() { + // Ignore errors. + }).then(function() { + return $mmGrades.getGradesTable(course.id); + }); + }; + return self; }; diff --git a/www/addons/mod/lesson/services/handlers.js b/www/addons/mod/lesson/services/handlers.js index 61fb53e1c37..86325076b8d 100644 --- a/www/addons/mod/lesson/services/handlers.js +++ b/www/addons/mod/lesson/services/handlers.js @@ -96,9 +96,9 @@ angular.module('mm.addons.mod_lesson') downloadBtn.hidden = true; refreshBtn.hidden = true; - $mmaModLessonPrefetchHandler.getDownloadSize(module, courseId).then(function(size) { + $mmaModLessonPrefetchHandler.getDownloadSize(module, courseId, true).then(function(size) { $mmUtil.confirmDownloadSize(size).then(function() { - return $mmaModLessonPrefetchHandler.prefetch(module, courseId).catch(function(error) { + return $mmaModLessonPrefetchHandler.prefetch(module, courseId, true).catch(function(error) { if (!$scope.$$destroyed) { $mmUtil.showErrorModalDefault(error, 'mm.core.errordownloading', true); return $q.reject(); diff --git a/www/addons/mod/lesson/services/prefetch_handler.js b/www/addons/mod/lesson/services/prefetch_handler.js index 7b58cdf68aa..9e7ac57a48b 100644 --- a/www/addons/mod/lesson/services/prefetch_handler.js +++ b/www/addons/mod/lesson/services/prefetch_handler.js @@ -162,21 +162,20 @@ angular.module('mm.addons.mod_lesson') * @name $mmaModLessonPrefetchHandler#getDownloadSize * @param {Object} module Module to get the size. * @param {Number} courseId Course ID the module belongs to. - * @param {String} [siteId] Site ID. If not defined, current site. - * @return {Object} With the file size and a boolean to indicate if it is the total size or only partial. + * @param {Boolean} single True if we're downloading a single module, false if we're downloading a whole section. + * @return {Promise} Resolved With the file size and a boolean to indicate if it is the total size or only partial. */ - self.getDownloadSize = function(module, courseId, siteId) { - siteId = siteId || $mmSite.getId(); - + self.getDownloadSize = function(module, courseId, single) { var lesson, password, - result; + result, + siteId = $mmSite.getId(); return $mmaModLesson.getLesson(courseId, module.id, siteId).then(function(lessonData) { lesson = lessonData; // Get the lesson password if it's needed. - return self.gatherLessonPassword(lesson.id, false, true, true, siteId); + return self.gatherLessonPassword(lesson.id, false, true, single, siteId); }).then(function(data) { password = data.password; lesson = data.lesson || lesson; diff --git a/www/addons/mod/quiz/services/prefetch_handler.js b/www/addons/mod/quiz/services/prefetch_handler.js index 1bad3d95e20..c7022ed4350 100644 --- a/www/addons/mod/quiz/services/prefetch_handler.js +++ b/www/addons/mod/quiz/services/prefetch_handler.js @@ -139,10 +139,10 @@ angular.module('mm.addons.mod_quiz') * @name $mmaModQuizPrefetchHandler#getDownloadSize * @param {Object} module Module to get the size. * @param {Number} courseId Course ID the module belongs to. - * @param {String} [siteId] Site ID. If not defined, current site. + * @param {Boolean} single True if we're downloading a single module, false if we're downloading a whole section. * @return {Object} With the file size and a boolean to indicate if it is the total size or only partial. */ - self.getDownloadSize = function(module, courseId, siteId) { + self.getDownloadSize = function(module, courseId, single) { return {size: -1, total: false}; }; diff --git a/www/addons/mod/wiki/services/prefetch_handler.js b/www/addons/mod/wiki/services/prefetch_handler.js index 65ad4e2d72b..8745f8ebace 100644 --- a/www/addons/mod/wiki/services/prefetch_handler.js +++ b/www/addons/mod/wiki/services/prefetch_handler.js @@ -52,12 +52,12 @@ angular.module('mm.addons.mod_wiki') * @name $mmaModWikiPrefetchHandler#getDownloadSize * @param {Object} module Module to get the size. * @param {Number} courseId Course ID the module belongs to. - * @param {String} [siteId] Site ID. If not defined, current site. + * @param {Boolean} single True if we're downloading a single module, false if we're downloading a whole section. * @return {Promise} With the file size and a boolean to indicate if it is the total size or only partial. */ - self.getDownloadSize = function(module, courseId, siteId) { - var promises = []; - siteId = siteId || $mmSite.getId(); + self.getDownloadSize = function(module, courseId, single) { + var promises = [], + siteId = $mmSite.getId(); promises.push(self.getFiles(module, courseId, siteId).then(function(files) { return $mmUtil.sumFileSizes(files); diff --git a/www/addons/myoverview/controllers/index.js b/www/addons/myoverview/controllers/index.js index 3e4e9bac05d..b8f7076646a 100644 --- a/www/addons/myoverview/controllers/index.js +++ b/www/addons/myoverview/controllers/index.js @@ -21,7 +21,8 @@ angular.module('mm.addons.myoverview') * @ngdoc controller * @name mmaMyOverviewCtrl */ -.controller('mmaMyOverviewCtrl', function($scope, $mmaMyOverview, $mmUtil, $q, $mmCourses, $mmCoursesDelegate) { +.controller('mmaMyOverviewCtrl', function($scope, $mmaMyOverview, $mmUtil, $q, $mmCourses, $mmCoursesDelegate, $mmCourseHelper) { + var prefetchIconsInitialized = false; $scope.tabShown = 'courses'; $scope.timeline = { @@ -44,6 +45,11 @@ angular.module('mm.addons.myoverview') $scope.showFilter = false; $scope.searchEnabled = $mmCourses.isSearchCoursesAvailable() && !$mmCourses.isSearchCoursesDisabledInSite(); + $scope.prefetchCoursesData = { + inprogress: {}, + past: {}, + future: {} + }; function fetchMyOverviewTimeline(afterEventId, refresh) { return $mmaMyOverview.getActionEventsByTimesort(afterEventId).then(function(events) { @@ -104,6 +110,8 @@ angular.module('mm.addons.myoverview') $scope.courses.inprogress.push(course); } }); + + initPrefetchCoursesIcons(); }).catch(function(message) { $mmUtil.showErrorModalDefault(message, 'Error getting my overview data.'); return $q.reject(); @@ -137,6 +145,34 @@ angular.module('mm.addons.myoverview') }); } + // Initialize the prefetch icon for selected courses. + function initPrefetchCoursesIcons() { + if (prefetchIconsInitialized) { + // Already initialized. + return; + } + + prefetchIconsInitialized = true; + + Object.keys($scope.prefetchCoursesData).forEach(function(filter) { + if (!$scope.courses[filter] || $scope.courses[filter].length < 2) { + // Not enough courses. + $scope.prefetchCoursesData[filter].icon = ''; + return; + } + + $mmCourseHelper.determineCoursesStatus($scope.courses[filter]).then(function(status) { + var icon = $mmCourseHelper.getCourseStatusIconFromStatus(status); + if (icon == 'spinner') { + // It seems all courses are being downloaded, show a download button instead. + icon = 'ion-ios-cloud-download-outline'; + } + $scope.prefetchCoursesData[filter].icon = icon; + }); + + }); + } + $scope.switchFilter = function() { $scope.showFilter = !$scope.showFilter; if (!$scope.showFilter) { @@ -175,6 +211,7 @@ angular.module('mm.addons.myoverview') } break; case 'courses': + prefetchIconsInitialized = false; promise = fetchMyOverviewCourses(); break; } @@ -238,4 +275,26 @@ angular.module('mm.addons.myoverview') course.canLoadMore = courseEvents.canLoadMore; }); }; + + // Download all the shown courses. + $scope.downloadCourses = function() { + var selected = $scope.courses.selected, + selectedData = $scope.prefetchCoursesData[selected], + initialIcon = selectedData.icon; + + selectedData.icon = 'spinner'; + selectedData.badge = ''; + return $mmCourseHelper.confirmAndPrefetchCourses($scope.courses[selected]).then(function(downloaded) { + selectedData.icon = downloaded ? 'ion-android-refresh' : initialIcon; + }, function(error) { + if (!$scope.$$destroyed) { + $mmUtil.showErrorModalDefault(error, 'mm.course.errordownloadingcourse', true); + selectedData.icon = initialIcon; + } + }, function(progress) { + selectedData.badge = progress.count + ' / ' + progress.total; + }).finally(function() { + selectedData.badge = ''; + }); + }; }); diff --git a/www/addons/myoverview/templates/index.html b/www/addons/myoverview/templates/index.html index d9c440450a8..064c779db93 100644 --- a/www/addons/myoverview/templates/index.html +++ b/www/addons/myoverview/templates/index.html @@ -29,10 +29,7 @@
-
- -
-
+
-
- +
+ + + + + +
diff --git a/www/addons/notes/services/handlers.js b/www/addons/notes/services/handlers.js index 9710cba8bfb..fc5725d8533 100644 --- a/www/addons/notes/services/handlers.js +++ b/www/addons/notes/services/handlers.js @@ -295,6 +295,27 @@ angular.module('mm.addons.notes') }); }; + /** + * Prefetch the addon for a certain course. + * + * @param {Object} course Course to prefetch. + * @return {Promise} Promise resolved when the prefetch is finished. + */ + self.prefetch = function(course) { + // Invalidate data to be sure to get the latest info. + return $mmaNotes.invalidateNotes(course.id).catch(function() { + // Ignore errors. + }).then(function() { + return $mmaNotes.getNotes(course.id, false, true); + }).then(function(notesTypes) { + var promises = []; + angular.forEach(notesTypes, function(notes) { + promises.push($mmaNotes.getNotesUserData(notes, course.id)); + }); + return $mmUtil.allPromises(promises); + }); + }; + return self; }; diff --git a/www/addons/participants/services/handlers.js b/www/addons/participants/services/handlers.js index c1e91cf39f9..0bedb88cb0d 100644 --- a/www/addons/participants/services/handlers.js +++ b/www/addons/participants/services/handlers.js @@ -125,6 +125,21 @@ angular.module('mm.addons.participants') return $mmaParticipants.isPluginEnabledForCourse(courseId); }; + /** + * Prefetch the addon for a certain course. + * + * @param {Object} course Course to prefetch. + * @return {Promise} Promise resolved when the prefetch is finished. + */ + self.prefetch = function(course) { + // Invalidate data to be sure to get the latest info. + return $mmaParticipants.invalidateParticipantsList(course.id).catch(function() { + // Ignore errors. + }).then(function() { + return $mmaParticipants.getAllParticipants(course.id); + }); + }; + return self; }; diff --git a/www/addons/participants/services/participants.js b/www/addons/participants/services/participants.js index 20c531ea208..aa45b7b9bde 100644 --- a/www/addons/participants/services/participants.js +++ b/www/addons/participants/services/participants.js @@ -27,6 +27,34 @@ angular.module('mm.addons.participants') var self = {}; + /** + * Get all the participants for a certain course, performing 1 request per page until there are no more participants. + * + * @module mm.addons.participants + * @ngdoc method + * @name $mmaParticipants#getAllParticipants + * @param {Number} courseId ID of the course. + * @param {Number} [limitFrom] Position of the first participant to get. + * @param {Number} [limitNumber] Number of participants to get in each request. + * @param {String} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise to be resolved when the participants are retrieved. + */ + self.getAllParticipants = function(courseId, limitFrom, limitNumber, siteId) { + siteId = siteId || $mmSite.getId(); + limitFrom = limitFrom || 0; + + return self.getParticipants(courseId, limitFrom, limitNumber, siteId).then(function(data) { + if (data.canLoadMore) { + // Load the next "page". + limitFrom = limitFrom + data.participants.length; + return self.getAllParticipants(courseId, limitFrom, limitNumber, siteId).then(function(nextParts) { + return data.participants.concat(nextParts); + }); + } + return data.participants; + }); + }; + /** * Get cache key for participant list WS calls. * @@ -43,7 +71,7 @@ angular.module('mm.addons.participants') * @module mm.addons.participants * @ngdoc method * @name $mmaParticipants#getParticipants - * @param {String} courseId ID of the course. + * @param {Number} courseId ID of the course. * @param {Number} limitFrom Position of the first participant to get. * @param {Number} limitNumber Number of participants to get. * @param {String} [siteId] Site Id. If not defined, use current site. diff --git a/www/core/components/course/controllers/sections.js b/www/core/components/course/controllers/sections.js index 18e3a84efab..ae6eee6d3cc 100644 --- a/www/core/components/course/controllers/sections.js +++ b/www/core/components/course/controllers/sections.js @@ -23,7 +23,7 @@ angular.module('mm.core.course') */ .controller('mmCourseSectionsCtrl', function($mmCourse, $mmUtil, $scope, $stateParams, $translate, $mmCourseHelper, $mmEvents, $mmSite, $mmCoursePrefetchDelegate, $mmCourses, $q, $ionicHistory, $ionicPlatform, mmCoreCourseAllSectionsId, - mmCoreEventSectionStatusChanged, $state, $timeout, $mmCoursesDelegate, $controller) { + mmCoreEventSectionStatusChanged, $state, $timeout, $mmCoursesDelegate, $controller, mmCoreEventCourseStatusChanged) { var courseId = $stateParams.courseid, sectionId = parseInt($stateParams.sid, 10), sectionNumber = parseInt($stateParams.sectionnumber, 10), @@ -37,6 +37,7 @@ angular.module('mm.core.course') $scope.downloadSectionsIcon = getDownloadSectionIcon(); $scope.sectionHasContent = $mmCourseHelper.sectionHasContent; $scope.courseActions = []; + $scope.prefetchCourseIcon = 'spinner'; // Show spinner while calculating it. function loadSections(refresh) { var promise; @@ -76,7 +77,8 @@ angular.module('mm.core.course') // Fake event (already prevented) to avoid errors on app. var ev = document.createEvent("MouseEvent"); return newScope.action(ev, course); - } + }, + prefetch: button.prefetch }; newScope.$destroy(); @@ -145,6 +147,13 @@ angular.module('mm.core.course') }); } + // Determines the prefetch icon of a course. + function determineCoursePrefetchIcon() { + return $mmCourseHelper.getCourseStatusIcon(courseId).then(function(icon) { + $scope.prefetchCourseIcon = icon; + }); + } + // Prefetch a section. The second parameter indicates if the prefetch was started manually (true) // or it was automatically started because all modules are being downloaded (false). function prefetch(section, manual) { @@ -240,14 +249,51 @@ angular.module('mm.core.course') section.isCalculating = true; $mmCourseHelper.confirmDownloadSize(courseId, section, $scope.sections).then(function() { prefetch(section, true); + }, function(error) { + // User cancelled or there was an error calculating the size. + if (error) { + $mmUtil.showErrorModal(error); + } }).finally(function() { section.isCalculating = false; }); }; + // Prefetch the whole course, including the course options. + $scope.prefetchCourse = function() { + $mmCourseHelper.confirmAndPrefetchCourse($scope, course, $scope.sections, $scope.courseActions).then(function(downloaded) { + if (downloaded && $scope.downloadSectionsEnabled) { + // Recalculate the status. + calculateSectionStatus(false); + } + }); + }; + + // Load the sections. loadSections().finally(function() { autoloadSection(); $scope.sectionsLoaded = true; + + // Determine the course prefetch status. + determineCoursePrefetchIcon().then(function() { + if ($scope.prefetchCourseIcon == 'spinner') { + // Course is being downloaded. Get the download promise. + var promise = $mmCourseHelper.getCourseDownloadPromise(courseId); + if (promise) { + // There is a download promise. Show an error if it fails. + promise.catch(function(error) { + if (!$scope.$$destroyed) { + $mmUtil.showErrorModalDefault(error, 'mm.course.errordownloadingcourse', true); + } + }); + } else { + // No download, this probably means that the app was closed while downloading. Set previous status. + $mmCourse.setCoursePreviousStatus(courseId).then(function(status) { + $scope.prefetchCourseIcon = $mmCourseHelper.getCourseStatusIconFromStatus(status); + }); + } + } + }); }); // Listen for section status changes. @@ -279,7 +325,14 @@ angular.module('mm.core.course') } }); + var courseStatusObserver = $mmEvents.on(mmCoreEventCourseStatusChanged, function(data) { + if (data.siteId == $mmSite.getId() && data.courseId == courseId) { + $scope.prefetchCourseIcon = $mmCourseHelper.getCourseStatusIconFromStatus(data.status); + } + }); + $scope.$on('$destroy', function() { statusObserver && statusObserver.off && statusObserver.off(); + courseStatusObserver && courseStatusObserver.off && courseStatusObserver.off(); }); }); diff --git a/www/core/components/course/lang/en.json b/www/core/components/course/lang/en.json index 88811317e51..8c6cb9d35c1 100644 --- a/www/core/components/course/lang/en.json +++ b/www/core/components/course/lang/en.json @@ -11,6 +11,8 @@ "contents": "Contents", "couldnotloadsectioncontent": "Could not load the section content. Please try again later.", "couldnotloadsections": "Could not load the sections. Please try again later.", + "downloadcourse": "Download course", + "errordownloadingcourse": "Error downloading course.", "errordownloadingsection": "Error downloading section.", "errorgetmodule": "Error getting activity data.", "hiddenfromstudents": "Hidden from students", diff --git a/www/core/components/course/services/course.js b/www/core/components/course/services/course.js index a26ab1aa1be..1a06d0776dd 100644 --- a/www/core/components/course/services/course.js +++ b/www/core/components/course/services/course.js @@ -15,12 +15,22 @@ angular.module('mm.core.course') .constant('mmCoreCourseModulesStore', 'course_modules') // @deprecated since version 2.6. Please do not use. +.constant('mmCoreCourseStatusStore', 'course_status') -.config(function($mmSitesFactoryProvider, mmCoreCourseModulesStore) { +.config(function($mmSitesFactoryProvider, mmCoreCourseModulesStore, mmCoreCourseStatusStore) { var stores = [ { name: mmCoreCourseModulesStore, keyPath: 'id' + }, + { + name: mmCoreCourseStatusStore, + keyPath: 'id', + indexes: [ + { + name: 'status', + } + ] } ]; $mmSitesFactoryProvider.registerStores(stores); @@ -33,7 +43,8 @@ angular.module('mm.core.course') * @ngdoc service * @name $mmCourse */ -.factory('$mmCourse', function($mmSite, $translate, $q, $log, $mmEvents, $mmSitesManager, mmCoreEventCompletionModuleViewed) { +.factory('$mmCourse', function($mmSite, $translate, $q, $log, $mmEvents, $mmSitesManager, mmCoreEventCompletionModuleViewed, + mmCoreCourseStatusStore, mmCoreDownloading, mmCoreNotDownloaded, mmCoreEventCourseStatusChanged, $mmUtil) { $log = $log.getInstance('$mmCourse'); @@ -110,6 +121,32 @@ angular.module('mm.core.course') } }; + /** + * Clear all courses status in a site. + * + * @module mm.core.course + * @ngdoc method + * @name $mmCourse#clearAllCoursesStatus + * @param {String} siteId Site ID. + * @return {Promise} Promise resolved when all status are cleared. + */ + self.clearAllCoursesStatus = function(siteId) { + var promises = []; + $log.debug('Clear all course status for site ' + siteId); + return $mmSitesManager.getSite(siteId).then(function(site) { + var db = site.getDb(); + return db.getAll(mmCoreCourseStatusStore).then(function(entries) { + angular.forEach(entries, function(entry) { + promises.push(db.remove(mmCoreCourseStatusStore, entry.id).then(function() { + // Trigger course status changed, setting it as not downloaded. + self._triggerCourseStatusChanged(entry.id, mmCoreNotDownloaded, siteId); + })); + }); + return $q.all(promises); + }); + }); + }; + /** * Get completion status of all the activities in a course for a certain user. * @@ -156,6 +193,47 @@ angular.module('mm.core.course') return 'mmCourse:activitiescompletion:' + courseid + ':' + userid; } + /** + * Get the data stored for a course. + * + * @module mm.core.course + * @ngdoc method + * @name $mmCourse#getCourseStatusData + * @param {Number} courseId Course ID. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the data. + */ + self.getCourseStatusData = function(courseId, siteId) { + return $mmSitesManager.getSite(siteId).then(function(site) { + var db = site.getDb(); + + return db.get(mmCoreCourseStatusStore, courseId).then(function(entry) { + if (!entry) { + return $q.reject(); + } + return entry; + }); + }); + }; + + /** + * Get a course status. + * + * @module mm.core.course + * @ngdoc method + * @name $mmCourse#getCourseStatus + * @param {Number} courseId Course ID. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the status. + */ + self.getCourseStatus = function(courseId, siteId) { + return self.getCourseStatusData(courseId, siteId).then(function(entry) { + return entry.status || mmCoreNotDownloaded; + }).catch(function() { + return mmCoreNotDownloaded; + }); + }; + /** * Gets a module basic info by module ID. * @@ -630,6 +708,107 @@ angular.module('mm.core.course') }); }; + /** + * Change the course status, setting it to the previous status. + * + * @module mm.core.course + * @ngdoc method + * @name $mmCourse#setCoursePreviousStatus + * @param {Number} courseId Course ID. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the status is changed. Resolve param: new status. + */ + self.setCoursePreviousStatus = function(courseId, siteId) { + siteId = siteId || $mmSite.getId(); + + $log.debug('Set previous status for course ' + courseId + ' in site ' + siteId); + + return $mmSitesManager.getSite(siteId).then(function(site) { + var db = site.getDb(); + + // Get current stored data, we'll only update 'status' and 'updated' fields. + return db.get(mmCoreCourseStatusStore, courseId).then(function(entry) { + if (entry.status == mmCoreDownloading) { + // Going back from downloading to previous status, restore previous download time. + entry.downloadtime = entry.previousdownloadtime; + } + entry.status = entry.previous || mmCoreNotDownloaded; + entry.updated = Date.now(); + $log.debug('Set previous status \'' + entry.status + '\' for course ' + courseId); + + return db.insert(mmCoreCourseStatusStore, entry).then(function() { + // Success updating, trigger event. + self._triggerCourseStatusChanged(courseId, entry.status, siteId); + return entry.status; + }); + }); + }); + }; + + /** + * Store course status. + * + * @module mm.core.course + * @ngdoc method + * @name $mmCourse#setCourseStatus + * @param {Number} courseId Course ID. + * @param {String} status New course status. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the status is stored. + */ + self.setCourseStatus = function(courseId, status, siteId) { + siteId = siteId || $mmSite.getId(); + + $log.debug('Set status \'' + status + '\' for course ' + courseId + ' in site ' + siteId); + + return $mmSitesManager.getSite(siteId).then(function(site) { + var db = site.getDb(), + downloadTime, + previousDownloadTime; + + if (status == mmCoreDownloading) { + // Set download time if course is now downloading. + downloadTime = $mmUtil.timestamp(); + } + + // Search current status to set it as previous status. + return db.get(mmCoreCourseStatusStore, courseId).then(function(entry) { + if (typeof downloadTime == 'undefined') { + // Keep previous download time. + downloadTime = entry.downloadtime; + previousDownloadTime = entry.previousdownloadtime; + } else { + // downloadTime will be updated, store current time as previous. + previousDownloadTime = entry.downloadTime; + } + + return entry.status; + }).catch(function() { + return undefined; // No previous status. + }).then(function(previousStatus) { + var promise; + if (previousStatus === status) { + // The course already has this status, no need to change it. + promise = $q.when(); + } else { + promise = db.insert(mmCoreCourseStatusStore, { + id: courseId, + status: status, + previous: previousStatus, + updated: new Date().getTime(), + downloadtime: downloadTime, + previousdownloadtime: previousDownloadTime + }); + } + + return promise.then(function() { + // Success inserting, trigger event. + self._triggerCourseStatusChanged(courseId, status, siteId); + }); + }); + }); + }; + /** * Translate a module name to current language. * @@ -650,6 +829,26 @@ angular.module('mm.core.course') return translated !== langKey ? translated : moduleName; }; + /** + * Trigger mmCoreEventCourseStatusChanged with the right data. + * + * @module mm.core.course + * @ngdoc method + * @name $mmCourse#_triggerCourseStatusChanged + * @param {Number} courseId Course ID. + * @param {String} status New course status. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Void} + * @protected + */ + self._triggerCourseStatusChanged = function(courseId, status, siteId) { + var data = { + siteId: siteId, + courseId: courseId, + status: status + }; + $mmEvents.trigger(mmCoreEventCourseStatusChanged, data); + }; return self; }); diff --git a/www/core/components/course/services/helper.js b/www/core/components/course/services/helper.js index 39d7c1ad953..707ad61d08d 100644 --- a/www/core/components/course/services/helper.js +++ b/www/core/components/course/services/helper.js @@ -24,10 +24,11 @@ angular.module('mm.core.course') .factory('$mmCourseHelper', function($q, $mmCoursePrefetchDelegate, $mmFilepool, $mmUtil, $mmCourse, $mmSite, $state, $mmText, mmCoreNotDownloaded, mmCoreOutdated, mmCoreDownloading, mmCoreCourseAllSectionsId, $mmSitesManager, $mmAddonManager, $controller, $mmCourseDelegate, $translate, $mmEvents, mmCoreEventPackageStatusChanged, mmCoreNotDownloadable, - mmCoreDownloaded) { + mmCoreDownloaded, $mmCoursesDelegate) { var self = {}, - calculateSectionStatus = false; + calculateSectionStatus = false, + courseDwnPromises = {}; /** @@ -174,16 +175,17 @@ angular.module('mm.core.course') * @module mm.core.course * @ngdoc method * @name $mmCourseHelper#confirmDownloadSize - * @param {Number} courseid Course ID the section belongs to. - * @param {Object} section Section. - * @param {Object[]} sections List of sections. Used when downloading all the sections. - * @return {Promise} Promise resolved if the user confirms or there's no need to confirm. + * @param {Number} courseid Course ID the section belongs to. + * @param {Object} [section] Section. If not provided, all sections. + * @param {Object[]} [sections] List of sections. Used when downloading all the sections. + * @param {Boolean} [alwaysConfirm] True to show a confirm even if the size isn't high, false otherwise. + * @return {Promise} Promise resolved if the user confirms or there's no need to confirm. */ - self.confirmDownloadSize = function(courseid, section, sections) { + self.confirmDownloadSize = function(courseid, section, sections, alwaysConfirm) { var sizePromise; // Calculate the size of the download. - if (section.id != mmCoreCourseAllSectionsId) { + if (section && section.id != mmCoreCourseAllSectionsId) { sizePromise = $mmCoursePrefetchDelegate.getDownloadSize(section.modules, courseid); } else { var promises = [], @@ -207,7 +209,7 @@ angular.module('mm.core.course') return sizePromise.then(function(size) { // Show confirm modal if needed. - return $mmUtil.confirmDownloadSize(size); + return $mmUtil.confirmDownloadSize(size, undefined, undefined, undefined, undefined, alwaysConfirm); }); }; @@ -693,9 +695,9 @@ angular.module('mm.core.course') scope.prefetchStatusIcon = 'spinner'; // Show spinner since this operation might take a while. // We need to call getDownloadSize, the package might have been updated. - return $mmCoursePrefetchDelegate.getModuleDownloadSize(module, courseId).then(function(size) { + return $mmCoursePrefetchDelegate.getModuleDownloadSize(module, courseId, true).then(function(size) { return $mmUtil.confirmDownloadSize(size).then(function() { - return $mmCoursePrefetchDelegate.prefetchModule(module, courseId).catch(function(error) { + return $mmCoursePrefetchDelegate.prefetchModule(module, courseId, true).catch(function(error) { return failPrefetch(!scope.$$destroyed, error); }); }, function() { @@ -761,6 +763,272 @@ angular.module('mm.core.course') }); }; + /** + * Get a course download promise (if any). + * + * @module mm.core.course + * @ngdoc method + * @name $mmCourseHelper#getCourseDownloadPromise + * @param {Number} courseId Course ID. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Download promise, undefined if not found: + */ + self.getCourseDownloadPromise = function(courseId, siteId) { + siteId = siteId || $mmSite.getId(); + return courseDwnPromises[siteId] && courseDwnPromises[siteId][courseId]; + }; + + /** + * Get a course status icon. + * + * @module mm.core.course + * @ngdoc method + * @name $mmCourseHelper#getCourseStatusIcon + * @param {Number} courseId Course ID. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the icon name. + */ + self.getCourseStatusIcon = function(courseId, siteId) { + return $mmCourse.getCourseStatus(courseId, siteId).then(function(status) { + return self.getCourseStatusIconFromStatus(status); + }); + }; + + /** + * Get a course status icon from status. + * + * @module mm.core.course + * @ngdoc method + * @name $mmCourseHelper#getCourseStatusIconFromStatus + * @param {String} status Course status. + * @return {String} Icon name. + */ + self.getCourseStatusIconFromStatus = function(status) { + if (status == mmCoreDownloaded) { + // Always show refresh icon, we cannot knew if there's anything new in course options. + return 'ion-android-refresh'; + } else if (status == mmCoreDownloading) { + return 'spinner'; + } else { + return 'ion-ios-cloud-download-outline'; + } + }; + + /** + * Prefetch all the activities in a course and also the course options. + * + * @module mm.core.course + * @ngdoc method + * @name $mmCourseHelper#prefetchCourse + * @param {Object} course The course to prefetch. + * @param {Object[]} sections List of course sections. + * @param {Object[]} courseOptions List of course options. Each option should have a "prefetch" function if it's downloadable. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the download finishes. + */ + self.prefetchCourse = function(course, sections, courseOptions, siteId) { + siteId = siteId || $mmSite.getId(); + + if (courseDwnPromises[siteId] && courseDwnPromises[siteId][course.id]) { + // There's already a download ongoing for this course, return the promise. + return courseDwnPromises[siteId][course.id]; + } else if (!courseDwnPromises[siteId]) { + courseDwnPromises[siteId] = {}; + } + + // First of all, mark the course as being downloaded. + courseDwnPromises[siteId][course.id] = $mmCourse.setCourseStatus(course.id, mmCoreDownloading, siteId).then(function() { + var promises = [], + allSectionsSection; + + // Prefetch all the sections. If the first section is "All sections", use it. Otherwise, use a fake "All sections". + allSectionsSection = sections[0].id == mmCoreCourseAllSectionsId ? sections[0] : {id: mmCoreCourseAllSectionsId}; + promises.push(self.prefetch(allSectionsSection, course.id, sections)); + + // Prefetch course options. + angular.forEach(courseOptions, function(option) { + if (option.prefetch) { + promises.push($q.when(option.prefetch(course))); + } + }); + + return $mmUtil.allPromises(promises); + }).then(function() { + // Download success, mark the course as downloaded. + return $mmCourse.setCourseStatus(course.id, mmCoreDownloaded, siteId); + }).catch(function(error) { + // Error, restore previous status. + return $mmCourse.setCoursePreviousStatus(course.id, siteId).then(function() { + return $q.reject(error); + }); + }).finally(function() { + delete courseDwnPromises[siteId][course.id]; + }); + + return courseDwnPromises[siteId][course.id]; + }; + + /** + * Show a confirm and prefetch a course. It will retrieve the sections and the course options if not provided. + * This function will set the icon to "spinner" when starting and it will also set it back to the initial icon if the + * user cancels. All the other updates of the icon should be made when mmCoreEventCourseStatusChanged is received. + * + * @module mm.core.course + * @ngdoc method + * @name $mmCourseHelper#confirmAndPrefetchCourse + * @param {Object} scope Scope to set the icon. + * @param {Object} course Course to prefetch. + * @param {Object[]} [sections] List of course sections. + * @param {Object[]} [courseOptions] List of options. Each option should have a "prefetch" function if it's downloadable. + * @return {Promise} Promise resolved with true when the download finishes, resolved with false if user + * doesn't confirm, rejected if an error occurs. + */ + self.confirmAndPrefetchCourse = function(scope, course, sections, courseOptions) { + var initialIcon = scope.prefetchCourseIcon, + promise, + siteId = $mmSite.getId(); + + scope.prefetchCourseIcon = 'spinner'; + + // Get the sections first if needed. + if (sections) { + promise = $q.when(sections); + } else { + promise = $mmCourse.getSections(course.id, false, true); + } + + return promise.then(function(sections) { + // Confirm the download. + return self.confirmDownloadSize(course.id, undefined, sections, true).then(function() { + // User confirmed, get the course actions if needed. + if (courseOptions) { + promise = $q.when(courseOptions); + } else { + promise = $mmCoursesDelegate.getNavHandlersToDisplay(course, false, false, true); + } + + return promise.then(function(handlers) { + // Now we have all the data, download the course. + return self.prefetchCourse(course, sections, handlers, siteId); + }).then(function() { + // Download successful. + return true; + }); + }, function(error) { + // User cancelled or there was an error calculating the size. + if (error) { + $mmUtil.showErrorModal(error); + } + scope.prefetchCourseIcon = initialIcon; + return false; + }); + }).catch(function(error) { + // Don't show error message if scope is destroyed. + if (!scope.$$destroyed) { + $mmUtil.showErrorModalDefault(error, 'mm.course.errordownloadingcourse', true); + } + + return $q.reject(error); + }); + }; + + /** + * Confirm and prefetches a list of courses. + * + * @module mm.core.course + * @ngdoc method + * @name $mmCourseHelper#confirmAndPrefetchCourses + * @param {Object[]} courses List of courses to download. + * @return {Promise} Promise resolved with true when downloaded, resolved with false if user cancels, rejected if error. + * It will send a "progress" everytime a course is downloaded or fails to download. + */ + self.confirmAndPrefetchCourses = function(courses) { + var siteId = $mmSite.getId(); + + // Confirm the download without checking size because it could take a while. + return $mmUtil.showConfirm($translate('mm.core.areyousure')).then(function() { + var deferred = $q.defer(), // Use a deferred to be able to notify the progress. + promises = [], + total = courses.length, + count = 0; + + // Notify the start of the download. + setTimeout(function() { + deferred.notify({ + count: count, + total: total + }); + }); + + angular.forEach(courses, function(course) { + var subPromises = [], + sections, + handlers, + success = true; + + // Get the sections and the handlers. + subPromises.push($mmCourse.getSections(course.id, false, true).then(function(courseSections) { + sections = courseSections; + })); + subPromises.push($mmCoursesDelegate.getNavHandlersToDisplay(course, false, false, true).then(function(cHandlers) { + handlers = cHandlers; + })); + + promises.push($q.all(subPromises).then(function() { + return self.prefetchCourse(course, sections, handlers, siteId); + }).catch(function(error) { + success = false; + return $q.reject(error); + }).finally(function() { + // Course downloaded or failed, notify the progress. + count++; + deferred.notify({ + count: count, + total: total, + course: course.id, + success: success + }); + })); + }); + + $mmUtil.allPromises(promises).then(function() { + deferred.resolve(true); + }, deferred.reject); + + return deferred.promise; + }, function() { + // User cancelled. + return false; + }); + }; + + /** + * Determine the status of a list of courses. + * + * @ngdoc method + * @name $mmCourseHelper#determineCoursesStatus + * @param {Object[]} courses Courses + * @return {Promise} Promise resolved with the status. + */ + self.determineCoursesStatus = function(courses) { + // Get the status of each course. + var promises = [], + siteId = $mmSite.getId(); + + angular.forEach(courses, function(course) { + promises.push($mmCourse.getCourseStatus(course.id, siteId)); + }); + + return $q.all(promises).then(function(statuses) { + // Now determine the status of the whole list. + var status = statuses[0]; + for (var i = 1; i < statuses.length; i++) { + status = $mmFilepool.determinePackagesStatus(status, statuses[i]); + } + return status; + }); + }; + return self; }) diff --git a/www/core/components/course/services/prefetchdelegate.js b/www/core/components/course/services/prefetchdelegate.js index 5a4c9d4afd6..258bc2ee366 100644 --- a/www/core/components/course/services/prefetchdelegate.js +++ b/www/core/components/course/services/prefetchdelegate.js @@ -59,7 +59,7 @@ angular.module('mm.core') * - (Optional) updatesNames (RegExp) RegExp of update names to check. If getCourseUpdates returns * an update whose names matches this, the module will be marked * as outdated. Ignored if hasUpdates function is defined. - * - getDownloadSize(module, courseid) (Object|Promise) Get the download size of a module. + * - getDownloadSize(module, courseid, single) (Object|Promise) Get the download size of a module. * The returning object should have size field with file size * in bytes and and total field which indicates if it's been * able to calculate the total size (true) or only partial size @@ -619,14 +619,15 @@ angular.module('mm.core') * @name $mmCoursePrefetchDelegate#prefetchModule * @param {Object} module Module to be prefetch. * @param {Number} courseid Course ID the module belongs to. + * @param {Boolean} single True if we're downloading a single module, false if we're downloading a whole section. * @return {Promise} Promise resolved when finished. */ - self.prefetchModule = function(module, courseid) { + self.prefetchModule = function(module, courseid, single) { var handler = enabledHandlers[module.modname]; // Check if the module has a prefetch handler. if (handler) { - return handler.prefetch(module, courseid); + return handler.prefetch(module, courseid, single); } return $q.when(); }; @@ -639,9 +640,10 @@ angular.module('mm.core') * @name $mmCoursePrefetchDelegate#getModuleDownloadSize * @param {Object} module Module to be get info from. * @param {Number} courseid Course ID the module belongs to. + * @param {Boolean} single True if we're downloading a single module, false if we're downloading a whole section. * @return {Promise} Promise with the size. */ - self.getModuleDownloadSize = function(module, courseid) { + self.getModuleDownloadSize = function(module, courseid, single) { var downloadSize, handler = enabledHandlers[module.modname]; @@ -649,7 +651,7 @@ angular.module('mm.core') if (handler) { return self.isModuleDownloadable(module, courseid).then(function(downloadable) { if (!downloadable) { - return; + return {size: 0, total: true}; } downloadSize = statusCache.getValue(handler.component, module.id, 'downloadSize'); @@ -657,15 +659,19 @@ angular.module('mm.core') return downloadSize; } - return $q.when(handler.getDownloadSize(module, courseid)).then(function(size) { + return $q.when(handler.getDownloadSize(module, courseid, single)).then(function(size) { return statusCache.setValue(handler.component, module.id, 'downloadSize', size); - }).catch(function() { - return statusCache.getValue(handler.component, module.id, 'downloadSize', true); + }).catch(function(error) { + var cachedSize = statusCache.getValue(handler.component, module.id, 'downloadSize', true); + if (cachedSize) { + return cachedSize; + } + return $q.reject(error); }); }); } - return $q.when(0); + return $q.when({size: 0, total: false}); }; /** diff --git a/www/core/components/course/templates/sections.html b/www/core/components/course/templates/sections.html index abb0829c921..4834d7aa702 100644 --- a/www/core/components/course/templates/sections.html +++ b/www/core/components/course/templates/sections.html @@ -3,6 +3,7 @@ + diff --git a/www/core/components/courses/controllers/list.js b/www/core/components/courses/controllers/list.js index a2cdb6b5c69..6fe0272e40e 100644 --- a/www/core/components/courses/controllers/list.js +++ b/www/core/components/courses/controllers/list.js @@ -22,13 +22,16 @@ angular.module('mm.core.courses') * @name mmCoursesListCtrl */ .controller('mmCoursesListCtrl', function($scope, $mmCourses, $mmCoursesDelegate, $mmUtil, $mmEvents, $mmSite, $q, - mmCoursesEventMyCoursesUpdated, mmCoreEventSiteUpdated) { + mmCoursesEventMyCoursesUpdated, mmCoreEventSiteUpdated, $mmCourseHelper) { var updateSiteObserver, - myCoursesObserver; + myCoursesObserver, + prefetchIconInitialized = false; $scope.searchEnabled = $mmCourses.isSearchCoursesAvailable() && !$mmCourses.isSearchCoursesDisabledInSite(); $scope.filter = {}; + $scope.prefetchCoursesData = {}; + $scope.showFilter = false; // Convenience function to fetch courses. function fetchCourses(refresh) { @@ -46,12 +49,39 @@ angular.module('mm.core.courses') course.admOptions = options.admOptions[course.id]; }); $scope.courses = courses; + + initPrefetchCoursesIcon(); }); }, function(error) { $mmUtil.showErrorModalDefault(error, 'mm.courses.errorloadcourses', true); }); } + // Initialize the prefetch icon for the list of courses. + function initPrefetchCoursesIcon() { + if (prefetchIconInitialized) { + // Already initialized. + return; + } + + prefetchIconInitialized = true; + + if (!$scope.courses || $scope.courses.length < 2) { + // Not enough courses. + $scope.prefetchCoursesData.icon = ''; + return; + } + + $mmCourseHelper.determineCoursesStatus($scope.courses).then(function(status) { + var icon = $mmCourseHelper.getCourseStatusIconFromStatus(status); + if (icon == 'spinner') { + // It seems all courses are being downloaded, show a download button instead. + icon = 'ion-ios-cloud-download-outline'; + } + $scope.prefetchCoursesData.icon = icon; + }); + } + fetchCourses().finally(function() { $scope.coursesLoaded = true; }); @@ -64,12 +94,38 @@ angular.module('mm.core.courses') $q.all(promises).finally(function() { + prefetchIconInitialized = false; fetchCourses(true).finally(function() { $scope.$broadcast('scroll.refreshComplete'); }); }); }; + $scope.switchFilter = function() { + $scope.filter.filterText = ''; + $scope.showFilter = !$scope.showFilter; + }; + + // Download all the courses. + $scope.downloadCourses = function() { + var initialIcon = $scope.prefetchCoursesData.icon; + + $scope.prefetchCoursesData.icon = 'spinner'; + $scope.prefetchCoursesData.badge = ''; + return $mmCourseHelper.confirmAndPrefetchCourses($scope.courses).then(function(downloaded) { + $scope.prefetchCoursesData.icon = downloaded ? 'ion-android-refresh' : initialIcon; + }, function(error) { + if (!$scope.$$destroyed) { + $mmUtil.showErrorModalDefault(error, 'mm.course.errordownloadingcourse', true); + $scope.prefetchCoursesData.icon = initialIcon; + } + }, function(progress) { + $scope.prefetchCoursesData.badge = progress.count + ' / ' + progress.total; + }).finally(function() { + $scope.prefetchCoursesData.badge = ''; + }); + }; + myCoursesObserver = $mmEvents.on(mmCoursesEventMyCoursesUpdated, function(siteid) { if (siteid == $mmSite.getId()) { fetchCourses(); diff --git a/www/core/components/courses/controllers/viewresult.js b/www/core/components/courses/controllers/viewresult.js index 168650ebc96..ac3d2705f99 100644 --- a/www/core/components/courses/controllers/viewresult.js +++ b/www/core/components/courses/controllers/viewresult.js @@ -23,7 +23,7 @@ angular.module('mm.core.courses') */ .controller('mmCoursesViewResultCtrl', function($scope, $stateParams, $mmCourses, $mmCoursesDelegate, $mmUtil, $translate, $q, $ionicModal, $mmEvents, $mmSite, mmCoursesSearchComponent, mmCoursesEnrolInvalidKey, mmCoursesEventMyCoursesUpdated, - $timeout, $mmFS, $rootScope, $mmApp, $ionicPlatform) { + $timeout, $mmFS, $rootScope, $mmApp, $ionicPlatform, $mmCourseHelper, $mmCourse, mmCoreEventCourseStatusChanged) { var course = angular.copy($stateParams.course || {}), // Copy the object to prevent modifying the one from the previous view. selfEnrolWSAvailable = $mmCourses.isSelfEnrolmentEnabled(), @@ -38,7 +38,8 @@ angular.module('mm.core.courses') inAppLoadListener, inAppFinishListener, inAppExitListener, - appResumeListener; + appResumeListener, + obsStatus; $scope.course = course; $scope.component = mmCoursesSearchComponent; @@ -163,7 +164,29 @@ angular.module('mm.core.courses') }); } - getCourse(); + getCourse().finally(function() { + // Determine course prefetch icon. + $scope.prefetchCourseIcon = 'spinner'; + $mmCourseHelper.getCourseStatusIcon(course.id).then(function(icon) { + $scope.prefetchCourseIcon = icon; + + if (icon == 'spinner') { + // Course is being downloaded. Get the download promise. + var promise = $mmCourseHelper.getCourseDownloadPromise(course.id); + if (promise) { + // There is a download promise. If it fails, show an error. + promise.catch(function(error) { + if (!scope.$$destroyed) { + $mmUtil.showErrorModalDefault(error, 'mm.course.errordownloadingcourse', true); + } + }); + } else { + // No download, this probably means that the app was closed while downloading. Set previous status. + $mmCourse.setCoursePreviousStatus(courseId); + } + } + }); + }); $scope.doRefresh = function() { refreshData().finally(function() { @@ -171,6 +194,17 @@ angular.module('mm.core.courses') }); }; + $scope.prefetchCourse = function() { + $mmCourseHelper.confirmAndPrefetchCourse($scope, course, undefined, course._handlers); + }; + + // Listen for status change in course. + obsStatus = $mmEvents.on(mmCoreEventCourseStatusChanged, function(data) { + if (data.siteId == $mmSite.getId() && data.courseId == course.id) { + $scope.prefetchCourseIcon = $mmCourseHelper.getCourseStatusIconFromStatus(data.status); + } + }); + if (selfEnrolWSAvailable && course.enrollmentmethods && course.enrollmentmethods.indexOf('self') > -1) { // Setup password modal for self-enrolment. $ionicModal.fromTemplateUrl('core/components/courses/templates/password-modal.html', { @@ -325,4 +359,8 @@ angular.module('mm.core.courses') } }; } + + $scope.$on('$destroy', function() { + obsStatus && obsStatus.off && obsStatus.off(); + }); }); diff --git a/www/core/components/courses/directives/courselistprogress.js b/www/core/components/courses/directives/courselistprogress.js index 9af7c5f70e5..b43192f6731 100644 --- a/www/core/components/courses/directives/courselistprogress.js +++ b/www/core/components/courses/directives/courselistprogress.js @@ -30,27 +30,8 @@ angular.module('mm.core.courses') * * */ -.directive('mmCourseListProgress', function($ionicActionSheet, $mmCoursesDelegate, $translate, $controller, $q) { - - /** - * Check if the actions button should be shown. - * - * @param {Object} scope Directive's scope. - * @param {Boolean} refresh Whether to refresh the list of handlers. - * @return {Promise} Promise resolved when done. - */ - function shouldShowActions(scope, refresh) { - scope.loaded = false; - - return $mmCoursesDelegate.getNavHandlersForCourse(scope.course, refresh, true).then(function(handlers) { - scope.showActions = !!handlers.length; - }).catch(function(error) { - scope.showActions = false; - return $q.reject(error); - }).finally(function() { - scope.loaded = true; - }); - } +.directive('mmCourseListProgress', function($ionicActionSheet, $mmCoursesDelegate, $translate, $controller, $q, $mmCourseHelper, + $mmUtil, $mmCourse, $mmEvents, $mmSite, mmCoreEventCourseStatusChanged) { return { restrict: 'E', @@ -62,17 +43,66 @@ angular.module('mm.core.courses') showSummary: "=?" }, link: function(scope) { - var buttons; + var buttons, + obsStatus, + downloadText = $translate.instant('mm.course.downloadcourse'), + downloadingText = $translate.instant('mm.core.downloading'), + downloadButton = { + isDownload: true, + className: 'mm-download-course', + priority: 1000 + }; - shouldShowActions(scope, false); + // Always show options, since the download course option will always be there. + scope.actionsLoaded = true; + + // Determine course prefetch icon. + $mmCourseHelper.getCourseStatusIcon(scope.course.id).then(function(icon) { + scope.prefetchCourseIcon = icon; + + if (icon == 'spinner') { + downloadButton.text = downloadingText; + + // Course is being downloaded. Get the download promise. + var promise = $mmCourseHelper.getCourseDownloadPromise(scope.course.id); + if (promise) { + // There is a download promise. If it fails, show an error. + promise.catch(function(error) { + if (!scope.$$destroyed) { + $mmUtil.showErrorModalDefault(error, 'mm.course.errordownloadingcourse', true); + } + }); + } else { + // No download, this probably means that the app was closed while downloading. Set previous status. + $mmCourse.setCoursePreviousStatus(scope.course.id); + } + } else { + downloadButton.text = '' + downloadText; + } + }); + + // Listen for status change in course. + obsStatus = $mmEvents.on(mmCoreEventCourseStatusChanged, function(data) { + if (data.siteId == $mmSite.getId() && data.courseId == scope.course.id) { + var icon = $mmCourseHelper.getCourseStatusIconFromStatus(data.status); + scope.prefetchCourseIcon = icon; + + if (icon == 'spinner') { + downloadButton.text = downloadingText; + } else { + downloadButton.text = '' + downloadText; + } + } + }); scope.showCourseActions = function($event) { $event.preventDefault(); $event.stopPropagation(); // Get the list of handlers to display. - scope.loaded = false; + scope.actionsLoaded = false; $mmCoursesDelegate.getNavHandlersToDisplay(scope.course, false, false, true).then(function(handlers) { + buttons = handlers.map(function(handler) { var newScope = scope.$new(); $controller(handler.controller, {$scope: newScope}); @@ -88,22 +118,31 @@ angular.module('mm.core.courses') newScope.$destroy(); return buttonInfo; - }).sort(function(a, b) { + }); + + // Add the download button. + buttons.unshift(downloadButton); + + // Sort the buttons. + buttons = buttons.sort(function(a, b) { return b.priority - a.priority; }); - }).finally(function() { + }).then(function() { // We have the list of buttons to show, show the action sheet. - scope.loaded = true; - $ionicActionSheet.show({ titleText: scope.course.fullname, buttons: buttons, cancelText: $translate.instant('mm.core.cancel'), buttonClicked: function(index) { - if (angular.isFunction(buttons[index].action)) { + if (buttons[index].isDownload) { + // Download button. + $mmCourseHelper.confirmAndPrefetchCourse(scope, scope.course); + return true; + } else if (angular.isFunction(buttons[index].action)) { // Execute the action and close the action sheet. return buttons[index].action($event, scope.course); } + // Never close the action sheet. It will automatically be closed if success. return false; }, @@ -112,8 +151,16 @@ angular.module('mm.core.courses') return true; } }); + }).catch(function(error) { + $mmUtil.showErrorModalDefault(error, 'Error loading options'); + }).finally(function() { + scope.actionsLoaded = true; }); }; + + scope.$on('$destroy', function() { + obsStatus && obsStatus.off && obsStatus.off(); + }); } }; }); diff --git a/www/core/components/courses/lang/en.json b/www/core/components/courses/lang/en.json index 83ac75364b1..f04f3836df8 100644 --- a/www/core/components/courses/lang/en.json +++ b/www/core/components/courses/lang/en.json @@ -5,6 +5,7 @@ "categories": "Course categories", "confirmselfenrol": "Are you sure you want to enrol yourself in this course?", "courses": "Courses", + "downloadcourses": "Download courses", "enrolme": "Enrol me", "errorloadcategories": "An error occurred while loading categories.", "errorloadcourses": "An error occurred while loading courses.", diff --git a/www/core/components/courses/scss/styles.scss b/www/core/components/courses/scss/styles.scss index a562d1b726f..9ff5e7a52c2 100644 --- a/www/core/components/courses/scss/styles.scss +++ b/www/core/components/courses/scss/styles.scss @@ -15,6 +15,18 @@ $doughnut-text-colour: $gray-darker; color: $doughnut-text-colour; position: absolute; } + + &.item-progress .mm-course-download-spinner { + top: 46px; + } + + &:not(.item-progress) { + padding-right: 80px; + + .mm-course-download-spinner { + right: 40px; + } + } } .item-progress { diff --git a/www/core/components/courses/services/delegate.js b/www/core/components/courses/services/delegate.js index 2d58e872fcd..2b25abe7634 100644 --- a/www/core/components/courses/services/delegate.js +++ b/www/core/components/courses/services/delegate.js @@ -50,6 +50,8 @@ angular.module('mm.core.courses') * for the list of scope variables expected. * - invalidateEnabledForCourse(courseId, navOptions, admOptions) (Promise) Optional. Should * invalidate data to determine if handler is enabled for a certain course. + * - prefetch(course) (Promise) Optional. Will be called when a course is downloaded, and it + * should prefetch all the data to be able to see the addon in offline. */ self.registerNavHandler = function(addon, handler, priority) { if (typeof navHandlers[addon] !== 'undefined') { @@ -305,7 +307,8 @@ angular.module('mm.core.courses') if (enabled) { handlersToDisplay.push({ controller: handler.instance.getController(course.id), - priority: handler.priority + priority: handler.priority, + prefetch: handler.instance.prefetch }); } })); diff --git a/www/core/components/courses/templates/courselistprogress.html b/www/core/components/courses/templates/courselistprogress.html index f6924d020af..cbe47348fb7 100644 --- a/www/core/components/courses/templates/courselistprogress.html +++ b/www/core/components/courses/templates/courselistprogress.html @@ -18,8 +18,11 @@

{{course.fullname}}

- - + + + + +

diff --git a/www/core/components/courses/templates/list.html b/www/core/components/courses/templates/list.html index b8e736c1828..ba9e42dd1f8 100644 --- a/www/core/components/courses/templates/list.html +++ b/www/core/components/courses/templates/list.html @@ -1,13 +1,17 @@ + + + + -

+
diff --git a/www/core/components/courses/templates/viewresult.html b/www/core/components/courses/templates/viewresult.html index a0b088f008d..28efc546c19 100644 --- a/www/core/components/courses/templates/viewresult.html +++ b/www/core/components/courses/templates/viewresult.html @@ -40,6 +40,11 @@

{{course.fullname}}

{{ 'mm.courses.notenrollable' | translate }}

+ + + +

{{ 'mm.course.downloadcourse' | translate }}

+

{{ 'mm.course.contents' | translate }}

diff --git a/www/core/components/grades/services/grades.js b/www/core/components/grades/services/grades.js index cc4e50c8a8e..09eeaa5603b 100644 --- a/www/core/components/grades/services/grades.js +++ b/www/core/components/grades/services/grades.js @@ -78,12 +78,13 @@ angular.module('mm.core.grades') * @ngdoc method * @name $mmGrades#invalidateGradesTableData * @param {Number} courseId Course ID. - * @param {Number} userId User ID. + * @param {Number} [userId] User ID (empty for current user in the site). * @param {Number} [siteId] Site id (empty for current site). * @return {Promise} Promise resolved when the data is invalidated. */ self.invalidateGradesTableData = function(courseId, userId, siteId) { return $mmSitesManager.getSite(siteId).then(function(site) { + userId = userId || site.getUserId(); return site.invalidateWsCacheForKey(getGradesTableCacheKey(courseId, userId)); }); }; diff --git a/www/core/components/settings/controllers/space-usage.js b/www/core/components/settings/controllers/space-usage.js index fbb1fa336e3..09b1a83a000 100644 --- a/www/core/components/settings/controllers/space-usage.js +++ b/www/core/components/settings/controllers/space-usage.js @@ -23,7 +23,7 @@ angular.module('mm.core.settings') * @todo When "mock site" is implemented we should have functions to calculate the site usage and delete its files. */ .controller('mmSettingsSpaceUsageCtrl', function($log, $scope, $mmSitesManager, $mmFS, $q, $mmUtil, $translate, $mmSite, - $mmText, $mmFilepool) { + $mmText, $mmFilepool, $mmCourse) { $log = $log.getInstance('mmSettingsSpaceUsageCtrl'); $scope.currentSiteId = $mmSite.getId(); @@ -110,6 +110,7 @@ angular.module('mm.core.settings') return $mmSitesManager.getSite(siteid); }).then(function(site) { return site.deleteFolder().then(function() { + $mmCourse.clearAllCoursesStatus(siteid); $mmFilepool.clearAllPackagesStatus(siteid); $mmFilepool.clearFilepool(siteid); updateSiteUsage(siteData, 0); diff --git a/www/core/directives/contextmenu.js b/www/core/directives/contextmenu.js index 4ae6abfbadd..a9a7cbdc002 100644 --- a/www/core/directives/contextmenu.js +++ b/www/core/directives/contextmenu.js @@ -189,6 +189,8 @@ angular.module('mm.core') * @param {Boolean} [closeOnClick=true] If close the popover when clicked. Only works if action or href is provided. * @param {Boolean} [closeWhenDone=false] Close popover when action is done. Only if action is supplied and closeOnClick=false. * @param {Number} [priority] Used to sort items. The highest priority, the highest position. + * @param {String} [badge] A badge to show in the item. + * @param {String} [badgeClass] A class to set in the badge. */ .directive('mmContextMenuItem', function($mmUtil, $timeout, $ionicPlatform) { @@ -245,7 +247,9 @@ angular.module('mm.core') closeOnClick: '=?', closeWhenDone: '=?', priority: '=?', - ngShow: '=?' + ngShow: '=?', + badge: '=?', + badgeClass: '=?' }, link: function(scope, element, attrs, CtxtMenuCtrl) { // Initialize values. Change the name of some of them to prevent being reconverted to string. diff --git a/www/core/lang/en.json b/www/core/lang/en.json index 1133484ca80..a04acb91ed1 100644 --- a/www/core/lang/en.json +++ b/www/core/lang/en.json @@ -103,6 +103,8 @@ "lastdownloaded": "Last downloaded", "lastmodified": "Last modified", "lastsync": "Last synchronisation", + "layoutgrid": "Grid", + "list": "List", "listsep": ",", "loading": "Loading", "loadmore": "Load more", diff --git a/www/core/lib/events.js b/www/core/lib/events.js index 38a8c590d69..09b55755605 100644 --- a/www/core/lib/events.js +++ b/www/core/lib/events.js @@ -31,6 +31,7 @@ angular.module('mm.core') .constant('mmCoreEventCompletionModuleViewed', 'completion_module_viewed') .constant('mmCoreEventUserDeleted', 'user_deleted') .constant('mmCoreEventPackageStatusChanged', 'filepool_package_status_changed') +.constant('mmCoreEventCourseStatusChanged', 'course_status_changed') .constant('mmCoreEventSectionStatusChanged', 'section_status_changed') .constant('mmCoreEventRemoteAddonsLoaded', 'remote_addons_loaded') .constant('mmCoreEventOnline', 'online') // Deprecated on version 3.1.3. diff --git a/www/core/lib/util.js b/www/core/lib/util.js index 02b4cfa04d3..6f7cfb720a3 100644 --- a/www/core/lib/util.js +++ b/www/core/lib/util.js @@ -1416,9 +1416,10 @@ angular.module('mm.core') * Default: 'mm.course.confirmdownloadunknownsize'. * @param {Number} [wifiThreshold] Threshold to show confirm in WiFi connection. Default: mmCoreWifiDownloadThreshold. * @param {Number} [limitedThreshold] Threshold to show confirm in limited connection. Default: mmCoreDownloadThreshold. + * @param {Boolean} [alwaysConfirm] True to show a confirm even if the size isn't high, false otherwise. * @return {Promise} Promise resolved when the user confirms or if no confirm needed. */ - self.confirmDownloadSize = function(sizeCalc, message, unknownsizemessage, wifiThreshold, limitedThreshold) { + self.confirmDownloadSize = function(sizeCalc, message, unknownsizemessage, wifiThreshold, limitedThreshold, alwaysConfirm) { wifiThreshold = typeof wifiThreshold == 'undefined' ? mmCoreWifiDownloadThreshold : wifiThreshold; limitedThreshold = typeof limitedThreshold == 'undefined' ? mmCoreDownloadThreshold : limitedThreshold; @@ -1439,6 +1440,8 @@ angular.module('mm.core') message = message || 'mm.course.confirmdownload'; var readableSize = $mmText.bytesToSize(sizeCalc.size, 2); return self.showConfirm($translate(message, {size: readableSize})); + } else if (alwaysConfirm) { + return self.showConfirm($translate('mm.core.areyousure')); } return $q.when(); }; diff --git a/www/core/templates/contextmenu.html b/www/core/templates/contextmenu.html index 6f709052bc7..25e816338a7 100644 --- a/www/core/templates/contextmenu.html +++ b/www/core/templates/contextmenu.html @@ -11,6 +11,7 @@ + {{item.badge}}
diff --git a/www/core/templates/contextmenuicon.html b/www/core/templates/contextmenuicon.html index 9f0cf5fde5f..86e96262cb2 100644 --- a/www/core/templates/contextmenuicon.html +++ b/www/core/templates/contextmenuicon.html @@ -1,2 +1,2 @@ - +
\ No newline at end of file