\ No newline at end of file
diff --git a/src/components/tabs/tabs.ios.scss b/src/components/tabs/tabs.ios.scss
new file mode 100644
index 000000000..b9134b986
--- /dev/null
+++ b/src/components/tabs/tabs.ios.scss
@@ -0,0 +1,11 @@
+core-tabs {
+ .core-tabs-bar {
+ -webkit-box-pack: center;
+ -webkit-justify-content: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+ > a {
+ font-size: 1.6rem;
+ }
+ }
+}
diff --git a/src/components/tabs/tabs.scss b/src/components/tabs/tabs.scss
new file mode 100644
index 000000000..b7daa7804
--- /dev/null
+++ b/src/components/tabs/tabs.scss
@@ -0,0 +1,32 @@
+core-tabs {
+ .core-tabs-bar {
+ @include position(null, null, 0, 0);
+
+ z-index: $z-index-toolbar;
+ display: flex;
+ width: 100%;
+ background: $core-top-tabs-background;
+
+ > a {
+ @extend .tab-button;
+
+ background: $core-top-tabs-background;
+ color: $core-top-tabs-color !important;
+ border-bottom: 1px solid $core-top-tabs-border;
+ font-size: 1.6rem;
+
+ &[aria-selected=true] {
+ color: $core-top-tabs-color-active !important;
+ border-bottom: 2px solid $core-top-tabs-color-active;
+ }
+ }
+ }
+
+ core-tab {
+ display: none;
+
+ &.selected {
+ display: block;
+ }
+ }
+}
diff --git a/src/components/tabs/tabs.ts b/src/components/tabs/tabs.ts
new file mode 100644
index 000000000..93ab502fa
--- /dev/null
+++ b/src/components/tabs/tabs.ts
@@ -0,0 +1,183 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, Input, Output, EventEmitter, OnInit, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
+import { CoreTabComponent } from './tab';
+
+/**
+ * This component displays some tabs that usually share data between them.
+ *
+ * If your tabs don't share any data then you should probably use ion-tabs. This component doesn't use different ion-nav
+ * for each tab, so it will not load pages.
+ *
+ * Example usage:
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * Obviously, the tab contents will only be shown if that tab is selected.
+ */
+@Component({
+ selector: 'core-tabs',
+ templateUrl: 'tabs.html'
+})
+export class CoreTabsComponent implements OnInit, AfterViewInit {
+ @Input() selectedIndex?: number = 0; // Index of the tab to select.
+ @Output() ionChange: EventEmitter = new EventEmitter(); // Emitted when the tab changes.
+ @ViewChild('originalTabs') originalTabsRef: ElementRef;
+
+ tabs: CoreTabComponent[] = []; // List of tabs.
+ selected: number; // Selected tab number.
+ protected originalTabsContainer: HTMLElement; // The container of the original tabs. It will include each tab's content.
+
+ constructor() {}
+
+ /**
+ * Component being initialized.
+ */
+ ngOnInit() {
+ this.originalTabsContainer = this.originalTabsRef.nativeElement;
+ }
+
+ /**
+ * View has been initialized.
+ */
+ ngAfterViewInit() {
+ let selectedIndex = this.selectedIndex || 0,
+ selectedTab = this.tabs[selectedIndex];
+
+ if (!selectedTab.enabled || !selectedTab.show) {
+ // The tab is not enabled or not shown. Get the first tab that is enabled.
+ selectedTab = this.tabs.find((tab, index) => {
+ if (tab.enabled && tab.show) {
+ selectedIndex = index;
+ return true;
+ }
+ return false;
+ });
+ }
+
+ if (selectedTab) {
+ this.selectTab(selectedIndex);
+ }
+ }
+
+ /**
+ * Add a new tab if it isn't already in the list of tabs.
+ *
+ * @param {CoreTabComponent} tab The tab to add.
+ */
+ addTab(tab: CoreTabComponent) : void {
+ // Check if tab is already in the list.
+ if (this.getIndex(tab) == -1) {
+ this.tabs.push(tab);
+ this.sortTabs();
+ }
+ }
+
+ /**
+ * Get the index of tab.
+ *
+ * @param {any} tab [description]
+ * @return {number} [description]
+ */
+ getIndex(tab: any) : number {
+ for (let i = 0; i < this.tabs.length; i++) {
+ let t = this.tabs[i];
+ if (t === tab || (typeof t.id != 'undefined' && t.id === tab.id)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Get the current selected tab.
+ *
+ * @return {CoreTabComponent} Selected tab.
+ */
+ getSelected() : CoreTabComponent {
+ return this.tabs[this.selected];
+ }
+
+ /**
+ * Remove a tab from the list of tabs.
+ *
+ * @param {CoreTabComponent} tab The tab to remove.
+ */
+ removeTab(tab: CoreTabComponent) : void {
+ const index = this.getIndex(tab);
+ this.tabs.splice(index, 1);
+ }
+
+ /**
+ * Select a certain tab.
+ *
+ * @param {number} index The index of the tab to select.
+ */
+ selectTab(index: number) : void {
+ if (index == this.selected) {
+ // Already selected.
+ return;
+ }
+
+ if (index < 0 || index >= this.tabs.length) {
+ // Index isn't valid, select the first one.
+ index = 0;
+ }
+
+ const currenTab = this.getSelected(),
+ newTab = this.tabs[index];
+
+ if (!newTab.enabled || !newTab.show) {
+ // The tab isn't enabled or shown, stop.
+ return;
+ }
+
+ if (currenTab) {
+ // Unselect previous selected tab.
+ currenTab.element.classList.remove('selected');
+ }
+
+ this.selected = index;
+ newTab.element.classList.add('selected');
+ newTab.ionSelect.emit(newTab);
+ this.ionChange.emit(newTab);
+ }
+
+ /**
+ * Sort the tabs, keeping the same order as in the original list.
+ */
+ protected sortTabs() {
+ if (this.originalTabsContainer) {
+ let newTabs = [],
+ newSelected;
+
+ this.tabs.forEach((tab, index) => {
+ let originalIndex = Array.prototype.indexOf.call(this.originalTabsContainer.children, tab.element);
+ if (originalIndex != -1) {
+ newTabs[originalIndex] = tab;
+ if (this.selected == index) {
+ newSelected = originalIndex;
+ }
+ }
+ });
+
+ this.tabs = newTabs;
+ }
+ }
+}
diff --git a/src/core/constants.ts b/src/core/constants.ts
index 08b8d83e8..519460a68 100644
--- a/src/core/constants.ts
+++ b/src/core/constants.ts
@@ -16,33 +16,34 @@
* Static class to contain all the core constants.
*/
export class CoreConstants {
- public static secondsYear = 31536000;
- public static secondsDay = 86400;
- public static secondsHour = 3600;
- public static secondsMinute = 60;
- public static wifiDownloadThreshold = 104857600; // 100MB.
- public static downloadThreshold = 10485760; // 10MB.
- public static dontShowError = 'CoreDontShowError';
- public static noSiteId = 'NoSite';
+ public static SECONDS_YEAR = 31536000;
+ public static SECONDS_WEEK = 604800;
+ public static SECONDS_DAY = 86400;
+ public static SECONDS_HOUR = 3600;
+ public static SECONDS_MINUTE = 60;
+ public static WIFI_DOWNLOAD_THRESHOLD = 104857600; // 100MB.
+ public static DOWNLOAD_THRESHOLD = 10485760; // 10MB.
+ public static DONT_SHOW_ERROR = 'CoreDontShowError';
+ public static NO_SITE_ID = 'NoSite';
// Settings constants.
- public static settingsRichTextEditor = 'CoreSettingsRichTextEditor';
- public static settingsNotificationSound = 'CoreSettingsNotificationSound';
- public static settingsSyncOnlyOnWifi = 'mmCoreSyncOnlyOnWifi';
+ public static SETTINGS_RICH_TEXT_EDITOR = 'CoreSettingsRichTextEditor';
+ public static SETTINGS_NOTIFICATION_SOUND = 'CoreSettingsNotificationSound';
+ public static SETTINGS_SYNC_ONLY_ON_WIFI = 'CoreSettingsSyncOnlyOnWifi';
// WS constants.
- public static wsTimeout = 30000;
- public static wsPrefix = 'local_mobile_';
+ public static WS_TIMEOUT = 30000;
+ public static WS_PREFIX = 'local_mobile_';
// Login constants.
- public static loginSSOCode = 2; // SSO in browser window is required.
- public static loginSSOInAppCode = 3; // SSO in embedded browser is required.
- public static loginLaunchData = 'mmLoginLaunchData';
+ public static LOGIN_SSO_CODE = 2; // SSO in browser window is required.
+ public static LOGIN_SSO_INAPP_CODE = 3; // SSO in embedded browser is required.
+ public static LOGIN_LAUNCH_DATA = 'CoreLoginLaunchData';
// Download status constants.
- public static downloaded = 'downloaded';
- public static downloading = 'downloading';
- public static notDownloaded = 'notdownloaded';
- public static outdated = 'outdated';
- public static notDownloadable = 'notdownloadable';
+ public static DOWNLOADED = 'downloaded';
+ public static DOWNLOADING = 'downloading';
+ public static NOT_DOWNLOADED = 'notdownloaded';
+ public static OUTDATED = 'outdated';
+ public static NOT_DOWNLOADABLE = 'notdownloadable';
}
diff --git a/src/core/course/classes/module-prefetch-handler.ts b/src/core/course/classes/module-prefetch-handler.ts
new file mode 100644
index 000000000..392296bff
--- /dev/null
+++ b/src/core/course/classes/module-prefetch-handler.ts
@@ -0,0 +1,514 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injector } from '@angular/core';
+import { TranslateService } from '@ngx-translate/core';
+import { CoreAppProvider } from '../../../providers/app';
+import { CoreFilepoolProvider } from '../../../providers/filepool';
+import { CoreSitesProvider } from '../../../providers/sites';
+import { CoreDomUtilsProvider } from '../../../providers/utils/dom';
+import { CoreUtilsProvider } from '../../../providers/utils/utils';
+import { CoreCourseProvider } from '../providers/course';
+import { CoreCourseModulePrefetchHandler } from '../providers/module-prefetch-delegate';
+import { CoreConstants } from '../../constants';
+
+/**
+ * A prefetch function to be passed to prefetchPackage.
+ * This function should NOT call storePackageStatus, downloadPackage or prefetchPakage from filepool.
+ * It receives the same params as prefetchPackage except the function itself. This includes all extra parameters sent after siteId.
+ * The string returned by this function will be stored as "extra" data in the filepool package. If you don't need to store
+ * extra data, don't return anything.
+ *
+ * @param {any} module Module.
+ * @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.
+ * @param {string} siteId Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when the prefetch finishes. The string returned will be stored as "extra" data in the
+ * filepool package. If you don't need to store extra data, don't return anything.
+ */
+export type prefetchFunction = (module: any, courseId: number, single: boolean, siteId: string, ...args) => Promise;
+
+/**
+ * Base prefetch handler to be registered in CoreCourseModulePrefetchDelegate. It is useful to minimize the amount of
+ * functions that handlers need to implement. It also provides some helper features like preventing a module to be
+ * downloaded twice at the same time.
+ *
+ * If your handler inherits from this service, you just need to override the functions that you want to change.
+ *
+ * The implementation of this default handler is aimed for resources that only need to prefetch files, not WebService calls.
+ *
+ * By default, prefetching a module will only download its files (downloadOrPrefetch). This might be enough for resources.
+ * If you need to prefetch WebServices, then you need to override the "download" and "prefetch" functions. In this case, it's
+ * recommended to call the prefetchPackage function since it'll handle changing the status of the module.
+ */
+export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePrefetchHandler {
+ /**
+ * A name to identify the addon.
+ * @type {string}
+ */
+ name = 'CoreCourseModulePrefetchHandlerBase';
+
+ /**
+ * Name of the module. It should match the "modname" of the module returned in core_course_get_contents.
+ * @type {string}
+ */
+ modname = '';
+
+ /**
+ * The handler's component.
+ * @type {string}
+ */
+ component = 'core_module';
+
+ /**
+ * The RegExp to check updates. If a module has an update whose name matches this RegExp, the module will be marked
+ * as outdated. This RegExp is ignored if hasUpdates function is defined.
+ * @type {RegExp}
+ */
+ updatesNames = /^.*files$/;
+
+ /**
+ * Whether the module is a resource (true) or an activity (false).
+ * @type {boolean}
+ */
+ isResource: boolean;
+
+ /**
+ * List of download promises to prevent downloading the module twice at the same time.
+ * @type {{[s: string]: {[s: string]: Promise}}}
+ */
+ protected downloadPromises: {[s: string]: {[s: string]: Promise}} = {};
+
+ // List of services that will be injected using injector. It's done like this so subclasses don't have to send all the
+ // services to the parent in the constructor.
+ protected translate: TranslateService;
+ protected appProvider: CoreAppProvider;
+ protected courseProvider: CoreCourseProvider;
+ protected filepoolProvider: CoreFilepoolProvider;
+ protected sitesProvider: CoreSitesProvider;
+ protected domUtils: CoreDomUtilsProvider;
+ protected utils: CoreUtilsProvider;
+
+ constructor(injector: Injector) {
+ this.translate = injector.get(TranslateService);
+ this.appProvider = injector.get(CoreAppProvider);
+ this.courseProvider = injector.get(CoreCourseProvider);
+ this.filepoolProvider = injector.get(CoreFilepoolProvider);
+ this.sitesProvider = injector.get(CoreSitesProvider);
+ this.domUtils = injector.get(CoreDomUtilsProvider);
+ this.utils = injector.get(CoreUtilsProvider);
+ }
+
+ /**
+ * Add an ongoing download to the downloadPromises list. When the promise finishes it will be removed.
+ *
+ * @param {number} id Unique identifier per component.
+ * @param {Promise} promise Promise to add.
+ * @param {String} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise of the current download.
+ */
+ addOngoingDownload(id: number, promise: Promise, siteId?: string) : Promise {
+ siteId = siteId || this.sitesProvider.getCurrentSiteId();
+
+ const uniqueId = this.getUniqueId(id);
+
+ if (!this.downloadPromises[siteId]) {
+ this.downloadPromises[siteId] = {};
+ }
+
+ this.downloadPromises[siteId][uniqueId] = promise.finally(() => {
+ delete this.downloadPromises[siteId][uniqueId];
+ });
+
+ return this.downloadPromises[siteId][uniqueId];
+ }
+
+ /**
+ * Download the module.
+ *
+ * @param {any} module The module object returned by WS.
+ * @param {number} courseId Course ID.
+ * @return {Promise} Promise resolved when all content is downloaded.
+ */
+ download(module: any, courseId: number) : Promise {
+ return this.downloadOrPrefetch(module, courseId, false);
+ }
+
+ /**
+ * Download or prefetch the content.
+ *
+ * @param {any} module The module object returned by WS.
+ * @param {number} courseId Course ID.
+ * @param {boolean} [prefetch] True to prefetch, false to download right away.
+ * @param {string} [dirPath] Path of the directory where to store all the content files. This is to keep the files
+ * relative paths and make the package work in an iframe. Undefined to download the files
+ * in the filepool root folder.
+ * @return {Promise} Promise resolved when all content is downloaded. Data returned is not reliable.
+ */
+ downloadOrPrefetch(module: any, courseId: number, prefetch?: boolean, dirPath?: string) : Promise {
+ if (!this.appProvider.isOnline()) {
+ // Cannot download in offline.
+ return Promise.reject(this.translate.instant('core.networkerrormsg'));
+ }
+
+ const siteId = this.sitesProvider.getCurrentSiteId();
+
+ // Load module contents (ignore cache so we always have the latest data).
+ return this.loadContents(module, courseId, true).then(() => {
+ // Get the intro files.
+ return this.getIntroFiles(module, courseId);
+ }).then((introFiles) => {
+ let downloadFn = prefetch ? this.filepoolProvider.prefetchPackage.bind(this.filepoolProvider) :
+ this.filepoolProvider.downloadPackage.bind(this.filepoolProvider),
+ contentFiles = this.getContentDownloadableFiles(module),
+ promises = [];
+
+ if (dirPath) {
+ // Download intro files in filepool root folder.
+ promises.push(this.filepoolProvider.downloadOrPrefetchFiles(siteId, introFiles, prefetch, false,
+ this.component, module.id));
+
+ // Download content files inside dirPath.
+ promises.push(downloadFn(siteId, contentFiles, this.component, module.id, undefined, dirPath));
+ } else {
+ // No dirPath, download everything in filepool root folder.
+ let files = introFiles.concat(contentFiles);
+ promises.push(downloadFn(siteId, files, this.component, module.id));
+ }
+
+ return Promise.all(promises);
+ });
+ }
+
+ /**
+ * Returns a list of content files that can be downloaded.
+ *
+ * @param {any} module The module object returned by WS.
+ * @return {any[]} List of files.
+ */
+ getContentDownloadableFiles(module: any) {
+ let files = [];
+
+ if (module.contents && module.contents.length) {
+ module.contents.forEach((content) => {
+ if (this.isFileDownloadable(content)) {
+ files.push(content);
+ }
+ });
+ }
+
+ return files;
+ }
+
+ /**
+ * Get the download size of a module.
+ *
+ * @param {any} module Module.
+ * @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<{size: number, total: boolean}>} Promise resolved with the size and a boolean indicating if it was able
+ * to calculate the total size.
+ */
+ getDownloadSize(module: any, courseId: number, single?: boolean) : Promise<{size: number, total: boolean}> {
+ return this.getFiles(module, courseId).then((files) => {
+ return this.utils.sumFileSizes(files);
+ }).catch(() => {
+ return {size: -1, total: false};
+ });
+ }
+
+ /**
+ * Get the downloaded size of a module. If not defined, we'll use getFiles to calculate it (it can be slow).
+ *
+ * @param {any} module Module.
+ * @param {number} courseId Course ID the module belongs to.
+ * @return {number|Promise} Size, or promise resolved with the size.
+ */
+ getDownloadedSize?(module: any, courseId: number) : number|Promise {
+ const siteId = this.sitesProvider.getCurrentSiteId();
+ return this.filepoolProvider.getFilesSizeByComponent(siteId, this.component, module.id);
+ }
+
+ /**
+ * Get list of files. If not defined, we'll assume they're in module.contents.
+ *
+ * @param {any} module Module.
+ * @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 with the list of files.
+ */
+ getFiles(module: any, courseId: number, single?: boolean) : Promise {
+ // Load module contents if needed.
+ return this.loadContents(module, courseId).then(() => {
+ return this.getIntroFiles(module, courseId).then((files) => {
+ return files.concat(this.getContentDownloadableFiles(module));
+ });
+ });
+ }
+
+ /**
+ * Returns module intro files.
+ *
+ * @param {any} module The module object returned by WS.
+ * @param {number} courseId Course ID.
+ * @return {Promise} Promise resolved with list of intro files.
+ */
+ getIntroFiles(module: any, courseId: number) : Promise {
+ return Promise.resolve(this.getIntroFilesFromInstance(module));
+ }
+
+ /**
+ * Returns module intro files from instance.
+ *
+ * @param {any} module The module object returned by WS.
+ * @param {any} [instance] The instance to get the intro files (book, assign, ...). If not defined, module will be used.
+ * @return {any[]} List of intro files.
+ */
+ getIntroFilesFromInstance(module: any, instance?: any) {
+ if (instance) {
+ if (typeof instance.introfiles != 'undefined') {
+ return instance.introfiles;
+ } else if (instance.intro) {
+ return this.domUtils.extractDownloadableFilesFromHtmlAsFakeFileObjects(instance.intro);
+ }
+ }
+
+ if (module.description) {
+ return this.domUtils.extractDownloadableFilesFromHtmlAsFakeFileObjects(module.description);
+ }
+
+ return [];
+ }
+
+ /**
+ * If there's an ongoing download for a certain identifier return it.
+ *
+ * @param {number} id Unique identifier per component.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise of the current download.
+ */
+ getOngoingDownload(id: number, siteId?: string) : Promise {
+ siteId = siteId || this.sitesProvider.getCurrentSiteId();
+
+ if (this.isDownloading(id, siteId)) {
+ // There's already a download ongoing, return the promise.
+ return this.downloadPromises[siteId][this.getUniqueId(id)];
+ }
+ return Promise.resolve();
+ }
+
+ /**
+ * Create unique identifier using component and id.
+ *
+ * @param {number} id Unique ID inside component.
+ * @return {string} Unique ID.
+ */
+ getUniqueId(id: number) {
+ return this.component + '#' + id;
+ }
+
+ /**
+ * Invalidate the prefetched content.
+ *
+ * @param {number} moduleId The module ID.
+ * @return {Promise} Promise resolved when the data is invalidated.
+ */
+ invalidateContent(moduleId: number) : Promise {
+ const promises = [],
+ siteId = this.sitesProvider.getCurrentSiteId();
+
+ promises.push(this.courseProvider.invalidateModule(moduleId));
+ promises.push(this.filepoolProvider.invalidateFilesByComponent(siteId, this.component, moduleId));
+
+ return Promise.all(promises);
+ }
+
+ /**
+ * Invalidate WS calls needed to determine module status. It doesn't need to invalidate check updates.
+ * It should NOT invalidate files nor all the prefetched data.
+ *
+ * @param {any} module Module.
+ * @param {number} courseId Course ID the module belongs to.
+ * @return {Promise} Promise resolved when invalidated.
+ */
+ invalidateModule(module: any, courseId: number) : Promise {
+ return this.courseProvider.invalidateModule(module.id);
+ }
+
+ /**
+ * Check if a module can be downloaded. If the function is not defined, we assume that all modules are downloadable.
+ *
+ * @param {any} module Module.
+ * @param {number} courseId Course ID the module belongs to.
+ * @return {boolean|Promise} Whether the module can be downloaded. The promise should never be rejected.
+ */
+ isDownloadable(module: any, courseId: number) : boolean|Promise {
+ // By default, mark all instances as downloadable.
+ return true;
+ }
+
+ /**
+ * Check if a there's an ongoing download for the given identifier.
+ *
+ * @param {number} id Unique identifier per component.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Boolean} True if downloading, false otherwise.
+ */
+ isDownloading(id: number, siteId?: string) : boolean {
+ siteId = siteId || this.sitesProvider.getCurrentSiteId();
+ return !!(this.downloadPromises[siteId] && this.downloadPromises[siteId][this.getUniqueId(id)]);
+ }
+
+ /**
+ * Whether or not the handler is enabled on a site level.
+ *
+ * @return {boolean|Promise} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
+ */
+ isEnabled() : boolean|Promise {
+ return true;
+ }
+
+ /**
+ * Check if a file is downloadable.
+ *
+ * @param {any} file File to check.
+ * @return {boolean} Whether the file is downloadable.
+ */
+ isFileDownloadable(file: any) : boolean {
+ return file.type === 'file';
+ }
+
+ /**
+ * Load module contents into module.contents if they aren't loaded already.
+ *
+ * @param {any} module Module to load the contents.
+ * @param {number} [courseId] The course ID. Recommended to speed up the process and minimize data usage.
+ * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down).
+ * @return {Promise} Promise resolved when loaded.
+ */
+ loadContents(module: any, courseId: number, ignoreCache?: boolean) : Promise {
+ if (this.isResource) {
+ return this.courseProvider.loadModuleContents(module, courseId, undefined, false, ignoreCache);
+ }
+ return Promise.resolve();
+ }
+
+ /**
+ * Prefetch a module.
+ *
+ * @param {any} module Module.
+ * @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 done.
+ */
+ prefetch(module: any, courseId?: number, single?: boolean): Promise {
+ return this.downloadOrPrefetch(module, courseId, true);
+ }
+
+ /**
+ * Prefetch the module, setting package status at start and finish.
+ *
+ * Example usage from a child instance:
+ * return this.prefetchPackage(module, courseId, single, this.prefetchModule.bind(this), siteId, someParam, anotherParam);
+ *
+ * Then the function "prefetchModule" will receive params:
+ * prefetchModule(module, courseId, single, siteId, someParam, anotherParam)
+ *
+ * @param {any} module Module.
+ * @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.
+ * @param {prefetchFunction} downloadFn Function to perform the prefetch. Please check the documentation of prefetchFunction.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when the module has been downloaded. Data returned is not reliable.
+ */
+ prefetchPackage(module: any, courseId: number, single: boolean, downloadFn: prefetchFunction, siteId?: string, ...args) :
+ Promise {
+ siteId = siteId || this.sitesProvider.getCurrentSiteId();
+
+ if (!this.appProvider.isOnline()) {
+ // Cannot prefetch in offline.
+ return Promise.reject(this.translate.instant('core.networkerrormsg'));
+ }
+
+ if (this.isDownloading(module.id, siteId)) {
+ // There's already a download ongoing for this module, return the promise.
+ return this.getOngoingDownload(module.id, siteId);
+ }
+
+ const prefetchPromise = this.setDownloading(module.id, siteId).then(() => {
+ // Package marked as downloading, call the download function.
+ // Send all the params except downloadFn. This includes all params passed after siteId.
+ return downloadFn.apply(downloadFn, [module, courseId, single, siteId].concat(args));
+ }).then((extra: string) => {
+ // Prefetch finished, mark as downloaded.
+ return this.setDownloaded(module.id, siteId, extra);
+ }).catch((error) => {
+ // Error prefetching, go back to previous status and reject the promise.
+ return this.setPreviousStatusAndReject(module.id, error, siteId);
+ });
+
+ return this.addOngoingDownload(module.id, prefetchPromise, siteId);
+ }
+
+ /**
+ * Mark the module as downloaded.
+ *
+ * @param {number} id Unique identifier per component.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @param {string} [extra] Extra data to store.
+ * @return {Promise} Promise resolved when done.
+ */
+ setDownloaded(id: number, siteId?: string, extra?: string) : Promise {
+ siteId = siteId || this.sitesProvider.getCurrentSiteId();
+ return this.filepoolProvider.storePackageStatus(siteId, CoreConstants.DOWNLOADED, this.component, id, extra);
+ }
+
+ /**
+ * Mark the module as downloading.
+ *
+ * @param {number} id Unique identifier per component.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when done.
+ */
+ setDownloading(id: number, siteId?: string) : Promise {
+ siteId = siteId || this.sitesProvider.getCurrentSiteId();
+ return this.filepoolProvider.storePackageStatus(siteId, CoreConstants.DOWNLOADING, this.component, id);
+ }
+
+ /**
+ * Set previous status and return a rejected promise.
+ *
+ * @param {number} id Unique identifier per component.
+ * @param {any} [error] Error to return.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Rejected promise.
+ */
+ setPreviousStatusAndReject(id: number, error?: any, siteId?: string) : Promise {
+ siteId = siteId || this.sitesProvider.getCurrentSiteId();
+ return this.filepoolProvider.setPackagePreviousStatus(siteId, this.component, id).then(() => {
+ return Promise.reject(error);
+ });
+ }
+
+ /**
+ * Remove module downloaded files. If not defined, we'll use getFiles to remove them (slow).
+ *
+ * @param {any} module Module.
+ * @param {number} courseId Course ID the module belongs to.
+ * @return {Promise} Promise resolved when done.
+ */
+ removeFiles(module: any, courseId: number) : Promise {
+ return this.filepoolProvider.removeFilesByComponent(this.sitesProvider.getCurrentSiteId(), this.component, module.id);
+ }
+}
diff --git a/src/core/course/components/components.module.ts b/src/core/course/components/components.module.ts
new file mode 100644
index 000000000..ab679c5f3
--- /dev/null
+++ b/src/core/course/components/components.module.ts
@@ -0,0 +1,55 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { IonicModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreComponentsModule } from '../../../components/components.module';
+import { CoreDirectivesModule } from '../../../directives/directives.module';
+import { CoreCourseFormatComponent } from './format/format';
+import { CoreCourseModuleComponent } from './module/module';
+import { CoreCourseModuleCompletionComponent } from './module-completion/module-completion';
+import { CoreCourseModuleDescriptionComponent } from './module-description/module-description';
+import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsupported-module';
+
+@NgModule({
+ declarations: [
+ CoreCourseFormatComponent,
+ CoreCourseModuleComponent,
+ CoreCourseModuleCompletionComponent,
+ CoreCourseModuleDescriptionComponent,
+ CoreCourseUnsupportedModuleComponent
+ ],
+ imports: [
+ CommonModule,
+ IonicModule,
+ TranslateModule.forChild(),
+ CoreComponentsModule,
+ CoreDirectivesModule
+ ],
+ providers: [
+ ],
+ exports: [
+ CoreCourseFormatComponent,
+ CoreCourseModuleComponent,
+ CoreCourseModuleCompletionComponent,
+ CoreCourseModuleDescriptionComponent,
+ CoreCourseUnsupportedModuleComponent
+ ],
+ entryComponents: [
+ CoreCourseUnsupportedModuleComponent
+ ]
+})
+export class CoreCourseComponentsModule {}
diff --git a/src/core/course/components/format/format.html b/src/core/course/components/format/format.html
new file mode 100644
index 000000000..60289795a
--- /dev/null
+++ b/src/core/course/components/format/format.html
@@ -0,0 +1,83 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/core/course/components/format/format.ts b/src/core/course/components/format/format.ts
new file mode 100644
index 000000000..f6ff16cdb
--- /dev/null
+++ b/src/core/course/components/format/format.ts
@@ -0,0 +1,319 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, Input, OnInit, OnChanges, OnDestroy, ViewContainerRef, ComponentFactoryResolver, ViewChild, ChangeDetectorRef,
+ SimpleChange, Output, EventEmitter } from '@angular/core';
+import { TranslateService } from '@ngx-translate/core';
+import { CoreEventsProvider } from '../../../../providers/events';
+import { CoreLoggerProvider } from '../../../../providers/logger';
+import { CoreSitesProvider } from '../../../../providers/sites';
+import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
+import { CoreCourseProvider } from '../../../course/providers/course';
+import { CoreCourseHelperProvider } from '../../../course/providers/helper';
+import { CoreCourseFormatDelegate } from '../../../course/providers/format-delegate';
+import { CoreCourseModulePrefetchDelegate } from '../../../course/providers/module-prefetch-delegate';
+
+/**
+ * Component to display course contents using a certain format. If the format isn't found, use default one.
+ *
+ * The inputs of this component will be shared with the course format components. Please use CoreCourseFormatDelegate
+ * to register your handler for course formats.
+ *
+ * Example usage:
+ *
+ *
+ */
+@Component({
+ selector: 'core-course-format',
+ templateUrl: 'format.html'
+})
+export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
+ @Input() course: any; // The course to render.
+ @Input() sections: any[]; // List of course sections.
+ @Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
+ @Output() completionChanged?: EventEmitter; // Will emit an event when any module completion changes.
+
+ // Get the containers where to inject dynamic components. We use a setter because they might be inside a *ngIf.
+ @ViewChild('courseFormat', { read: ViewContainerRef }) set courseFormat(el: ViewContainerRef) {
+ if (this.course) {
+ this.createComponent('courseFormat', this.cfDelegate.getCourseFormatComponent(this.course), el);
+ } else {
+ // The component hasn't been initialized yet. Store the container.
+ this.componentContainers['courseFormat'] = el;
+ }
+ };
+ @ViewChild('courseSummary', { read: ViewContainerRef }) set courseSummary(el: ViewContainerRef) {
+ this.createComponent('courseSummary', this.cfDelegate.getCourseSummaryComponent(this.course), el);
+ };
+ @ViewChild('sectionSelector', { read: ViewContainerRef }) set sectionSelector(el: ViewContainerRef) {
+ this.createComponent('sectionSelector', this.cfDelegate.getSectionSelectorComponent(this.course), el);
+ };
+ @ViewChild('singleSection', { read: ViewContainerRef }) set singleSection(el: ViewContainerRef) {
+ this.createComponent('singleSection', this.cfDelegate.getSingleSectionComponent(this.course), el);
+ };
+ @ViewChild('allSections', { read: ViewContainerRef }) set allSections(el: ViewContainerRef) {
+ this.createComponent('allSections', this.cfDelegate.getAllSectionsComponent(this.course), el);
+ };
+
+ // Instances and containers of all the components that the handler could define.
+ protected componentContainers: {[type: string]: ViewContainerRef} = {};
+ componentInstances: {[type: string]: any} = {};
+
+ displaySectionSelector: boolean;
+ selectedSection: any;
+ allSectionsId: number = CoreCourseProvider.ALL_SECTIONS_ID;
+ selectOptions: any = {};
+ loaded: boolean;
+
+ protected logger;
+ protected sectionStatusObserver;
+
+ constructor(logger: CoreLoggerProvider, private cfDelegate: CoreCourseFormatDelegate, translate: TranslateService,
+ private factoryResolver: ComponentFactoryResolver, private cdr: ChangeDetectorRef,
+ private courseHelper: CoreCourseHelperProvider, private domUtils: CoreDomUtilsProvider,
+ eventsProvider: CoreEventsProvider, private sitesProvider: CoreSitesProvider,
+ prefetchDelegate: CoreCourseModulePrefetchDelegate) {
+
+ this.logger = logger.getInstance('CoreCourseFormatComponent');
+ this.selectOptions.title = translate.instant('core.course.sections');
+ this.completionChanged = new EventEmitter();
+
+ // Listen for section status changes.
+ this.sectionStatusObserver = eventsProvider.on(CoreEventsProvider.SECTION_STATUS_CHANGED, (data) => {
+ if (this.downloadEnabled && this.sections && this.sections.length && this.course && data.sectionId &&
+ data.courseId == this.course.id) {
+ // Check if the affected section is being downloaded. If so, we don't update section status
+ // because it'll already be updated when the download finishes.
+ let downloadId = this.courseHelper.getSectionDownloadId({id: data.sectionId});
+ if (prefetchDelegate.isBeingDownloaded(downloadId)) {
+ return;
+ }
+
+ // Get the affected section.
+ let section;
+ for (let i = 0; i < this.sections.length; i++) {
+ let s = this.sections[i];
+ if (s.id === data.sectionId) {
+ section = s;
+ break;
+ }
+ }
+
+ if (!section) {
+ // Section not found, stop.
+ return;
+ }
+
+ // Recalculate the status.
+ this.courseHelper.calculateSectionStatus(section, this.course.id, false).then(() => {
+ if (section.isDownloading && !prefetchDelegate.isBeingDownloaded(downloadId)) {
+ // All the modules are now downloading, set a download all promise.
+ this.prefetch(section, false);
+ }
+ });
+ }
+ }, this.sitesProvider.getCurrentSiteId());
+ }
+
+ /**
+ * Component being initialized.
+ */
+ ngOnInit() {
+ this.displaySectionSelector = this.cfDelegate.displaySectionSelector(this.course);
+
+ this.createComponent(
+ 'courseFormat', this.cfDelegate.getCourseFormatComponent(this.course), this.componentContainers['courseFormat']);
+ }
+
+ /**
+ * Detect changes on input properties.
+ */
+ ngOnChanges(changes: {[name: string]: SimpleChange}) {
+ if (changes.sections && this.sections) {
+ if (!this.selectedSection) {
+ // There is no selected section yet, calculate which one to get.
+ this.cfDelegate.getCurrentSection(this.course, this.sections).then((section) => {
+ this.loaded = true;
+ this.sectionChanged(section);
+ });
+ } else {
+ // We have a selected section, but the list has changed. Search the section in the list.
+ let newSection;
+ for (let i = 0; i < this.sections.length; i++) {
+ let section = this.sections[i];
+ if (this.compareSections(section, this.selectedSection)) {
+ newSection = section;
+ break;
+ }
+ }
+
+ if (!newSection) {
+ // Section not found, calculate which one to use.
+ newSection = this.cfDelegate.getCurrentSection(this.course, this.sections);
+ }
+ this.sectionChanged(newSection);
+ }
+ }
+
+ if (changes.downloadEnabled && this.downloadEnabled) {
+ this.calculateSectionsStatus(false);
+ }
+
+ // Apply the changes to the components and call ngOnChanges if it exists.
+ for (let type in this.componentInstances) {
+ let instance = this.componentInstances[type];
+
+ for (let name in changes) {
+ instance[name] = changes[name].currentValue;
+ }
+
+ if (instance.ngOnChanges) {
+ instance.ngOnChanges(changes);
+ }
+ }
+ }
+
+ /**
+ * Create a component, add it to a container and set the input data.
+ *
+ * @param {string} type The "type" of the component.
+ * @param {any} componentClass The class of the component to create.
+ * @param {ViewContainerRef} container The container to add the component to.
+ * @return {boolean} Whether the component was successfully created.
+ */
+ protected createComponent(type: string, componentClass: any, container: ViewContainerRef) : boolean {
+ if (!componentClass || !container) {
+ // No component to instantiate or container doesn't exist right now.
+ return false;
+ }
+
+ if (this.componentInstances[type] && container === this.componentContainers[type]) {
+ // Component already instantiated and the component hasn't been destroyed, nothing to do.
+ return true;
+ }
+
+ try {
+ // Create the component and add it to the container.
+ const factory = this.factoryResolver.resolveComponentFactory(componentClass),
+ componentRef = container.createComponent(factory);
+
+ this.componentContainers[type] = container;
+ this.componentInstances[type] = componentRef.instance;
+
+ // Set the Input data.
+ this.componentInstances[type].course = this.course;
+ this.componentInstances[type].sections = this.sections;
+ this.componentInstances[type].downloadEnabled = this.downloadEnabled;
+
+ this.cdr.detectChanges(); // The instances are used in ngIf, tell Angular that something has changed.
+
+ return true;
+ } catch(ex) {
+ this.logger.error('Error creating component', type, ex);
+ return false;
+ }
+ }
+
+ /**
+ * Function called when selected section changes.
+ *
+ * @param {any} newSection The new selected section.
+ */
+ sectionChanged(newSection: any) {
+ let previousValue = this.selectedSection;
+ this.selectedSection = newSection;
+
+ // If there is a component to render the current section, update its section.
+ if (this.componentInstances.singleSection) {
+ this.componentInstances.singleSection.section = this.selectedSection;
+ if (this.componentInstances.singleSection.ngOnChanges) {
+ this.componentInstances.singleSection.ngOnChanges({
+ section: new SimpleChange(previousValue, newSection, typeof previousValue != 'undefined')
+ });
+ }
+ }
+ }
+
+ /**
+ * Compare if two sections are equal.
+ *
+ * @param {any} s1 First section.
+ * @param {any} s2 Second section.
+ * @return {boolean} Whether they're equal.
+ */
+ compareSections(s1: any, s2: any) : boolean {
+ return s1 && s2 ? s1.id === s2.id : s1 === s2;
+ }
+
+ /**
+ * Calculate the status of sections.
+ *
+ * @param {boolean} refresh [description]
+ */
+ protected calculateSectionsStatus(refresh?: boolean) : void {
+ this.courseHelper.calculateSectionsStatus(this.sections, this.course.id, refresh).catch(() => {
+ // Ignore errors (shouldn't happen).
+ });
+ }
+
+ /**
+ * Confirm and prefetch a section. If the section is "all sections", prefetch all the sections.
+ *
+ * @param {Event} e Click event.
+ * @param {any} section Section to download.
+ */
+ prefetch(e: Event, section: any) : void {
+ e.preventDefault();
+ e.stopPropagation();
+
+ section.isCalculating = true;
+ this.courseHelper.confirmDownloadSizeSection(this.course.id, section, this.sections).then(() => {
+ this.prefetchSection(section, true);
+ }, (error) => {
+ // User cancelled or there was an error calculating the size.
+ if (error) {
+ this.domUtils.showErrorModal(error);
+ }
+ }).finally(() => {
+ section.isCalculating = false;
+ });
+ }
+
+ /**
+ * Prefetch a section.
+ *
+ * @param {any} section The section to download.
+ * @param {boolean} [manual] Whether the prefetch was started manually or it was automatically started because all modules
+ * are being downloaded.
+ */
+ protected prefetchSection(section: any, manual?: boolean) {
+ this.courseHelper.prefetchSection(section, this.course.id, this.sections).catch((error) => {
+ // Don't show error message if it's an automatic download.
+ if (!manual) {
+ return;
+ }
+
+ this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingsection', true);
+ });
+ }
+
+ /**
+ * Component destroyed.
+ */
+ ngOnDestroy() {
+ if (this.sectionStatusObserver) {
+ this.sectionStatusObserver.off();
+ }
+ }
+}
diff --git a/src/core/course/components/module-completion/module-completion.html b/src/core/course/components/module-completion/module-completion.html
new file mode 100644
index 000000000..7de6d78d5
--- /dev/null
+++ b/src/core/course/components/module-completion/module-completion.html
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/src/core/course/components/module-completion/module-completion.scss b/src/core/course/components/module-completion/module-completion.scss
new file mode 100644
index 000000000..b0b4a663c
--- /dev/null
+++ b/src/core/course/components/module-completion/module-completion.scss
@@ -0,0 +1,7 @@
+core-course-module-completion a {
+ img {
+ padding: 5px;
+ width: 30px;
+ vertical-align: middle;
+ }
+}
\ No newline at end of file
diff --git a/src/core/course/components/module-completion/module-completion.ts b/src/core/course/components/module-completion/module-completion.ts
new file mode 100644
index 000000000..1bd9e99a2
--- /dev/null
+++ b/src/core/course/components/module-completion/module-completion.ts
@@ -0,0 +1,150 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, Input, Output, EventEmitter, OnChanges, SimpleChange } from '@angular/core';
+import { TranslateService } from '@ngx-translate/core';
+import { CoreSitesProvider } from '../../../../providers/sites';
+import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
+import { CoreTextUtilsProvider } from '../../../../providers/utils/text';
+
+/**
+ * Component to handle activity completion. It shows a checkbox with the current status, and allows manually changing
+ * the completion if it's allowed.
+ *
+ * Example usage:
+ *
+ *
+ */
+@Component({
+ selector: 'core-course-module-completion',
+ templateUrl: 'module-completion.html'
+})
+export class CoreCourseModuleCompletionComponent implements OnChanges {
+ @Input() completion: any; // The completion status.
+ @Input() moduleName?: string; // The name of the module this completion affects.
+ @Output() completionChanged?: EventEmitter; // Will emit an event when the completion changes.
+
+ completionImage: string;
+ completionDescription: string;
+
+ constructor(private textUtils: CoreTextUtilsProvider, private translate: TranslateService,
+ private domUtils: CoreDomUtilsProvider, private sitesProvider: CoreSitesProvider) {
+ this.completionChanged = new EventEmitter();
+ }
+
+ /**
+ * Detect changes on input properties.
+ */
+ ngOnChanges(changes: {[name: string]: SimpleChange}) {
+ if (changes.completion && this.completion) {
+ this.showStatus();
+ }
+ }
+
+ /**
+ * Completion clicked.
+ *
+ * @param {Event} e The click event.
+ */
+ completionClicked(e: Event) : void {
+ if (this.completion) {
+ if (typeof this.completion.cmid == 'undefined' || this.completion.tracking !== 1) {
+ return;
+ }
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ let modal = this.domUtils.showModalLoading(),
+ params = {
+ cmid: this.completion.cmid,
+ completed: this.completion.state === 1 ? 0 : 1
+ },
+ currentSite = this.sitesProvider.getCurrentSite();
+
+ currentSite.write('core_completion_update_activity_completion_status_manually', params).then((response) => {
+ if (!response.status) {
+ return Promise.reject(null);
+ }
+
+ this.completionChanged.emit();
+ }).catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'core.errorchangecompletion', true);
+ }).finally(() => {
+ modal.dismiss();
+ });
+ }
+ }
+
+ /**
+ * Set image and description to show as completion icon.
+ */
+ protected showStatus() : void {
+ let langKey,
+ moduleName = this.moduleName || '',
+ image;
+
+ if (this.completion.tracking === 1 && this.completion.state === 0) {
+ image = 'completion-manual-n';
+ langKey = 'core.completion-alt-manual-n';
+ } else if (this.completion.tracking === 1 && this.completion.state === 1) {
+ image = 'completion-manual-y';
+ langKey = 'core.completion-alt-manual-y';
+ } else if (this.completion.tracking === 2 && this.completion.state === 0) {
+ image = 'completion-auto-n';
+ langKey = 'core.completion-alt-auto-n';
+ } else if (this.completion.tracking === 2 && this.completion.state === 1) {
+ image = 'completion-auto-y';
+ langKey = 'core.completion-alt-auto-y';
+ } else if (this.completion.tracking === 2 && this.completion.state === 2) {
+ image = 'completion-auto-pass';
+ langKey = 'core.completion-alt-auto-pass';
+ } else if (this.completion.tracking === 2 && this.completion.state === 3) {
+ image = 'completion-auto-fail';
+ langKey = 'core.completion-alt-auto-fail';
+ }
+
+ if (image) {
+ if (this.completion.overrideby > 0) {
+ image += '-override';
+ }
+ this.completionImage = 'assets/img/completion/' + image + '.svg';
+ }
+
+ if (moduleName) {
+ this.textUtils.formatText(moduleName, true, true, 50).then((modNameFormatted) => {
+ let promise;
+
+ if (this.completion.overrideby > 0) {
+ langKey += '-override';
+
+ // @todo: Get user profile.
+ // promise = $mmUser.getProfile(scope.completion.overrideby, scope.completion.courseId, true).then(function(profile) {
+ // return {
+ // overrideuser: profile.fullname,
+ // modname: modNameFormatted
+ // };
+ // });
+ } else {
+ promise = Promise.resolve(modNameFormatted);
+ }
+
+ return promise.then((translateParams) => {
+ this.completionDescription = this.translate.instant(langKey, {$a: translateParams});
+ });
+ });
+ }
+ }
+}
diff --git a/src/core/course/components/module-description/module-description.html b/src/core/course/components/module-description/module-description.html
new file mode 100644
index 000000000..8ccd37fc2
--- /dev/null
+++ b/src/core/course/components/module-description/module-description.html
@@ -0,0 +1,6 @@
+
+
+
+ {{ note }}
+
+
\ No newline at end of file
diff --git a/src/core/course/components/module-description/module-description.ts b/src/core/course/components/module-description/module-description.ts
new file mode 100644
index 000000000..d10994df4
--- /dev/null
+++ b/src/core/course/components/module-description/module-description.ts
@@ -0,0 +1,45 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, Input } from '@angular/core';
+
+/**
+ * Component to display the description of a module.
+ *
+ * This directive is meant to display a module description in a similar way throughout all the app.
+ *
+ * You can add a note at the right side of the description by using the 'note' attribute.
+ *
+ * You can also pass a component and componentId to be used in format-text.
+ *
+ * Module descriptions are shortened by default, allowing the user to see the full description by clicking in it.
+ * If you want the whole description to be shown you can use the 'showFull' attribute.
+ *
+ * Example usage:
+ *
+ *
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/core/course/components/module/module.scss b/src/core/course/components/module/module.scss
new file mode 100644
index 000000000..8eeee189e
--- /dev/null
+++ b/src/core/course/components/module/module.scss
@@ -0,0 +1,30 @@
+core-course-module {
+
+ a.core-course-module-handler {
+ align-items: flex-start;
+ item-inner {
+ padding-right: 0;
+ }
+ }
+
+ .core-module-icon {
+ align-items: flex-start;
+ }
+
+ .core-module-buttons {
+ display: flex;
+ flex-flow: row;
+ align-items: center;
+ z-index: 1;
+ cursor: pointer;
+ pointer-events: auto;
+ position: absolute;
+ right: 0;
+ top: 4px;
+
+ .spinner {
+ right: 7px;
+ position: absolute;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/core/course/components/module/module.ts b/src/core/course/components/module/module.ts
new file mode 100644
index 000000000..1353c4d64
--- /dev/null
+++ b/src/core/course/components/module/module.ts
@@ -0,0 +1,71 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
+import { NavController } from 'ionic-angular';
+import { CoreCourseModuleHandlerButton } from '../../providers/module-delegate';
+
+/**
+ * Component to display a module entry in a list of modules.
+ *
+ * Example usage:
+ *
+ *
+ */
+@Component({
+ selector: 'core-course-module',
+ templateUrl: 'module.html'
+})
+export class CoreCourseModuleComponent implements OnInit {
+ @Input() module: any; // The module to render.
+ @Input() courseId: number; // The course the module belongs to.
+ @Output() completionChanged?: EventEmitter; // Will emit an event when the module completion changes.
+
+ constructor(private navCtrl: NavController) {
+ this.completionChanged = new EventEmitter();
+ }
+
+ /**
+ * Component being initialized.
+ */
+ ngOnInit() {
+ // Handler data must be defined. If it isn't, set it to prevent errors.
+ if (this.module && !this.module.handlerData) {
+ this.module.handlerData = {};
+ }
+ }
+
+ /**
+ * Function called when the module is clicked.
+ *
+ * @param {Event} event Click event.
+ */
+ moduleClicked(event: Event) {
+ if (this.module.uservisible !== false && this.module.handlerData.action) {
+ this.module.handlerData.action(event, this.navCtrl, this.module, this.courseId);
+ }
+ }
+
+ /**
+ * Function called when a button is clicked.
+ *
+ * @param {Event} event Click event.
+ * @param {CoreCourseModuleHandlerButton} button The clicked button.
+ */
+ buttonClicked(event: Event, button: CoreCourseModuleHandlerButton) {
+ if (button && button.action) {
+ button.action(event, this.navCtrl, this.module, this.courseId);
+ }
+ }
+}
diff --git a/src/core/course/components/unsupported-module/unsupported-module.html b/src/core/course/components/unsupported-module/unsupported-module.html
new file mode 100644
index 000000000..775aff655
--- /dev/null
+++ b/src/core/course/components/unsupported-module/unsupported-module.html
@@ -0,0 +1,18 @@
+
\ No newline at end of file
diff --git a/src/core/course/components/unsupported-module/unsupported-module.ts b/src/core/course/components/unsupported-module/unsupported-module.ts
new file mode 100644
index 000000000..1c1910d87
--- /dev/null
+++ b/src/core/course/components/unsupported-module/unsupported-module.ts
@@ -0,0 +1,48 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, Input, OnInit } from '@angular/core';
+import { IonicPage, NavParams } from 'ionic-angular';
+import { TranslateService } from '@ngx-translate/core';
+import { CoreTextUtilsProvider } from '../../../../providers/utils/text';
+import { CoreCourseProvider } from '../../providers/course';
+import { CoreCourseModuleDelegate } from '../../providers/module-delegate';
+
+/**
+ * Component that displays info about an unsupported module.
+ */
+@Component({
+ selector: 'core-course-unsupported-module',
+ templateUrl: 'unsupported-module.html',
+})
+export class CoreCourseUnsupportedModuleComponent implements OnInit {
+ @Input() course: any; // The course to module belongs to.
+ @Input() module: any; // The module to render.
+
+ isDisabledInSite: boolean;
+ isSupportedByTheApp: boolean;
+ moduleName: string;
+
+ constructor(navParams: NavParams, private translate: TranslateService, private textUtils: CoreTextUtilsProvider,
+ private courseProvider: CoreCourseProvider, private moduleDelegate: CoreCourseModuleDelegate) {}
+
+ /**
+ * Component being initialized.
+ */
+ ngOnInit() {
+ this.isDisabledInSite = this.moduleDelegate.isModuleDisabledInSite(this.module.modname);
+ this.isSupportedByTheApp = this.moduleDelegate.hasHandler(this.module.modname);
+ this.moduleName = this.courseProvider.translateModuleName(this.module.modname);
+ }
+}
diff --git a/src/core/course/course.module.ts b/src/core/course/course.module.ts
new file mode 100644
index 000000000..1c74f500a
--- /dev/null
+++ b/src/core/course/course.module.ts
@@ -0,0 +1,45 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { CoreCourseProvider } from './providers/course';
+import { CoreCourseHelperProvider } from './providers/helper';
+import { CoreCourseFormatDelegate } from './providers/format-delegate';
+import { CoreCourseModuleDelegate } from './providers/module-delegate';
+import { CoreCourseModulePrefetchDelegate } from './providers/module-prefetch-delegate';
+import { CoreCourseFormatDefaultHandler } from './providers/default-format';
+import { CoreCourseFormatSingleActivityModule } from './formats/singleactivity/singleactivity.module';
+import { CoreCourseFormatSocialModule } from './formats/social/social.module';
+import { CoreCourseFormatTopicsModule} from './formats/topics/topics.module';
+import { CoreCourseFormatWeeksModule } from './formats/weeks/weeks.module';
+
+@NgModule({
+ declarations: [],
+ imports: [
+ CoreCourseFormatSingleActivityModule,
+ CoreCourseFormatTopicsModule,
+ CoreCourseFormatWeeksModule,
+ CoreCourseFormatSocialModule
+ ],
+ providers: [
+ CoreCourseProvider,
+ CoreCourseHelperProvider,
+ CoreCourseFormatDelegate,
+ CoreCourseModuleDelegate,
+ CoreCourseModulePrefetchDelegate,
+ CoreCourseFormatDefaultHandler
+ ],
+ exports: []
+})
+export class CoreCourseModule {}
diff --git a/src/core/course/formats/singleactivity/components/format.ts b/src/core/course/formats/singleactivity/components/format.ts
new file mode 100644
index 000000000..571f258f0
--- /dev/null
+++ b/src/core/course/formats/singleactivity/components/format.ts
@@ -0,0 +1,119 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, Input, OnChanges, ViewContainerRef, ComponentFactoryResolver, ChangeDetectorRef,
+ SimpleChange } from '@angular/core';
+import { CoreLoggerProvider } from '../../../../../providers/logger';
+import { CoreCourseModuleDelegate } from '../../../providers/module-delegate';
+import { CoreCourseUnsupportedModuleComponent } from '../../../components/unsupported-module/unsupported-module';
+
+/**
+ * Component to display single activity format. It will determine the right component to use and instantiate it.
+ *
+ * The instantiated component will receive the course and the module as inputs.
+ */
+@Component({
+ selector: 'core-course-format-single-activity',
+ template: ''
+})
+export class CoreCourseFormatSingleActivityComponent implements OnChanges {
+ @Input() course: any; // The course to render.
+ @Input() sections: any[]; // List of course sections.
+ @Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
+
+ protected logger: any;
+ protected module: any;
+ protected componentInstance: any;
+
+ constructor(logger: CoreLoggerProvider, private viewRef: ViewContainerRef, private factoryResolver: ComponentFactoryResolver,
+ private cdr: ChangeDetectorRef, private moduleDelegate: CoreCourseModuleDelegate) {
+ this.logger = logger.getInstance('CoreCourseFormatSingleActivityComponent');
+ }
+
+ /**
+ * Detect changes on input properties.
+ */
+ ngOnChanges(changes: {[name: string]: SimpleChange}) {
+ if (this.course && this.sections && this.sections.length) {
+ // In single activity the module should only have 1 section and 1 module. Get the module.
+ let module = this.sections[0] && this.sections[0].modules && this.sections[0].modules[0];
+ if (module && !this.componentInstance) {
+ // We haven't created the component yet. Create it now.
+ this.createComponent(module);
+ }
+
+ if (this.componentInstance && this.componentInstance.ngOnChanges) {
+ // Call ngOnChanges of the component.
+ let newChanges: {[name: string]: SimpleChange} = {};
+
+ // Check if course has changed.
+ if (changes.course) {
+ newChanges.course = changes.course
+ this.componentInstance.course = this.course;
+ }
+
+ // Check if module has changed.
+ if (changes.sections && module != this.module) {
+ newChanges.module = {
+ currentValue: module,
+ firstChange: changes.sections.firstChange,
+ previousValue: this.module,
+ isFirstChange: () => {
+ return newChanges.module.firstChange;
+ }
+ };
+ this.componentInstance.module = module;
+ this.module = module;
+ }
+
+ if (Object.keys(newChanges).length) {
+ this.componentInstance.ngOnChanges(newChanges);
+ }
+ }
+ }
+ }
+
+ /**
+ * Create the component, add it to the container and set the input data.
+ *
+ * @param {any} module The module.
+ * @return {boolean} Whether the component was successfully created.
+ */
+ protected createComponent(module: any) : boolean {
+ let componentClass = this.moduleDelegate.getMainComponent(this.course, module) || CoreCourseUnsupportedModuleComponent;
+ if (!componentClass) {
+ // No component to instantiate.
+ return false;
+ }
+
+ try {
+ // Create the component and add it to the container.
+ const factory = this.factoryResolver.resolveComponentFactory(componentClass),
+ componentRef = this.viewRef.createComponent(factory);
+
+ this.componentInstance = componentRef.instance;
+
+ // Set the Input data.
+ this.componentInstance.courseId = this.course.id;
+ this.componentInstance.module = module;
+
+ // this.cdr.detectChanges(); // The instances are used in ngIf, tell Angular that something has changed.
+
+ return true;
+ } catch(ex) {
+ this.logger.error('Error creating component', ex);
+ return false;
+ }
+ }
+}
diff --git a/src/core/course/formats/singleactivity/providers/handler.ts b/src/core/course/formats/singleactivity/providers/handler.ts
new file mode 100644
index 000000000..396c94971
--- /dev/null
+++ b/src/core/course/formats/singleactivity/providers/handler.ts
@@ -0,0 +1,83 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreCourseFormatHandler } from '../../../providers/format-delegate';
+import { CoreCourseFormatSingleActivityComponent } from '../components/format';
+
+/**
+ * Handler to support singleactivity course format.
+ */
+@Injectable()
+export class CoreCourseFormatSingleActivityHandler implements CoreCourseFormatHandler {
+ name = 'singleactivity';
+
+ constructor() {}
+
+ /**
+ * Whether or not the handler is enabled on a site level.
+ *
+ * @return {boolean|Promise} True or promise resolved with true if enabled.
+ */
+ isEnabled() : boolean|Promise {
+ return true;
+ }
+
+ /**
+ * Whether it allows seeing all sections at the same time. Defaults to true.
+ *
+ * @param {any} course The course to check.
+ * @type {boolean} Whether it can view all sections.
+ */
+ canViewAllSections(course: any) : boolean {
+ return false;
+ }
+
+ /**
+ * Get the title to use in course page. If not defined, course fullname.
+ * This function will be called without sections first, and then call it again when the sections are retrieved.
+ *
+ * @param {any} course The course.
+ * @param {any[]} [sections] List of sections.
+ * @return {string} Title.
+ */
+ getCourseTitle(course: any, sections?: any[]) : string {
+ if (sections && sections[0] && sections[0].modules && sections[0].modules[0]) {
+ return sections[0].modules[0].name;
+ }
+ return course.fullname || '';
+ }
+
+ /**
+ * Whether the default section selector should be displayed. Defaults to true.
+ *
+ * @param {any} course The course to check.
+ * @type {boolean} Whether the default section selector should be displayed.
+ */
+ displaySectionSelector(course: any) : boolean {
+ return false;
+ }
+
+ /**
+ * Return the Component to use to display the course format instead of using the default one.
+ * Use it if you want to display a format completely different from the default one.
+ * If you want to customize the default format there are several methods to customize parts of it.
+ *
+ * @param {any} course The course to render.
+ * @return {any} The component to use, undefined if not found.
+ */
+ getCourseFormatComponent(course: any) : any {
+ return CoreCourseFormatSingleActivityComponent;
+ }
+}
diff --git a/src/core/course/formats/singleactivity/singleactivity.module.ts b/src/core/course/formats/singleactivity/singleactivity.module.ts
new file mode 100644
index 000000000..0ccb98388
--- /dev/null
+++ b/src/core/course/formats/singleactivity/singleactivity.module.ts
@@ -0,0 +1,40 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { CoreCourseFormatSingleActivityComponent } from './components/format';
+import { CoreCourseFormatSingleActivityHandler } from './providers/handler';
+import { CoreCourseFormatDelegate } from '../../providers/format-delegate';
+
+@NgModule({
+ declarations: [
+ CoreCourseFormatSingleActivityComponent
+ ],
+ imports: [
+ ],
+ providers: [
+ CoreCourseFormatSingleActivityHandler
+ ],
+ exports: [
+ CoreCourseFormatSingleActivityComponent
+ ],
+ entryComponents: [
+ CoreCourseFormatSingleActivityComponent
+ ]
+})
+export class CoreCourseFormatSingleActivityModule {
+ constructor(formatDelegate: CoreCourseFormatDelegate, handler: CoreCourseFormatSingleActivityHandler) {
+ formatDelegate.registerHandler(handler);
+ }
+}
diff --git a/src/core/course/formats/social/providers/handler.ts b/src/core/course/formats/social/providers/handler.ts
new file mode 100644
index 000000000..25d942515
--- /dev/null
+++ b/src/core/course/formats/social/providers/handler.ts
@@ -0,0 +1,25 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreCourseFormatSingleActivityHandler } from '../../singleactivity/providers/handler';
+
+/**
+ * Handler to support social course format.
+ * This format is like singleactivity in the mobile app, so we'll use the same implementation for both.
+ */
+@Injectable()
+export class CoreCourseFormatSocialHandler extends CoreCourseFormatSingleActivityHandler {
+ name = 'social';
+}
diff --git a/src/core/course/formats/social/social.module.ts b/src/core/course/formats/social/social.module.ts
new file mode 100644
index 000000000..0ee3677fa
--- /dev/null
+++ b/src/core/course/formats/social/social.module.ts
@@ -0,0 +1,32 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { CoreCourseFormatSocialHandler } from './providers/handler';
+import { CoreCourseFormatDelegate } from '../../providers/format-delegate';
+
+@NgModule({
+ declarations: [],
+ imports: [
+ ],
+ providers: [
+ CoreCourseFormatSocialHandler
+ ],
+ exports: []
+})
+export class CoreCourseFormatSocialModule {
+ constructor(formatDelegate: CoreCourseFormatDelegate, handler: CoreCourseFormatSocialHandler) {
+ formatDelegate.registerHandler(handler);
+ }
+}
diff --git a/src/core/course/formats/topics/providers/handler.ts b/src/core/course/formats/topics/providers/handler.ts
new file mode 100644
index 000000000..18d2c1759
--- /dev/null
+++ b/src/core/course/formats/topics/providers/handler.ts
@@ -0,0 +1,35 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreCourseFormatHandler } from '../../../providers/format-delegate';
+
+/**
+ * Handler to support topics course format.
+ */
+@Injectable()
+export class CoreCourseFormatTopicsHandler implements CoreCourseFormatHandler {
+ name = 'topics';
+
+ constructor() {}
+
+ /**
+ * Whether or not the handler is enabled on a site level.
+ *
+ * @return {boolean|Promise} True or promise resolved with true if enabled.
+ */
+ isEnabled() : boolean|Promise {
+ return true;
+ }
+}
diff --git a/src/core/course/formats/topics/topics.module.ts b/src/core/course/formats/topics/topics.module.ts
new file mode 100644
index 000000000..97cedcc73
--- /dev/null
+++ b/src/core/course/formats/topics/topics.module.ts
@@ -0,0 +1,32 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { CoreCourseFormatTopicsHandler } from './providers/handler';
+import { CoreCourseFormatDelegate } from '../../providers/format-delegate';
+
+@NgModule({
+ declarations: [],
+ imports: [
+ ],
+ providers: [
+ CoreCourseFormatTopicsHandler
+ ],
+ exports: []
+})
+export class CoreCourseFormatTopicsModule {
+ constructor(formatDelegate: CoreCourseFormatDelegate, handler: CoreCourseFormatTopicsHandler) {
+ formatDelegate.registerHandler(handler);
+ }
+}
diff --git a/src/core/course/formats/weeks/providers/handler.ts b/src/core/course/formats/weeks/providers/handler.ts
new file mode 100644
index 000000000..b6f5beff7
--- /dev/null
+++ b/src/core/course/formats/weeks/providers/handler.ts
@@ -0,0 +1,87 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreCourseFormatHandler } from '../../../providers/format-delegate';
+import { CoreTimeUtilsProvider } from '../../../../../providers/utils/time';
+import { CoreConstants } from '../../../../constants';
+
+/**
+ * Handler to support weeks course format.
+ */
+@Injectable()
+export class CoreCourseFormatWeeksHandler implements CoreCourseFormatHandler {
+ name = 'weeks';
+
+ constructor(private timeUtils: CoreTimeUtilsProvider) {}
+
+ /**
+ * Whether or not the handler is enabled on a site level.
+ *
+ * @return {boolean|Promise} True or promise resolved with true if enabled.
+ */
+ isEnabled() : boolean|Promise {
+ return true;
+ }
+
+ /**
+ * Given a list of sections, get the "current" section that should be displayed first.
+ *
+ * @param {any} course The course to get the title.
+ * @param {any[]} sections List of sections.
+ * @return {any|Promise} Current section (or promise resolved with current section).
+ */
+ getCurrentSection(course: any, sections: any[]) : any|Promise {
+ let now = this.timeUtils.timestamp();
+
+ if (now < course.startdate || (course.enddate && now > course.enddate)) {
+ // Course hasn't started yet or it has ended already. Return the first section.
+ return sections[1];
+ }
+
+ for (let i = 0; i < sections.length; i++) {
+ let section = sections[i];
+ if (typeof section.section == 'undefined' || section.section < 1) {
+ continue;
+ }
+
+ let dates = this.getSectionDates(section, course.startdate);
+ if ((now >= dates.start) && (now < dates.end)) {
+ return section;
+ }
+ }
+
+ // The section wasn't found, return the first section.
+ return sections[1];
+ }
+
+ /**
+ * Return the start and end date of a section.
+ *
+ * @param {any} section The section to treat.
+ * @param {number} startDate The course start date (in seconds).
+ * @return {{start: number, end: number}} An object with the start and end date of the section.
+ */
+ protected getSectionDates(section: any, startDate: number) : {start: number, end: number} {
+ // Hack alert. We add 2 hours to avoid possible DST problems. (e.g. we go into daylight savings and the date changes).
+ startDate = startDate + 7200;
+
+ let dates = {
+ start: startDate + (CoreConstants.SECONDS_WEEK * (section.section - 1)),
+ end: 0
+ };
+ dates.end = dates.start + CoreConstants.SECONDS_WEEK;
+ return dates;
+ }
+}
diff --git a/src/core/course/formats/weeks/weeks.module.ts b/src/core/course/formats/weeks/weeks.module.ts
new file mode 100644
index 000000000..b26e66f44
--- /dev/null
+++ b/src/core/course/formats/weeks/weeks.module.ts
@@ -0,0 +1,32 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { CoreCourseFormatWeeksHandler } from './providers/handler';
+import { CoreCourseFormatDelegate } from '../../providers/format-delegate';
+
+@NgModule({
+ declarations: [],
+ imports: [
+ ],
+ providers: [
+ CoreCourseFormatWeeksHandler
+ ],
+ exports: []
+})
+export class CoreCourseFormatWeeksModule {
+ constructor(formatDelegate: CoreCourseFormatDelegate, handler: CoreCourseFormatWeeksHandler) {
+ formatDelegate.registerHandler(handler);
+ }
+}
diff --git a/src/core/course/lang/en.json b/src/core/course/lang/en.json
new file mode 100644
index 000000000..f4997d593
--- /dev/null
+++ b/src/core/course/lang/en.json
@@ -0,0 +1,23 @@
+{
+ "activitydisabled": "Your organisation has disabled this activity in the mobile app.",
+ "activitynotyetviewableremoteaddon": "Your organisation installed a plugin that is not yet supported.",
+ "activitynotyetviewablesiteupgradeneeded": "Your organisation's Moodle installation needs to be updated.",
+ "allsections": "All sections",
+ "askadmintosupport": "Contact the site administrator and tell them you want to use this activity with the Moodle Mobile app.",
+ "confirmdeletemodulefiles": "Are you sure you want to delete these files?",
+ "confirmdownload": "You are about to download {{size}}. Are you sure you want to continue?",
+ "confirmdownloadunknownsize": "It was not possible to calculate the size of the download. Are you sure you want to continue?",
+ "confirmpartialdownloadsize": "You are about to download at least {{size}}. Are you sure you want to continue?",
+ "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",
+ "nocontentavailable": "No content available at the moment.",
+ "overriddennotice": "Your final grade from this activity was manually adjusted.",
+ "sections": "Sections",
+ "useactivityonbrowser": "You can still use it using your device's web browser."
+}
\ No newline at end of file
diff --git a/src/core/course/pages/section/section.html b/src/core/course/pages/section/section.html
new file mode 100644
index 000000000..6d64c169e
--- /dev/null
+++ b/src/core/course/pages/section/section.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
diff --git a/src/core/courses/pages/my-overview/my-overview.ts b/src/core/courses/pages/my-overview/my-overview.ts
index 21a7c423d..fb5183d40 100644
--- a/src/core/courses/pages/my-overview/my-overview.ts
+++ b/src/core/courses/pages/my-overview/my-overview.ts
@@ -12,11 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component } from '@angular/core';
+import { Component, OnDestroy } from '@angular/core';
import { IonicPage, NavController } from 'ionic-angular';
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
import { CoreCoursesProvider } from '../../providers/courses';
import { CoreCoursesMyOverviewProvider } from '../../providers/my-overview';
+import { CoreCourseHelperProvider } from '../../../course/providers/helper';
import * as moment from 'moment';
/**
@@ -27,7 +28,7 @@ import * as moment from 'moment';
selector: 'page-core-courses-my-overview',
templateUrl: 'my-overview.html',
})
-export class CoreCoursesMyOverviewPage {
+export class CoreCoursesMyOverviewPage implements OnDestroy {
tabShown = 'courses';
timeline = {
sort: 'sortbydates',
@@ -51,23 +52,25 @@ export class CoreCoursesMyOverviewPage {
showFilter = false;
searchEnabled: boolean;
filteredCourses: any[];
+ tabs = [];
+ prefetchCoursesData = {
+ inprogress: {},
+ past: {},
+ future: {}
+ };
- protected prefetchIconInitialized = false;
- protected myCoursesObserver;
- protected siteUpdatedObserver;
+ protected prefetchIconsInitialized = false;
+ protected isDestroyed;
constructor(private navCtrl: NavController, private coursesProvider: CoreCoursesProvider,
- private domUtils: CoreDomUtilsProvider, private myOverviewProvider: CoreCoursesMyOverviewProvider) {}
+ private domUtils: CoreDomUtilsProvider, private myOverviewProvider: CoreCoursesMyOverviewProvider,
+ private courseHelper: CoreCourseHelperProvider) {}
/**
* View loaded.
*/
ionViewDidLoad() {
this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite();
-
- this.switchTab(this.tabShown);
-
- // @todo: Course download.
}
/**
@@ -145,6 +148,8 @@ export class CoreCoursesMyOverviewPage {
this.courses.filter = '';
this.showFilter = false;
this.filteredCourses = this.courses[this.courses.selected];
+
+ this.initPrefetchCoursesIcons();
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'Error getting my overview data.');
});
@@ -230,6 +235,7 @@ export class CoreCoursesMyOverviewPage {
}
break;
case 'courses':
+ this.prefetchIconsInitialized = false;
return this.fetchMyOverviewCourses();
}
}).finally(() => {
@@ -260,11 +266,11 @@ export class CoreCoursesMyOverviewPage {
}
/**
- * Change tab being viewed.
+ * The tab has changed.
*
- * @param {string} tab Tab to display.
+ * @param {string} tab Name of the new tab.
*/
- switchTab(tab: string) {
+ tabChanged(tab: string) {
this.tabShown = tab;
switch (this.tabShown) {
case 'timeline':
@@ -316,4 +322,65 @@ export class CoreCoursesMyOverviewPage {
selectedChanged() {
this.filteredCourses = this.courses[this.courses.selected];
}
+
+ /**
+ * Prefetch all the shown courses.
+ */
+ prefetchCourses() {
+ let selected = this.courses.selected,
+ selectedData = this.prefetchCoursesData[selected],
+ initialIcon = selectedData.icon;
+
+ selectedData.icon = 'spinner';
+ selectedData.badge = '';
+ return this.courseHelper.confirmAndPrefetchCourses(this.courses[selected], (progress) => {
+ selectedData.badge = progress.count + ' / ' + progress.total;
+ }).then((downloaded) => {
+ selectedData.icon = downloaded ? 'refresh' : initialIcon;
+ }, (error) => {
+ if (!this.isDestroyed) {
+ this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
+ selectedData.icon = initialIcon;
+ }
+ }).finally(() => {
+ selectedData.badge = '';
+ });
+ }
+
+ /**
+ * Initialize the prefetch icon for selected courses.
+ */
+ protected initPrefetchCoursesIcons() {
+ if (this.prefetchIconsInitialized) {
+ // Already initialized.
+ return;
+ }
+
+ this.prefetchIconsInitialized = true;
+
+ Object.keys(this.prefetchCoursesData).forEach((filter) => {
+ if (!this.courses[filter] || this.courses[filter].length < 2) {
+ // Not enough courses.
+ this.prefetchCoursesData[filter].icon = '';
+ return;
+ }
+
+ this.courseHelper.determineCoursesStatus(this.courses[filter]).then((status) => {
+ let icon = this.courseHelper.getCourseStatusIconFromStatus(status);
+ if (icon == 'spinner') {
+ // It seems all courses are being downloaded, show a download button instead.
+ icon = 'cloud-download';
+ }
+ this.prefetchCoursesData[filter].icon = icon;
+ });
+
+ });
+ }
+
+ /**
+ * Component being destroyed.
+ */
+ ngOnDestroy() {
+ this.isDestroyed = true;
+ }
}
diff --git a/src/core/courses/providers/courses.ts b/src/core/courses/providers/courses.ts
index 9e8e0623b..ebb15e8d8 100644
--- a/src/core/courses/providers/courses.ts
+++ b/src/core/courses/providers/courses.ts
@@ -28,6 +28,7 @@ export class CoreCoursesProvider {
public static EVENT_MY_COURSES_REFRESHED = 'courses_my_courses_refreshed';
public static ACCESS_GUEST = 'courses_access_guest';
public static ACCESS_DEFAULT = 'courses_access_default';
+ protected ROOT_CACHE_KEY = 'mmCourses:';
protected logger;
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider) {
@@ -68,7 +69,7 @@ export class CoreCoursesProvider {
* @return {string} Cache key.
*/
protected getCategoriesCacheKey(categoryId: number, addSubcategories?: boolean) : string {
- return this.getRootCacheKey() + 'categories:' + categoryId + ':' + !!addSubcategories;
+ return this.ROOT_CACHE_KEY + 'categories:' + categoryId + ':' + !!addSubcategories;
}
/**
@@ -121,15 +122,6 @@ export class CoreCoursesProvider {
});
}
- /**
- * Get the root cache key for the WS calls related to courses.
- *
- * @return {string} Root cache key.
- */
- protected getRootCacheKey() : string {
- return 'mmCourses:';
- }
-
/**
* Check if My Courses is disabled in a certain site.
*
@@ -219,7 +211,7 @@ export class CoreCoursesProvider {
* @return {string} Cache key.
*/
protected getCourseEnrolmentMethodsCacheKey(id: number) : string {
- return this.getRootCacheKey() + 'enrolmentmethods:' + id;
+ return this.ROOT_CACHE_KEY + 'enrolmentmethods:' + id;
}
/**
@@ -251,7 +243,7 @@ export class CoreCoursesProvider {
* @return {string} Cache key.
*/
protected getCourseGuestEnrolmentInfoCacheKey(instanceId: number) : string {
- return this.getRootCacheKey() + 'guestinfo:' + instanceId;
+ return this.ROOT_CACHE_KEY + 'guestinfo:' + instanceId;
}
/**
@@ -291,7 +283,7 @@ export class CoreCoursesProvider {
* @return {string} Cache key.
*/
protected getCoursesCacheKey(ids: number[]) : string {
- return this.getRootCacheKey() + 'course:' + JSON.stringify(ids);
+ return this.ROOT_CACHE_KEY + 'course:' + JSON.stringify(ids);
}
/**
@@ -352,7 +344,7 @@ export class CoreCoursesProvider {
protected getCoursesByFieldCacheKey(field?: string, value?: any) : string {
field = field || '';
value = field ? value : '';
- return this.getRootCacheKey() + 'coursesbyfield:' + field + ':' + value;
+ return this.ROOT_CACHE_KEY + 'coursesbyfield:' + field + ':' + value;
}
/**
@@ -408,7 +400,7 @@ export class CoreCoursesProvider {
* @return {string} Cache key.
*/
protected getUserAdministrationOptionsCommonCacheKey() : string {
- return this.getRootCacheKey() + 'administrationOptions:';
+ return this.ROOT_CACHE_KEY + 'administrationOptions:';
}
/**
@@ -451,7 +443,7 @@ export class CoreCoursesProvider {
* @return {string} Cache key.
*/
protected getUserNavigationOptionsCommonCacheKey() : string {
- return this.getRootCacheKey() + 'navigationOptions:';
+ return this.ROOT_CACHE_KEY + 'navigationOptions:';
}
/**
@@ -566,7 +558,7 @@ export class CoreCoursesProvider {
* @return {string} Cache key.
*/
protected getUserCoursesCacheKey() : string {
- return this.getRootCacheKey() + 'usercourses';
+ return this.ROOT_CACHE_KEY + 'usercourses';
}
/**
diff --git a/src/core/courses/providers/delegate.ts b/src/core/courses/providers/delegate.ts
index 68c11e4dd..577047f8e 100644
--- a/src/core/courses/providers/delegate.ts
+++ b/src/core/courses/providers/delegate.ts
@@ -19,33 +19,132 @@ import { CoreSitesProvider } from '../../../providers/sites';
import { CoreUtilsProvider, PromiseDefer } from '../../../providers/utils/utils';
import { CoreCoursesProvider } from './courses';
+/**
+ * Interface that all courses handlers must implement.
+ */
export interface CoreCoursesHandler {
- name: string; // Name of the handler.
- priority: number; // The highest priority is displayed first.
- isEnabled(): boolean|Promise; // Whether or not the handler is enabled on a site level.
- isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any) :
- boolean|Promise; // Whether the handler is enabled on a course level. For perfomance reasons, do NOT call
- // WebServices in here, call them in shouldDisplayForCourse.
- shouldDisplayForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any) :
- boolean|Promise; // Whether the handler should be displayed in a course. If not implemented, assume it's true.
- getDisplayData?(courseId: number): CoreCoursesHandlerData; // Returns the data needed to render the handler.
- invalidateEnabledForCourse?(courseId: number, navOptions?: any, admOptions?: any) : Promise; // Should invalidate data
- // to determine if handler is enabled for a certain course.
- prefetch?(course: any) : Promise; // Will be called when a course is downloaded, and it should prefetch all the data
- // to be able to see the addon in offline.
+ /**
+ * Name of the handler.
+ * @type {string}
+ */
+ name: string;
+
+ /**
+ * The highest priority is displayed first.
+ * @type {number}
+ */
+ priority: number;
+
+ /**
+ * Whether or not the handler is enabled on a site level.
+ *
+ * @return {boolean|Promise} True or promise resolved with true if enabled.
+ */
+ isEnabled(): boolean|Promise;
+
+ /**
+ * Whether or not the handler is enabled for a certain course.
+ * For perfomance reasons, do NOT call WebServices in here, call them in shouldDisplayForCourse.
+ *
+ * @param {number} courseId The course ID.
+ * @param {any} accessData Access type and data. Default, guest, ...
+ * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
+ * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
+ * @return {boolean|Promise} True or promise resolved with true if enabled.
+ */
+ isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any) : boolean|Promise;
+
+ /**
+ * Whether or not the handler should be displayed for a course. If not implemented, assume it's true.
+ *
+ * @param {number} courseId The course ID.
+ * @param {any} accessData Access type and data. Default, guest, ...
+ * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
+ * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
+ * @return {boolean|Promise} True or promise resolved with true if enabled.
+ */
+ shouldDisplayForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any) : boolean|Promise;
+
+ /**
+ * Returns the data needed to render the handler.
+ *
+ * @param {number} courseId The course ID.
+ * @return {CoreCoursesHandlerData} Data.
+ */
+ getDisplayData?(courseId: number): CoreCoursesHandlerData;
+
+ /**
+ * Should invalidate the data to determine if the handler is enabled for a certain course.
+ *
+ * @param {number} courseId The course ID.
+ * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
+ * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
+ * @return {Promise} Promise resolved when done.
+ */
+ invalidateEnabledForCourse?(courseId: number, navOptions?: any, admOptions?: any) : Promise;
+
+ /**
+ * Called when a course is downloaded. It should prefetch all the data to be able to see the addon in offline.
+ *
+ * @param {any} course The course.
+ * @return {Promise} Promise resolved when done.
+ */
+ prefetch?(course: any) : Promise;
};
+/**
+ * Data needed to render a course handler. It's returned by the handler.
+ */
export interface CoreCoursesHandlerData {
- title: string; // Title to display for the handler.
- icon: string; // Name of the icon to display for the handler.
- class?: string; // Class to add to the displayed handler.
- action(course: any): void; // Action to perform when the handler is clicked.
+ /**
+ * Title to display for the handler.
+ * @type {string}
+ */
+ title: string;
+
+ /**
+ * Name of the icon to display for the handler.
+ * @type {string}
+ */
+ icon: string;
+
+ /**
+ * Class to add to the displayed handler.
+ * @type {string}
+ */
+ class?: string;
+
+ /**
+ * Action to perform when the handler is clicked.
+ *
+ * @param {any} course The course.
+ */
+ action(course: any): void;
};
+/**
+ * Data returned by the delegate for each handler.
+ */
export interface CoreCoursesHandlerToDisplay {
- data: CoreCoursesHandlerData; // Data to display.
- priority?: number; // Handler's priority.
- prefetch?(course: any) : Promise; // Function to prefetch the handler.
+ /**
+ * Data to display.
+ * @type {CoreCoursesHandlerData}
+ */
+ data: CoreCoursesHandlerData;
+
+ /**
+ * The highest priority is displayed first.
+ * @type {number}
+ */
+ priority?: number;
+
+ /**
+ * Called when a course is downloaded. It should prefetch all the data to be able to see the addon in offline.
+ *
+ * @param {any} course The course.
+ * @return {Promise} Promise resolved when done.
+ */
+ prefetch?(course: any) : Promise;
};
/**
diff --git a/src/core/courses/providers/my-overview.ts b/src/core/courses/providers/my-overview.ts
index aab67f913..a6b785fde 100644
--- a/src/core/courses/providers/my-overview.ts
+++ b/src/core/courses/providers/my-overview.ts
@@ -24,6 +24,7 @@ import * as moment from 'moment';
export class CoreCoursesMyOverviewProvider {
public static EVENTS_LIMIT = 20;
public static EVENTS_LIMIT_PER_COURSE = 10;
+ protected ROOT_CACHE_KEY = 'myoverview:';
constructor(private sitesProvider: CoreSitesProvider) {}
@@ -113,7 +114,7 @@ export class CoreCoursesMyOverviewProvider {
* @return {string} Cache key.
*/
protected getActionEventsByCoursesCacheKey() : string {
- return this.getRootCacheKey() + 'bycourse';
+ return this.ROOT_CACHE_KEY + 'bycourse';
}
/**
@@ -165,7 +166,7 @@ export class CoreCoursesMyOverviewProvider {
* @return {string} Cache key.
*/
protected getActionEventsByTimesortPrefixCacheKey() : string {
- return this.getRootCacheKey() + 'bytimesort:';
+ return this.ROOT_CACHE_KEY + 'bytimesort:';
}
/**
@@ -181,15 +182,6 @@ export class CoreCoursesMyOverviewProvider {
return this.getActionEventsByTimesortPrefixCacheKey() + afterEventId + ':' + limit;
}
- /**
- * Get the root cache key for the WS calls related to overview.
- *
- * @return {string} Root cache key.
- */
- protected getRootCacheKey() : string {
- return 'myoverview:';
- }
-
/**
* Invalidates get calendar action events for a given list of courses WS call.
*
diff --git a/src/core/emulator/pages/capture-media/capture-media.ts b/src/core/emulator/pages/capture-media/capture-media.ts
index e94d15be5..353648b3e 100644
--- a/src/core/emulator/pages/capture-media/capture-media.ts
+++ b/src/core/emulator/pages/capture-media/capture-media.ts
@@ -354,7 +354,7 @@ export class CoreEmulatorCaptureMediaPage implements OnInit, OnDestroy {
// Create the file and return it.
let fileName = this.type + '_' + this.timeUtils.readableTimestamp() + '.' + this.extension,
- path = this.textUtils.concatenatePaths(this.fileProvider.TMPFOLDER, 'media/' + fileName);
+ path = this.textUtils.concatenatePaths(CoreFileProvider.TMPFOLDER, 'media/' + fileName);
let loadingModal = this.domUtils.showModalLoading();
diff --git a/src/core/emulator/providers/capture-helper.ts b/src/core/emulator/providers/capture-helper.ts
index 3d03605ba..d71a44690 100644
--- a/src/core/emulator/providers/capture-helper.ts
+++ b/src/core/emulator/providers/capture-helper.ts
@@ -44,11 +44,9 @@ export class CoreEmulatorCaptureHelperProvider {
/**
* Capture media (image, audio, video).
*
- * @param {String} type Type of media: image, audio, video.
- * @param {Function} successCallback Function called when media taken.
- * @param {Function} errorCallback Function called when error or cancel.
- * @param {Object} [options] Optional options.
- * @return {Void}
+ * @param {string} type Type of media: image, audio, video.
+ * @param {any} [options] Optional options.
+ * @return {Promise} Promise resolved when captured, rejected if error.
*/
captureMedia(type: string, options: any) : Promise {
options = options || {};
diff --git a/src/core/emulator/providers/local-notifications.ts b/src/core/emulator/providers/local-notifications.ts
index d7eef13db..2d8e27af1 100644
--- a/src/core/emulator/providers/local-notifications.ts
+++ b/src/core/emulator/providers/local-notifications.ts
@@ -466,7 +466,7 @@ export class LocalNotificationsMock extends LocalNotifications {
// Schedule the notification again unless it should have been triggered more than an hour ago.
delete notification.triggered;
notification.at = notification.at * 1000;
- if (notification.at - Date.now() > - CoreConstants.secondsHour * 1000) {
+ if (notification.at - Date.now() > - CoreConstants.SECONDS_HOUR * 1000) {
this.schedule(notification);
}
}
@@ -551,19 +551,19 @@ export class LocalNotificationsMock extends LocalNotifications {
} else if (every == 'second') {
interval = 1000;
} else if (every == 'minute') {
- interval = CoreConstants.secondsMinute * 1000;
+ interval = CoreConstants.SECONDS_MINUTE * 1000;
} else if (every == 'hour') {
- interval = CoreConstants.secondsHour * 1000;
+ interval = CoreConstants.SECONDS_HOUR * 1000;
} else if (every == 'day') {
- interval = CoreConstants.secondsDay * 1000;
+ interval = CoreConstants.SECONDS_DAY * 1000;
} else if (every == 'week') {
- interval = CoreConstants.secondsDay * 7 * 1000;
+ interval = CoreConstants.SECONDS_DAY * 7 * 1000;
} else if (every == 'month') {
- interval = CoreConstants.secondsDay * 31 * 1000;
+ interval = CoreConstants.SECONDS_DAY * 31 * 1000;
} else if (every == 'quarter') {
- interval = CoreConstants.secondsHour * 2190 * 1000;
+ interval = CoreConstants.SECONDS_HOUR * 2190 * 1000;
} else if (every == 'year') {
- interval = CoreConstants.secondsYear * 1000;
+ interval = CoreConstants.SECONDS_YEAR * 1000;
} else {
interval = parseInt(every, 10);
if (isNaN(interval)) {
@@ -713,7 +713,7 @@ export class LocalNotificationsMock extends LocalNotifications {
tag: notification.id + '',
template: this.tileTemplate,
strings: [notification.title, notification.text, notification.title, notification.text, notification.title, notification.text],
- expirationTime: new Date(Date.now() + CoreConstants.secondsHour * 1000) // Expire in 1 hour.
+ expirationTime: new Date(Date.now() + CoreConstants.SECONDS_HOUR * 1000) // Expire in 1 hour.
})
tileNotif.show()
diff --git a/src/core/fileuploader/providers/delegate.ts b/src/core/fileuploader/providers/delegate.ts
index 7cedfcb50..f266ea6e1 100644
--- a/src/core/fileuploader/providers/delegate.ts
+++ b/src/core/fileuploader/providers/delegate.ts
@@ -165,7 +165,7 @@ export class CoreFileUploaderDelegate {
protected lastUpdateHandlersStart: number;
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider) {
- this.logger = logger.getInstance('CoreCourseModuleDelegate');
+ this.logger = logger.getInstance('CoreFileUploaderDelegate');
eventsProvider.on(CoreEventsProvider.LOGIN, this.updateHandlers.bind(this));
eventsProvider.on(CoreEventsProvider.SITE_UPDATED, this.updateHandlers.bind(this));
diff --git a/src/core/fileuploader/providers/fileuploader.ts b/src/core/fileuploader/providers/fileuploader.ts
index 06c3716ba..0b1ba9093 100644
--- a/src/core/fileuploader/providers/fileuploader.ts
+++ b/src/core/fileuploader/providers/fileuploader.ts
@@ -27,10 +27,14 @@ import { CoreUtilsProvider } from '../../../providers/utils/utils';
import { CoreWSFileUploadOptions } from '../../../providers/ws';
/**
- * Interface for file upload options.
+ * File upload options.
*/
export interface CoreFileUploaderOptions extends CoreWSFileUploadOptions {
- deleteAfterUpload?: boolean; // Whether the file should be deleted after the upload (if success).
+ /**
+ * Whether the file should be deleted after the upload (if success).
+ * @type {boolean}
+ */
+ deleteAfterUpload?: boolean;
};
/**
diff --git a/src/core/fileuploader/providers/helper.ts b/src/core/fileuploader/providers/helper.ts
index 07936bb1d..438ee93c9 100644
--- a/src/core/fileuploader/providers/helper.ts
+++ b/src/core/fileuploader/providers/helper.ts
@@ -94,13 +94,13 @@ export class CoreFileUploaderHelperProvider {
fileData;
// We have the data of the file to be uploaded, but not its URL (needed). Create a copy of the file to upload it.
- return this.fileProvider.readFileData(file, this.fileProvider.FORMATARRAYBUFFER).then((data) => {
+ return this.fileProvider.readFileData(file, CoreFileProvider.FORMATARRAYBUFFER).then((data) => {
fileData = data;
// Get unique name for the copy.
- return this.fileProvider.getUniqueNameInFolder(this.fileProvider.TMPFOLDER, name);
+ return this.fileProvider.getUniqueNameInFolder(CoreFileProvider.TMPFOLDER, name);
}).then((newName) => {
- let filePath = this.textUtils.concatenatePaths(this.fileProvider.TMPFOLDER, newName);
+ let filePath = this.textUtils.concatenatePaths(CoreFileProvider.TMPFOLDER, newName);
return this.fileProvider.writeFile(filePath, fileData);
}).catch((error) => {
@@ -158,10 +158,10 @@ export class CoreFileUploaderHelperProvider {
fileName = fileName.replace(/(\.[^\.]*)\?[^\.]*$/, '$1');
// Get a unique name in the folder to prevent overriding another file.
- return this.fileProvider.getUniqueNameInFolder(this.fileProvider.TMPFOLDER, fileName, defaultExt);
+ return this.fileProvider.getUniqueNameInFolder(CoreFileProvider.TMPFOLDER, fileName, defaultExt);
}).then((newName) => {
// Now move or copy the file.
- const destPath = this.textUtils.concatenatePaths(this.fileProvider.TMPFOLDER, newName);
+ const destPath = this.textUtils.concatenatePaths(CoreFileProvider.TMPFOLDER, newName);
if (shouldDelete) {
return this.fileProvider.moveExternalFile(path, destPath);
} else {
diff --git a/src/core/login/pages/email-signup/email-signup.ts b/src/core/login/pages/email-signup/email-signup.ts
index 5311a4a47..86bca81d8 100644
--- a/src/core/login/pages/email-signup/email-signup.ts
+++ b/src/core/login/pages/email-signup/email-signup.ts
@@ -263,6 +263,6 @@ export class CoreLoginEmailSignupPage {
* Show authentication instructions.
*/
protected showAuthInstructions() {
- this.textUtils.expandText(this.translate.instant('core.login.instructions'), this.authInstructions, true);
+ this.textUtils.expandText(this.translate.instant('core.login.instructions'), this.authInstructions);
}
}
diff --git a/src/core/login/pages/init/init.ts b/src/core/login/pages/init/init.ts
index 7be53c668..7926f43f1 100644
--- a/src/core/login/pages/init/init.ts
+++ b/src/core/login/pages/init/init.ts
@@ -47,7 +47,7 @@ export class CoreLoginInitPage {
// Only accept the redirect if it was stored less than 20 seconds ago.
if (Date.now() - redirectData.timemodified < 20000) {
- if (redirectData.siteId != CoreConstants.noSiteId) {
+ if (redirectData.siteId != CoreConstants.NO_SITE_ID) {
// The redirect is pointing to a site, load it.
return this.sitesProvider.loadSite(redirectData.siteId).then(() => {
if (!this.loginHelper.isSiteLoggedOut(redirectData.page, redirectData.params)) {
diff --git a/src/core/login/providers/helper.ts b/src/core/login/providers/helper.ts
index 08caa097c..77faf9dbc 100644
--- a/src/core/login/providers/helper.ts
+++ b/src/core/login/providers/helper.ts
@@ -30,11 +30,38 @@ import { CoreConfigConstants } from '../../../configconstants';
import { CoreConstants } from '../../constants';
import { Md5 } from 'ts-md5/dist/md5';
+/**
+ * Data related to a SSO authentication.
+ */
export interface CoreLoginSSOData {
+ /**
+ * The site's URL.
+ * @type {string}
+ */
siteUrl?: string;
+
+ /**
+ * User's token.
+ * @type {string}
+ */
token?: string;
+
+ /**
+ * User's private token.
+ * @type {string}
+ */
privateToken?: string;
+
+ /**
+ * Name of the page to go after authenticated.
+ * @type {string}
+ */
pageName?: string;
+
+ /**
+ * Params to page to the page.
+ * @type {string}
+ */
pageParams?: any
};
@@ -187,7 +214,7 @@ export class CoreLoginHelperProvider {
* Show a confirm modal if needed and open a browser to perform SSO login.
*
* @param {string} siteurl URL of the site where the SSO login will be performed.
- * @param {number} typeOfLogin CoreConstants.loginSSOCode or CoreConstants.loginSSOInAppCode.
+ * @param {number} typeOfLogin CoreConstants.LOGIN_SSO_CODE or CoreConstants.LOGIN_SSO_INAPP_CODE.
* @param {string} [service] The service to use. If not defined, external service will be used.
* @param {string} [launchUrl] The URL to open for SSO. If not defined, local_mobile launch URL will be used.
* @return {Void}
@@ -594,7 +621,7 @@ export class CoreLoginHelperProvider {
return true;
}
- return code == CoreConstants.loginSSOInAppCode;
+ return code == CoreConstants.LOGIN_SSO_INAPP_CODE;
}
/**
@@ -604,7 +631,7 @@ export class CoreLoginHelperProvider {
* @return {boolean} True if SSO login is needed, false othwerise.
*/
isSSOLoginNeeded(code: number) : boolean {
- return code == CoreConstants.loginSSOCode || code == CoreConstants.loginSSOInAppCode;
+ return code == CoreConstants.LOGIN_SSO_CODE || code == CoreConstants.LOGIN_SSO_INAPP_CODE;
}
/**
@@ -615,7 +642,7 @@ export class CoreLoginHelperProvider {
* @param {string} siteId Site to load.
*/
protected loadSiteAndPage(page: string, params: any, siteId: string) : void {
- if (siteId == CoreConstants.noSiteId) {
+ if (siteId == CoreConstants.NO_SITE_ID) {
// Page doesn't belong to a site, just load the page.
this.appProvider.getRootNavController().setRoot(page, params);
} else {
@@ -687,7 +714,7 @@ export class CoreLoginHelperProvider {
* Open a browser to perform SSO login.
*
* @param {string} siteurl URL of the site where the SSO login will be performed.
- * @param {number} typeOfLogin CoreConstants.loginSSOCode or CoreConstants.loginSSOInAppCode.
+ * @param {number} typeOfLogin CoreConstants.LOGIN_SSO_CODE or CoreConstants.LOGIN_SSO_INAPP_CODE.
* @param {string} [service] The service to use. If not defined, external service will be used.
* @param {string} [launchUrl] The URL to open for SSO. If not defined, local_mobile launch URL will be used.
* @param {string} [pageName] Name of the page to go once authenticated. If not defined, site initial page.
@@ -791,7 +818,7 @@ export class CoreLoginHelperProvider {
// Store the siteurl and passport in $mmConfig for persistence. We are "configuring"
// the app to wait for an SSO. $mmConfig shouldn't be used as a temporary storage.
- this.configProvider.set(CoreConstants.loginLaunchData, JSON.stringify({
+ this.configProvider.set(CoreConstants.LOGIN_LAUNCH_DATA, JSON.stringify({
siteUrl: siteUrl,
passport: passport,
pageName: pageName || '',
@@ -934,7 +961,7 @@ export class CoreLoginHelperProvider {
/**
* Check if a confirm should be shown to open a SSO authentication.
*
- * @param {number} typeOfLogin CoreConstants.loginSSOCode or CoreConstants.loginSSOInAppCode.
+ * @param {number} typeOfLogin CoreConstants.LOGIN_SSO_CODE or CoreConstants.LOGIN_SSO_INAPP_CODE.
* @return {boolean} True if confirm modal should be shown, false otherwise.
*/
shouldShowSSOConfirm(typeOfLogin: number) : boolean {
@@ -988,7 +1015,7 @@ export class CoreLoginHelperProvider {
// Split signature:::token
const params = url.split(":::");
- return this.configProvider.get(CoreConstants.loginLaunchData).then((data): any => {
+ return this.configProvider.get(CoreConstants.LOGIN_LAUNCH_DATA).then((data): any => {
try {
data = JSON.parse(data);
} catch(ex) {
@@ -999,7 +1026,7 @@ export class CoreLoginHelperProvider {
passport = data.passport;
// Reset temporary values.
- this.configProvider.delete(CoreConstants.loginLaunchData);
+ this.configProvider.delete(CoreConstants.LOGIN_LAUNCH_DATA);
// Validate the signature.
// We need to check both http and https.
diff --git a/src/core/mainmenu/providers/delegate.ts b/src/core/mainmenu/providers/delegate.ts
index 5872e45f0..773615f58 100644
--- a/src/core/mainmenu/providers/delegate.ts
+++ b/src/core/mainmenu/providers/delegate.ts
@@ -18,18 +18,65 @@ import { CoreLoggerProvider } from '../../../providers/logger';
import { CoreSitesProvider } from '../../../providers/sites';
import { Subject, BehaviorSubject } from 'rxjs';
+/**
+ * Interface that all main menu handlers must implement.
+ */
export interface CoreMainMenuHandler {
- name: string; // Name of the handler.
- priority: number; // The highest priority is displayed first.
- isEnabled(): boolean|Promise; // Whether or not the handler is enabled on a site level.
- getDisplayData(): CoreMainMenuHandlerData; // Returns the data needed to render the handler.
+ /**
+ * Name of the handler.
+ * @type {string}
+ */
+ name: string;
+
+ /**
+ * The highest priority is displayed first.
+ * @type {number}
+ */
+ priority: number;
+
+ /**
+ * Whether or not the handler is enabled on a site level.
+ *
+ * @return {boolean|Promise} True or promise resolved with true if enabled.
+ */
+ isEnabled(): boolean|Promise;
+
+ /**
+ * Returns the data needed to render the handler.
+ *
+ * @param {number} courseId The course ID.
+ * @return {CoreMainMenuHandlerData} Data.
+ */
+ getDisplayData(): CoreMainMenuHandlerData;
};
+/**
+ * Data needed to render a main menu handler. It's returned by the handler.
+ */
export interface CoreMainMenuHandlerData {
- page: string; // Name of the page.
- title: string; // Title to display in the tab.
+ /**
+ * Name of the page to load for the handler.
+ * @type {string}
+ */
+ page: string;
+
+ /**
+ * Title to display for the handler.
+ * @type {string}
+ */
+ title: string;
+
+ /**
+ * Name of the icon to display for the handler.
+ * @type {string}
+ */
icon: string; // Name of the icon to display in the tab.
- class?: string; // Class to add to the displayed handler.
+
+ /**
+ * Class to add to the displayed handler.
+ * @type {string}
+ */
+ class?: string;
};
/**
diff --git a/src/core/mainmenu/providers/mainmenu.ts b/src/core/mainmenu/providers/mainmenu.ts
index 148e2473f..0f3bd8e71 100644
--- a/src/core/mainmenu/providers/mainmenu.ts
+++ b/src/core/mainmenu/providers/mainmenu.ts
@@ -17,10 +17,32 @@ import { CoreLangProvider } from '../../../providers/lang';
import { CoreSitesProvider } from '../../../providers/sites';
import { CoreConfigConstants } from '../../../configconstants';
+/**
+ * Custom main menu item.
+ */
export interface CoreMainMenuCustomItem {
+ /**
+ * Type of the item: app, inappbrowser, browser or embedded.
+ * @type {string}
+ */
type: string;
+
+ /**
+ * Url of the item.
+ * @type {string}
+ */
url: string;
+
+ /**
+ * Label to display for the item.
+ * @type {string}
+ */
label: string;
+
+ /**
+ * Name of the icon to display for the item.
+ * @type {string}
+ */
icon: string;
};
diff --git a/src/core/viewer/pages/image/image.html b/src/core/viewer/pages/image/image.html
new file mode 100644
index 000000000..d1eb33363
--- /dev/null
+++ b/src/core/viewer/pages/image/image.html
@@ -0,0 +1,14 @@
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
+
diff --git a/src/core/viewer/pages/image/image.module.ts b/src/core/viewer/pages/image/image.module.ts
new file mode 100644
index 000000000..62cd6dff0
--- /dev/null
+++ b/src/core/viewer/pages/image/image.module.ts
@@ -0,0 +1,31 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicPageModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreViewerImagePage } from './image';
+import { CoreDirectivesModule } from '../../../../directives/directives.module';
+
+@NgModule({
+ declarations: [
+ CoreViewerImagePage
+ ],
+ imports: [
+ CoreDirectivesModule,
+ IonicPageModule.forChild(CoreViewerImagePage),
+ TranslateModule.forChild()
+ ]
+})
+export class CoreViewerImagePageModule {}
diff --git a/src/core/viewer/pages/image/image.ts b/src/core/viewer/pages/image/image.ts
new file mode 100644
index 000000000..97eab737e
--- /dev/null
+++ b/src/core/viewer/pages/image/image.ts
@@ -0,0 +1,46 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component } from '@angular/core';
+import { TranslateService } from '@ngx-translate/core';
+import { IonicPage, ViewController, NavParams } from 'ionic-angular';
+
+/**
+ * Page to view an image. If opened as a modal, it will have a button to close the modal.
+ */
+@IonicPage({segment: 'core-viewer-image'})
+@Component({
+ selector: 'page-core-viewer-image',
+ templateUrl: 'image.html',
+})
+export class CoreViewerImagePage {
+ title: string; // Page title.
+ image: string; // Image URL.
+ component: string; // Component to use in external-content.
+ componentId: string|number; // Component ID to use in external-content.
+
+ constructor(private viewCtrl: ViewController, params: NavParams, translate: TranslateService) {
+ this.title = params.get('title') || translate.instant('core.imageviewer');
+ this.image = params.get('image');
+ this.component = params.get('component');
+ this.componentId = params.get('componentId');
+ }
+
+ /**
+ * Close modal.
+ */
+ closeModal() : void {
+ this.viewCtrl.dismiss();
+ }
+}
\ No newline at end of file
diff --git a/src/core/viewer/pages/text/text.html b/src/core/viewer/pages/text/text.html
index dfb15aacb..b00ee1f27 100644
--- a/src/core/viewer/pages/text/text.html
+++ b/src/core/viewer/pages/text/text.html
@@ -2,7 +2,7 @@
{{ title }}
-
+
diff --git a/src/core/viewer/pages/text/text.ts b/src/core/viewer/pages/text/text.ts
index dd0d6281d..e0647bf30 100644
--- a/src/core/viewer/pages/text/text.ts
+++ b/src/core/viewer/pages/text/text.ts
@@ -27,17 +27,14 @@ import { CoreTextUtilsProvider } from '../../../../providers/utils/text';
export class CoreViewerTextPage {
title: string; // Page title.
content: string; // Page content.
- isModal: boolean; // Whether it should be opened in a modal or in a page.
component: string; // Component to use in format-text.
componentId: string|number; // Component ID to use in format-text.
constructor(private viewCtrl: ViewController, params: NavParams, textUtils: CoreTextUtilsProvider) {
this.title = params.get('title');
this.content = params.get('content');
- this.isModal = params.get('isModal');
this.component = params.get('component');
this.componentId = params.get('componentId');
- // @todo: Use replacelinebreaks param like in Ionic 1? format-text should do it by default.
}
/**
diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts
index d71e2141b..da089bc25 100644
--- a/src/directives/format-text.ts
+++ b/src/directives/format-text.ts
@@ -51,7 +51,6 @@ export class CoreFormatTextDirective implements OnChanges {
// avoid this use class="inline" at the same time to use display: inline-block.
@Input() fullOnClick?: boolean|string; // Whether it should open a new page with the full contents on click. Only if
// "max-height" is set and the content has been collapsed.
- @Input() brOnFull?: boolean|string; // Whether new lines should be replaced by on full view.
@Input() fullTitle?: string; // Title to use in full view. Defaults to "Description".
@Output() afterRender?: EventEmitter; // Called when the data is rendered.
@@ -103,16 +102,14 @@ export class CoreFormatTextDirective implements OnChanges {
}
/**
- * Create a container for an image to adapt its width.
+ * Wrap an image with a container to adapt its width and, if needed, add an anchor to view it in full size.
*
* @param {number} elWidth Width of the directive's element.
* @param {HTMLElement} img Image to adapt.
- * @return {HTMLElement} Container.
*/
- protected createMagnifyingGlassContainer(elWidth: number, img: HTMLElement) : HTMLElement {
- // Check if image width has been adapted. If so, add an icon to view the image at full size.
+ protected adaptImage(elWidth: number, img: HTMLElement) : void {
let imgWidth = this.getElementWidth(img),
- // Wrap the image in a new div with position relative.
+ // Element to wrap the image.
container = document.createElement('span');
container.classList.add('core-adapted-img-container');
@@ -122,18 +119,38 @@ export class CoreFormatTextDirective implements OnChanges {
} else if (img.classList.contains('atto_image_button_left')) {
container.classList.add('atto_image_button_left');
}
- container.appendChild(img);
+
+ this.domUtils.wrapElement(img, container);
if (imgWidth > elWidth) {
- let imgSrc = this.textUtils.escapeHTML(img.getAttribute('src')),
- label = this.textUtils.escapeHTML(this.translate.instant('core.openfullimage'));
-
- // @todo: Implement image viewer. Maybe we can add the listener here directly?
- container.innerHTML += '';
+ // The image has been adapted, add an anchor to view it in full size.
+ this.addMagnifyingGlass(container, img);
}
+ }
- return container;
+ /**
+ * Add a magnifying glass icon to view an image at full size.
+ *
+ * @param {HTMLElement} container The container of the image.
+ * @param {HTMLElement} img The image.
+ */
+ addMagnifyingGlass(container: HTMLElement, img: HTMLElement) : void {
+ let imgSrc = this.textUtils.escapeHTML(img.getAttribute('src')),
+ label = this.textUtils.escapeHTML(this.translate.instant('core.openfullimage')),
+ anchor = document.createElement('a');
+
+ anchor.classList.add('core-image-viewer-icon');
+ anchor.setAttribute('aria-label', label);
+ // Add an ion-icon item to apply the right styles, but the ion-icon component won't be executed.
+ anchor.innerHTML = '';
+
+ anchor.addEventListener('click', (e: Event) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.domUtils.viewImage(imgSrc, img.getAttribute('alt'), this.component, this.componentId);
+ });
+
+ container.appendChild(anchor);
}
/**
@@ -159,8 +176,10 @@ export class CoreFormatTextDirective implements OnChanges {
this.text = this.text.trim();
this.formatContents().then((div: HTMLElement) => {
- this.element.innerHTML = ''; // Remove current contents.
+ // Disable media adapt to correctly calculate the height.
+ this.element.classList.add('core-disable-media-adapt');
+ this.element.innerHTML = ''; // Remove current contents.
if (this.maxHeight && div.innerHTML != "") {
// Move the children to the current element to be able to calculate the height.
// @todo: Display the element?
@@ -173,9 +192,12 @@ export class CoreFormatTextDirective implements OnChanges {
// If cannot calculate height, shorten always.
if (!height || height > this.maxHeight) {
- let expandInFullview = this.utils.isTrueOrOne(this.fullOnClick) || false;
+ let expandInFullview = this.utils.isTrueOrOne(this.fullOnClick) || false,
+ showMoreDiv = document.createElement('div');
- this.element.innerHTML += '
' + this.translate.instant('core.showmore') + '
';
+ showMoreDiv.classList.add('core-show-more');
+ showMoreDiv.innerHTML = this.translate.instant('core.showmore');
+ this.element.appendChild(showMoreDiv);
if (expandInFullview) {
this.element.classList.add('core-expand-in-fullview');
@@ -200,17 +222,15 @@ export class CoreFormatTextDirective implements OnChanges {
}
// Open a new state with the contents.
- // @todo: brOnFull is needed?
this.textUtils.expandText(this.fullTitle || this.translate.instant('core.description'), this.text,
- false, this.component, this.componentId);
+ this.component, this.componentId);
});
}
} else {
this.domUtils.moveChildren(div, this.element);
}
- this.element.classList.add('core-enabled-media-adapt');
-
+ this.element.classList.remove('core-disable-media-adapt');
this.finishRender();
});
}
@@ -233,7 +253,6 @@ export class CoreFormatTextDirective implements OnChanges {
// Apply format text function.
return this.textUtils.formatText(this.text, this.utils.isTrueOrOne(this.clean), this.utils.isTrueOrOne(this.singleLine));
}).then((formatted) => {
-
let div = document.createElement('div'),
canTreatVimeo = site && site.isVersionGreaterEqualThan(['3.3.4', '3.4']),
images,
@@ -271,9 +290,7 @@ export class CoreFormatTextDirective implements OnChanges {
this.addMediaAdaptClass(img);
this.addExternalContent(img);
if (this.utils.isTrueOrOne(this.adaptImg)) {
- // Create a container for the image and use it instead of the image.
- let container = this.createMagnifyingGlassContainer(elWidth, img);
- div.replaceChild(container, img);
+ this.adaptImage(elWidth, img);
}
});
}
@@ -345,16 +362,7 @@ export class CoreFormatTextDirective implements OnChanges {
* @return {number} The height of the element in pixels. When 0 is returned it means the element is not visible.
*/
protected getElementHeight(element: HTMLElement) : number {
- let height;
-
- // Disable media adapt to correctly calculate the height.
- element.classList.remove('core-enabled-media-adapt');
-
- height = this.domUtils.getElementHeight(element);
-
- element.classList.add('core-enabled-media-adapt');
-
- return height || 0;
+ return this.domUtils.getElementHeight(element) || 0;
}
/**
diff --git a/src/pipes/seconds-to-hms.ts b/src/pipes/seconds-to-hms.ts
index 8160522a2..34bd4d3cf 100644
--- a/src/pipes/seconds-to-hms.ts
+++ b/src/pipes/seconds-to-hms.ts
@@ -57,10 +57,10 @@ export class CoreSecondsToHMSPipe implements PipeTransform {
// Don't allow decimals.
seconds = Math.floor(seconds);
- hours = Math.floor(seconds / CoreConstants.secondsHour);
- seconds -= hours * CoreConstants.secondsHour;
- minutes = Math.floor(seconds / CoreConstants.secondsMinute);
- seconds -= minutes * CoreConstants.secondsMinute;
+ hours = Math.floor(seconds / CoreConstants.SECONDS_HOUR);
+ seconds -= hours * CoreConstants.SECONDS_HOUR;
+ minutes = Math.floor(seconds / CoreConstants.SECONDS_MINUTE);
+ seconds -= minutes * CoreConstants.SECONDS_MINUTE;
return this.textUtils.twoDigits(hours) + ':' + this.textUtils.twoDigits(minutes) + ':' + this.textUtils.twoDigits(seconds);
}
diff --git a/src/providers/app.ts b/src/providers/app.ts
index 7bc1f36be..3b74ca164 100644
--- a/src/providers/app.ts
+++ b/src/providers/app.ts
@@ -21,10 +21,32 @@ import { CoreDbProvider } from './db';
import { CoreLoggerProvider } from './logger';
import { SQLiteDB } from '../classes/sqlitedb';
+/**
+ * Data stored for a redirect to another page/site.
+ */
export interface CoreRedirectData {
+ /**
+ * ID of the site to load.
+ * @type {string}
+ */
siteId?: string;
- page?: string; // Name of the page to redirect.
- params?: any; // Params to pass to the page.
+
+ /**
+ * Name of the page to redirect to.
+ * @type {string}
+ */
+ page?: string;
+
+ /**
+ * Params to pass to the page.
+ * @type {any}
+ */
+ params?: any;
+
+ /**
+ * Timestamp when this redirect was last modified.
+ * @type {number}
+ */
timemodified?: number;
};
@@ -40,11 +62,11 @@ export interface CoreRedirectData {
*/
@Injectable()
export class CoreAppProvider {
- DBNAME = 'MoodleMobile';
- db: SQLiteDB;
- logger;
- ssoAuthenticationPromise : Promise;
- isKeyboardShown: boolean = false;
+ protected DBNAME = 'MoodleMobile';
+ protected db: SQLiteDB;
+ protected logger;
+ protected ssoAuthenticationPromise : Promise;
+ protected isKeyboardShown: boolean = false;
constructor(dbProvider: CoreDbProvider, private platform: Platform, private keyboard: Keyboard, private appCtrl: App,
private network: Network, logger: CoreLoggerProvider) {
diff --git a/src/providers/config.ts b/src/providers/config.ts
index 88cf788c9..7b9f4970e 100644
--- a/src/providers/config.ts
+++ b/src/providers/config.ts
@@ -22,9 +22,9 @@ import { SQLiteDB } from '../classes/sqlitedb';
*/
@Injectable()
export class CoreConfigProvider {
- appDB: SQLiteDB;
- TABLE_NAME = 'core_config';
- tableSchema = {
+ protected appDB: SQLiteDB;
+ protected TABLE_NAME = 'core_config';
+ protected tableSchema = {
name: this.TABLE_NAME,
columns: [
{
diff --git a/src/providers/cron.ts b/src/providers/cron.ts
index c6605700f..53cb47273 100644
--- a/src/providers/cron.ts
+++ b/src/providers/cron.ts
@@ -21,17 +21,64 @@ import { CoreUtilsProvider } from './utils/utils';
import { CoreConstants } from '../core/constants';
import { SQLiteDB } from '../classes/sqlitedb';
+/**
+ * Interface that all cron handlers must implement.
+ */
export interface CoreCronHandler {
- name: string; // Handler's name.
- getInterval?(): number; // Returns handler's interval in milliseconds. Defaults to CoreCronDelegate.DEFAULT_INTERVAL.
- usesNetwork?(): boolean; // Whether the process uses network or not. True if not defined.
- isSync?(): boolean; // Whether it's a synchronization process or not. True if not defined.
- canManualSync?(): boolean; // Whether the sync can be executed manually. Call isSync if not defined.
- execute?(siteId?: string): Promise; // Execute the process. Receives ID of site affected, undefined for all sites.
- // Important: If the promise is rejected then this function will be called again
- // often, it shouldn't be abused.
- running: boolean; // Whether the handler is running. Used internally by the provider, there's no need to set it.
- timeout: number; // Timeout ID for the handler scheduling. Used internally by the provider, there's no need to set it.
+ /**
+ * A name to identify the handler.
+ * @type {string}
+ */
+ name: string;
+
+ /**
+ * Returns handler's interval in milliseconds. Defaults to CoreCronDelegate.DEFAULT_INTERVAL.
+ *
+ * @return {number} Interval time (in milliseconds).
+ */
+ getInterval?(): number;
+
+ /**
+ * Check whether the process uses network or not. True if not defined.
+ *
+ * @return {boolean} Whether the process uses network or not
+ */
+ usesNetwork?(): boolean;
+
+ /**
+ * Check whether it's a synchronization process or not. True if not defined.
+ *
+ * @return {boolean} Whether it's a synchronization process or not.
+ */
+ isSync?(): boolean;
+
+ /**
+ * Check whether the sync can be executed manually. Call isSync if not defined.
+ *
+ * @return {boolean} Whether the sync can be executed manually.
+ */
+ canManualSync?(): boolean;
+
+ /**
+ * Execute the process.
+ *
+ * @param {string} [siteId] ID of the site affected. If not defined, all sites.
+ * @return {Promise} Promise resolved when done. If the promise is rejected, this function will be called again often,
+ * it shouldn't be abused.
+ */
+ execute?(siteId?: string): Promise;
+
+ /**
+ * Whether the handler is running. Used internally by the provider, there's no need to set it.
+ * @type {boolean}
+ */
+ running: boolean;
+
+ /**
+ * Timeout ID for the handler scheduling. Used internally by the provider, there's no need to set it.
+ * @type {number}
+ */
+ timeout: number;
};
/*
@@ -109,7 +156,7 @@ export class CoreCronDelegate {
if (isSync) {
// Check network connection.
- promise = this.configProvider.get(CoreConstants.settingsSyncOnlyOnWifi, false).then((syncOnlyOnWifi) => {
+ promise = this.configProvider.get(CoreConstants.SETTINGS_SYNC_ONLY_ON_WIFI, false).then((syncOnlyOnWifi) => {
return !syncOnlyOnWifi || !this.appProvider.isNetworkAccessLimited();
});
} else {
diff --git a/src/providers/db.ts b/src/providers/db.ts
index 0a463143e..a3c1f4aab 100644
--- a/src/providers/db.ts
+++ b/src/providers/db.ts
@@ -24,7 +24,7 @@ import { SQLiteDBMock } from '../core/emulator/classes/sqlitedb';
@Injectable()
export class CoreDbProvider {
- dbInstances = {};
+ protected dbInstances = {};
constructor(private sqlite: SQLite, private platform: Platform) {}
diff --git a/src/providers/events.ts b/src/providers/events.ts
index 198cb6a68..a9d538a25 100644
--- a/src/providers/events.ts
+++ b/src/providers/events.ts
@@ -16,8 +16,14 @@ import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
import { CoreLoggerProvider } from '../providers/logger';
+/**
+ * Observer instance to stop listening to an event.
+ */
export interface CoreEventObserver {
- off: () => void; // Unsubscribe.
+ /**
+ * Stop the observer.
+ */
+ off: () => void;
};
/*
@@ -39,6 +45,7 @@ export class CoreEventsProvider {
public static COMPLETION_MODULE_VIEWED = 'completion_module_viewed';
public static USER_DELETED = 'user_deleted';
public static PACKAGE_STATUS_CHANGED = 'package_status_changed';
+ public static COURSE_STATUS_CHANGED = 'course_status_changed';
public static SECTION_STATUS_CHANGED = 'section_status_changed';
public static REMOTE_ADDONS_LOADED = 'remote_addons_loaded';
public static LOGIN_SITE_CHECKED = 'login_site_checked';
@@ -48,9 +55,9 @@ export class CoreEventsProvider {
public static APP_LAUNCHED_URL = 'app_launched_url'; // App opened with a certain URL (custom URL scheme).
public static FILE_SHARED = 'file_shared';
- logger;
- observables: {[s: string] : Subject} = {};
- uniqueEvents = {};
+ protected logger;
+ protected observables: {[s: string] : Subject} = {};
+ protected uniqueEvents = {};
constructor(logger: CoreLoggerProvider) {
this.logger = logger.getInstance('CoreEventsProvider');
diff --git a/src/providers/file.ts b/src/providers/file.ts
index e8c693f8a..53d8d0d0a 100644
--- a/src/providers/file.ts
+++ b/src/providers/file.ts
@@ -27,20 +27,20 @@ import { Zip } from '@ionic-native/zip';
*/
@Injectable()
export class CoreFileProvider {
- logger;
- initialized = false;
- basePath = '';
- isHTMLAPI = false;
+ protected logger;
+ protected initialized = false;
+ protected basePath = '';
+ protected isHTMLAPI = false;
// Formats to read a file.
- FORMATTEXT = 0;
- FORMATDATAURL = 1;
- FORMATBINARYSTRING = 2;
- FORMATARRAYBUFFER = 3;
+ public static FORMATTEXT = 0;
+ public static FORMATDATAURL = 1;
+ public static FORMATBINARYSTRING = 2;
+ public static FORMATARRAYBUFFER = 3;
// Folders.
- SITESFOLDER = 'sites';
- TMPFOLDER = 'tmp';
+ public static SITESFOLDER = 'sites';
+ public static TMPFOLDER = 'tmp';
constructor(logger: CoreLoggerProvider, private platform: Platform, private file: File, private appProvider: CoreAppProvider,
private textUtils: CoreTextUtilsProvider, private zip: Zip, private mimeUtils: CoreMimetypeUtilsProvider) {
@@ -136,7 +136,7 @@ export class CoreFileProvider {
* @return {string} Site folder path.
*/
getSiteFolder(siteId: string) : string {
- return this.SITESFOLDER + '/' + siteId;
+ return CoreFileProvider.SITESFOLDER + '/' + siteId;
}
/**
@@ -380,17 +380,17 @@ export class CoreFileProvider {
* FORMATARRAYBUFFER
* @return {Promise} Promise to be resolved when the file is read.
*/
- readFile(path: string, format = this.FORMATTEXT) : Promise {
+ readFile(path: string, format = CoreFileProvider.FORMATTEXT) : Promise {
// Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, ''));
this.logger.debug('Read file ' + path + ' with format ' + format);
switch (format) {
- case this.FORMATDATAURL:
+ case CoreFileProvider.FORMATDATAURL:
return this.file.readAsDataURL(this.basePath, path);
- case this.FORMATBINARYSTRING:
+ case CoreFileProvider.FORMATBINARYSTRING:
return this.file.readAsBinaryString(this.basePath, path);
- case this.FORMATARRAYBUFFER:
+ case CoreFileProvider.FORMATARRAYBUFFER:
return this.file.readAsArrayBuffer(this.basePath, path);
default:
return this.file.readAsText(this.basePath, path);
@@ -408,8 +408,8 @@ export class CoreFileProvider {
* FORMATARRAYBUFFER
* @return {Promise} Promise to be resolved when the file is read.
*/
- readFileData(fileData: any, format = this.FORMATTEXT) : Promise {
- format = format || this.FORMATTEXT;
+ readFileData(fileData: any, format = CoreFileProvider.FORMATTEXT) : Promise {
+ format = format || CoreFileProvider.FORMATTEXT;
this.logger.debug('Read file from file data with format ' + format);
return new Promise((resolve, reject) => {
@@ -426,13 +426,13 @@ export class CoreFileProvider {
}
switch (format) {
- case this.FORMATDATAURL:
+ case CoreFileProvider.FORMATDATAURL:
reader.readAsDataURL(fileData);
break;
- case this.FORMATBINARYSTRING:
+ case CoreFileProvider.FORMATBINARYSTRING:
reader.readAsBinaryString(fileData);
break;
- case this.FORMATARRAYBUFFER:
+ case CoreFileProvider.FORMATARRAYBUFFER:
reader.readAsArrayBuffer(fileData);
break;
default:
@@ -883,7 +883,7 @@ export class CoreFileProvider {
* @return {Promise} Promise resolved when done.
*/
clearTmpFolder() : Promise {
- return this.removeDir(this.TMPFOLDER);
+ return this.removeDir(CoreFileProvider.TMPFOLDER);
}
/**
diff --git a/src/providers/filepool.ts b/src/providers/filepool.ts
index a576880ee..38f2ecb49 100644
--- a/src/providers/filepool.ts
+++ b/src/providers/filepool.ts
@@ -31,47 +31,199 @@ import { SQLiteDB } from '../classes/sqlitedb';
import { CoreConstants } from '../core/constants';
import { Md5 } from 'ts-md5/dist/md5';
-// Entry from filepool.
+/**
+ * Entry from filepool.
+ */
export interface CoreFilepoolFileEntry {
+ /**
+ * The fileId to identify the file.
+ * @type {string}
+ */
fileId?: string;
+
+ /**
+ * File's URL.
+ * @type {string}
+ */
url?: string;
+
+ /**
+ * File's revision.
+ * @type {number}
+ */
revision?: number;
+
+ /**
+ * File's timemodified.
+ * @type {number}
+ */
timemodified?: number;
- stale?: number; // 1 if stale, 0 otherwise.
+
+ /**
+ * 1 if file is stale (needs to be updated), 0 otherwise.
+ * @type {number}
+ */
+ stale?: number;
+
+ /**
+ * Timestamp when this file was downloaded.
+ * @type {number}
+ */
downloadTime?: number;
- isexternalfile?: number; // 1 if external, 0 otherwise.
+
+ /**
+ * 1 if it's a external file (from an external repository), 0 otherwise.
+ * @type {number}
+ */
+ isexternalfile?: number;
+
+ /**
+ * Type of the repository this file belongs to.
+ * @type {string}
+ */
repositorytype?: string;
+
+ /**
+ * File's path.
+ * @type {string}
+ */
path?: string;
+
+ /**
+ * File's extension.
+ * @type {string}
+ */
extension?: string;
};
-// Entry from files queue.
+/**
+ * Entry from the file's queue.
+ */
export interface CoreFilepoolQueueEntry {
+ /**
+ * The site the file belongs to.
+ * @type {string}
+ */
siteId?: string;
+
+ /**
+ * The fileId to identify the file.
+ * @type {string}
+ */
fileId?: string;
+
+ /**
+ * Timestamp when the file was added to the queue.
+ * @type {number}
+ */
added?: number;
+
+ /**
+ * The priority of the file.
+ * @type {number}
+ */
priority?: number;
+
+ /**
+ * File's URL.
+ * @type {string}
+ */
url?: string;
+
+ /**
+ * File's revision.
+ * @type {number}
+ */
revision?: number;
+
+ /**
+ * File's timemodified.
+ * @type {number}
+ */
timemodified?: number;
- isexternalfile?: number; // 1 if external, 0 otherwise.
+
+ /**
+ * 1 if it's a external file (from an external repository), 0 otherwise.
+ * @type {number}
+ */
+ isexternalfile?: number;
+
+ /**
+ * Type of the repository this file belongs to.
+ * @type {string}
+ */
repositorytype?: string;
+
+ /**
+ * File's path.
+ * @type {string}
+ */
path?: string;
+
+ /**
+ * File links (to link the file to components and componentIds).
+ * @type {any[]}
+ */
links?: any[];
};
-// Entry from packages table.
+/**
+ * Entry from packages table.
+ */
export interface CoreFilepoolPackageEntry {
+ /**
+ * Package id.
+ * @type {string}
+ */
id?: string;
+
+ /**
+ * The component to link the files to.
+ * @type {string}
+ */
component?: string;
+
+ /**
+ * An ID to use in conjunction with the component.
+ * @type {string|number}
+ */
componentId?: string|number;
+
+ /**
+ * Package status.
+ * @type {string}
+ */
status?: string;
+
+ /**
+ * Package previous status.
+ * @type {string}
+ */
previous?: string;
- revision?: string;
- timemodified?: number;
+
+ /**
+ * Timestamp when this package was updated.
+ * @type {number}
+ */
updated?: number;
+
+ /**
+ * Timestamp when this package was downloaded.
+ * @type {number}
+ */
downloadTime?: number;
+
+ /**
+ * Previous download time.
+ * @type {number}
+ */
previousDownloadTime?: number;
+
+ /**
+ * Extra data stored by the package.
+ * @type {string}
+ */
+ extra?: string;
};
/*
@@ -243,14 +395,6 @@ export class CoreFilepoolProvider {
name: 'previous',
type: 'TEXT'
},
- {
- name: 'revision',
- type: 'TEXT'
- },
- {
- name: 'timemodified',
- type: 'INTEGER'
- },
{
name: 'updated',
type: 'INTEGER'
@@ -262,6 +406,10 @@ export class CoreFilepoolProvider {
{
name: 'previousDownloadTime',
type: 'INTEGER'
+ },
+ {
+ name: 'extra',
+ type: 'TEXT'
}
]
},
@@ -384,7 +532,7 @@ export class CoreFilepoolProvider {
*
* @param {string} siteId The site ID.
* @param {string} fileId The file ID.
- * @param {Object} data Additional information to store about the file (timemodified, url, ...). See mmFilepoolStore schema.
+ * @param {any} data Additional information to store about the file (timemodified, url, ...). See FILES_TABLE schema.
* @return {Promise} Promise resolved on success.
*/
protected addFileToPool(siteId: string, fileId: string, data: any) : Promise {
@@ -566,7 +714,7 @@ export class CoreFilepoolProvider {
* @param {boolean} [checkSize=true] True if we shouldn't download files if their size is big, false otherwise.
* @param {boolean} [downloadUnknown] True to download file in WiFi if their size is unknown, false otherwise.
* Ignored if checkSize=false.
- * @param {Object} [options] Extra options (isexternalfile, repositorytype).
+ * @param {any} [options] Extra options (isexternalfile, repositorytype).
* @return {Promise} Promise resolved when the file is downloaded.
*/
protected addToQueueIfNeeded(siteId: string, fileUrl: string, component: string, componentId?: string|number, timemodified = 0,
@@ -648,7 +796,7 @@ export class CoreFilepoolProvider {
return site.getDb().deleteRecords(this.PACKAGES_TABLE).then(() => {
entries.forEach((entry) => {
// Trigger module status changed, setting it as not downloaded.
- this.triggerPackageStatusChanged(siteId, CoreConstants.notDownloaded, entry.component, entry.componentId);
+ this.triggerPackageStatusChanged(siteId, CoreConstants.NOT_DOWNLOADED, entry.component, entry.componentId);
});
});
});
@@ -693,13 +841,13 @@ export class CoreFilepoolProvider {
/**
* Given the current status of a list of packages and the status of one of the packages,
* determine the new status for the list of packages. The status of a list of packages is:
- * - CoreConstants.nowDownloadable if there are no downloadable packages.
- * - CoreConstants.notDownloaded if at least 1 package has status CoreConstants.notDownloaded.
- * - CoreConstants.downloaded if ALL the downloadable packages have status CoreConstants.downloaded.
- * - CoreConstants.downloading if ALL the downloadable packages have status CoreConstants.downloading or
- * CoreConstants.downloaded, with at least 1 package with CoreConstants.downloading.
- * - CoreConstants.outdated if ALL the downloadable packages have status CoreConstants.outdated or CoreConstants.downloaded
- * or CoreConstants.downloading, with at least 1 package with CoreConstants.outdated.
+ * - CoreConstants.NOT_DOWNLOADABLE if there are no downloadable packages.
+ * - CoreConstants.NOT_DOWNLOADED if at least 1 package has status CoreConstants.NOT_DOWNLOADED.
+ * - CoreConstants.DOWNLOADED if ALL the downloadable packages have status CoreConstants.DOWNLOADED.
+ * - CoreConstants.DOWNLOADING if ALL the downloadable packages have status CoreConstants.DOWNLOADING or
+ * CoreConstants.DOWNLOADED, with at least 1 package with CoreConstants.DOWNLOADING.
+ * - CoreConstants.OUTDATED if ALL the downloadable packages have status CoreConstants.OUTDATED or CoreConstants.DOWNLOADED
+ * or CoreConstants.DOWNLOADING, with at least 1 package with CoreConstants.OUTDATED.
*
* @param {string} current Current status of the list of packages.
* @param {string} packagestatus Status of one of the packages.
@@ -707,22 +855,22 @@ export class CoreFilepoolProvider {
*/
determinePackagesStatus(current: string, packageStatus: string) : string {
if (!current) {
- current = CoreConstants.notDownloadable;
+ current = CoreConstants.NOT_DOWNLOADABLE;
}
- if (packageStatus === CoreConstants.notDownloaded) {
+ if (packageStatus === CoreConstants.NOT_DOWNLOADED) {
// If 1 package is not downloaded the status of the whole list will always be not downloaded.
- return CoreConstants.notDownloaded;
- } else if (packageStatus === CoreConstants.downloaded && current === CoreConstants.notDownloadable) {
+ return CoreConstants.NOT_DOWNLOADED;
+ } else if (packageStatus === CoreConstants.DOWNLOADED && current === CoreConstants.NOT_DOWNLOADABLE) {
// If all packages are downloaded or not downloadable with at least 1 downloaded, status will be downloaded.
- return CoreConstants.downloaded;
- } else if (packageStatus === CoreConstants.downloading &&
- (current === CoreConstants.notDownloadable || current === CoreConstants.downloaded)) {
+ return CoreConstants.DOWNLOADED;
+ } else if (packageStatus === CoreConstants.DOWNLOADING &&
+ (current === CoreConstants.NOT_DOWNLOADABLE || current === CoreConstants.DOWNLOADED)) {
// If all packages are downloading/downloaded/notdownloadable with at least 1 downloading, status will be downloading.
- return CoreConstants.downloading;
- } else if (packageStatus === CoreConstants.outdated && current !== CoreConstants.notDownloaded) {
+ return CoreConstants.DOWNLOADING;
+ } else if (packageStatus === CoreConstants.OUTDATED && current !== CoreConstants.NOT_DOWNLOADED) {
// If there are no packages notdownloaded and there is at least 1 outdated, status will be outdated.
- return CoreConstants.outdated;
+ return CoreConstants.OUTDATED;
}
// Status remains the same.
@@ -833,23 +981,21 @@ export class CoreFilepoolProvider {
}
/**
- * Downloads or prefetches a list of files.
+ * Downloads or prefetches a list of files as a "package".
*
* @param {string} siteId The site ID.
* @param {any[]} fileList List of files to download.
* @param {boolean} [prefetch] True if should prefetch the contents (queue), false if they should be downloaded right now.
* @param {string} [component] The component to link the file to.
* @param {string|number} [componentId] An ID to use in conjunction with the component.
- * @param {string} [revision] Package's revision. If not defined, it will be calculated using the list of files.
- * @param {number} [timemod] Package's timemodified. If not defined, it will be calculated using the list of files.
+ * @param {string} [extra] Extra data to store for the package.
* @param {string} [dirPath] Name of the directory where to store the files (inside filepool dir). If not defined, store
* the files directly inside the filepool folder.
* @param {Function} [onProgress] Function to call on progress.
* @return {Promise} Promise resolved when the package is downloaded.
*/
protected downloadOrPrefetchPackage(siteId: string, fileList: any[], prefetch?: boolean, component?: string,
- componentId?: string|number, revision?: string, timemodified?: number, dirPath?: string,
- onProgress?: (event: any) => any) : Promise {
+ componentId?: string|number, extra?: string, dirPath?: string, onProgress?: (event: any) => any) : Promise {
let packageId = this.getPackageId(component, componentId),
promise;
@@ -861,11 +1007,8 @@ export class CoreFilepoolProvider {
this.packagesPromises[siteId] = {};
}
- revision = revision || String(this.getRevisionFromFileList(fileList));
- timemodified = timemodified || this.getTimemodifiedFromFileList(fileList);
-
// Set package as downloading.
- promise = this.storePackageStatus(siteId, CoreConstants.downloading, component, componentId).then(() => {
+ promise = this.storePackageStatus(siteId, CoreConstants.DOWNLOADING, component, componentId).then(() => {
let promises = [],
packageLoaded = 0;
@@ -919,7 +1062,7 @@ export class CoreFilepoolProvider {
return Promise.all(promises).then(() => {
// Success prefetching, store package as downloaded.
- return this.storePackageStatus(siteId, CoreConstants.downloaded, component, componentId, revision, timemodified);
+ return this.storePackageStatus(siteId, CoreConstants.DOWNLOADED, component, componentId, extra);
}).catch(() => {
// Error downloading, go back to previous status and reject the promise.
return this.setPackagePreviousStatus(siteId, component, componentId).then(() => {
@@ -943,17 +1086,15 @@ export class CoreFilepoolProvider {
* @param {any[]} fileList List of files to download.
* @param {string} component The component to link the file to.
* @param {string|number} [componentId] An ID to identify the download.
- * @param {string} [revision] Package's revision. If not defined, it will be calculated using the list of files.
- * @param {number} [timemodified] Package's timemodified. If not defined, it will be calculated using the list of files.
+ * @param {string} [extra] Extra data to store for the package.
* @param {string} [dirPath] Name of the directory where to store the files (inside filepool dir). If not defined, store
* the files directly inside the filepool folder.
* @param {Function} [onProgress] Function to call on progress.
* @return {Promise} Promise resolved when all files are downloaded.
*/
- downloadPackage(siteId: string, fileList: any[], component: string, componentId?: string|number, revision?: string,
- timemodified?: number, dirPath?: string, onProgress?: (event: any) => any) : Promise {
- return this.downloadOrPrefetchPackage(
- siteId, fileList, false, component, componentId, revision, timemodified, dirPath, onProgress);
+ downloadPackage(siteId: string, fileList: any[], component: string, componentId?: string|number, extra?: string,
+ dirPath?: string, onProgress?: (event: any) => any) : Promise {
+ return this.downloadOrPrefetchPackage(siteId, fileList, false, component, componentId, extra, dirPath, onProgress);
}
/**
@@ -1391,7 +1532,7 @@ export class CoreFilepoolProvider {
// Check if the file is in queue (waiting to be downloaded).
return this.hasFileInQueue(siteId, fileId).then(() => {
- return CoreConstants.downloading;
+ return CoreConstants.DOWNLOADING;
}).catch(() => {
// Check if the file is being downloaded right now.
let extension = this.mimeUtils.guessExtensionFromUrl(fileUrl),
@@ -1400,18 +1541,18 @@ export class CoreFilepoolProvider {
return Promise.resolve(path).then((filePath) => {
const downloadId = this.getFileDownloadId(fileUrl, filePath);
if (this.filePromises[siteId] && this.filePromises[siteId][downloadId]) {
- return CoreConstants.downloading;
+ return CoreConstants.DOWNLOADING;
}
// File is not being downloaded. Check if it's downloaded and if it's outdated.
return this.hasFileInPool(siteId, fileId).then((entry) => {
if (this.isFileOutdated(entry, revision, timemodified)) {
- return CoreConstants.outdated;
+ return CoreConstants.OUTDATED;
} else {
- return CoreConstants.downloaded;
+ return CoreConstants.DOWNLOADED;
}
}).catch(() => {
- return CoreConstants.notDownloaded;
+ return CoreConstants.NOT_DOWNLOADED;
});
});
});
@@ -1430,7 +1571,7 @@ export class CoreFilepoolProvider {
* @param {boolean} [checkSize=true] True if we shouldn't download files if their size is big, false otherwise.
* @param {boolean} [downloadUnknown] True to download file in WiFi if their size is unknown, false otherwise.
* Ignored if checkSize=false.
- * @param {Object} [options] Extra options (isexternalfile, repositorytype).
+ * @param {any} [options] Extra options (isexternalfile, repositorytype).
* @return {Promise} Resolved with the URL to use.
* @description
* This will return a URL pointing to the content of the requested URL.
@@ -1574,22 +1715,6 @@ export class CoreFilepoolProvider {
return Promise.reject(null);
}
- /**
- * Get a package current status.
- *
- * @param {string} siteId Site ID.
- * @param {string} component Package's component.
- * @param {string|number} [componentId] An ID to use in conjunction with the component.
- * @return {Promise} Promise resolved with the status.
- */
- getPackageCurrentStatus(siteId: string, component: string, componentId?: string|number) : Promise {
- return this.getPackageData(siteId, component, componentId).then((entry) => {
- return entry.status || CoreConstants.notDownloaded;
- }).catch(() => {
- return CoreConstants.notDownloaded;
- });
- }
-
/**
* Get the data stored for a package.
*
@@ -1683,6 +1808,19 @@ export class CoreFilepoolProvider {
return this.packagesPromises[siteId][packageId];
}
}
+ /**
+ * Get a package extra data.
+ *
+ * @param {string} siteId Site ID.
+ * @param {string} component Package's component.
+ * @param {string|number} [componentId] An ID to use in conjunction with the component.
+ * @return {Promise} Promise resolved with the extra data.
+ */
+ getPackageExtra(siteId: string, component: string, componentId?: string|number) : Promise {
+ return this.getPackageData(siteId, component, componentId).then((entry) => {
+ return entry.extra;
+ });
+ }
/**
* Get the ID of a package.
@@ -1705,23 +1843,9 @@ export class CoreFilepoolProvider {
*/
getPackagePreviousStatus(siteId: string, component: string, componentId?: string|number) : Promise {
return this.getPackageData(siteId, component, componentId).then((entry) => {
- return entry.previous || CoreConstants.notDownloaded;
+ return entry.previous || CoreConstants.NOT_DOWNLOADED;
}).catch(() => {
- return CoreConstants.notDownloaded;
- });
- }
-
- /**
- * Get a package revision.
- *
- * @param {string} siteId Site ID.
- * @param {string} component Package's component.
- * @param {string|number} [componentId] An ID to use in conjunction with the component.
- * @return {Promise} Promise resolved with the revision.
- */
- getPackageRevision(siteId: string, component: string, componentId?: string|number) : Promise {
- return this.getPackageData(siteId, component, componentId).then((entry) => {
- return entry.revision;
+ return CoreConstants.NOT_DOWNLOADED;
});
}
@@ -1731,65 +1855,13 @@ export class CoreFilepoolProvider {
* @param {string} siteId Site ID.
* @param {string} component Package's component.
* @param {string|number} [componentId] An ID to use in conjunction with the component.
- * @param {string} [revision='0'] Package's revision.
- * @param {number} [timemodified=0] Package's time modified.
* @return {Promise} Promise resolved with the status.
*/
- getPackageStatus(siteId: string, component: string, componentId?: string|number, revision = '0', timemodified = 0)
- : Promise {
- componentId = this.fixComponentId(componentId);
-
- return this.sitesProvider.getSite(siteId).then((site) => {
- const packageId = this.getPackageId(component, componentId),
- conditions = {id: packageId};
-
- // Get status.
- return site.getDb().getRecord(this.PACKAGES_TABLE, conditions).then((entry: CoreFilepoolPackageEntry) => {
- if (entry.status === CoreConstants.downloaded) {
- if (revision != entry.revision || timemodified > entry.timemodified) {
- // File is outdated. Let's change its status.
- let newData: CoreFilepoolPackageEntry = {
- status: CoreConstants.outdated,
- updated: Date.now()
- };
- site.getDb().updateRecords(this.PACKAGES_TABLE, newData, conditions).then(() => {
- // Success inserting, trigger event.
- this.triggerPackageStatusChanged(siteId, CoreConstants.outdated, component, componentId);
- });
- }
- } else if (entry.status === CoreConstants.outdated) {
- if (revision === entry.revision && timemodified === entry.timemodified) {
- // File isn't outdated anymore. Let's change its status.
- let newData: CoreFilepoolPackageEntry = {
- status: CoreConstants.downloaded,
- updated: Date.now()
- };
- site.getDb().updateRecords(this.PACKAGES_TABLE, newData, conditions).then(() => {
- // Success inserting, trigger event.
- this.triggerPackageStatusChanged(siteId, CoreConstants.downloaded, component, componentId);
- });
- }
- }
- return entry.status;
- }, () => {
- return CoreConstants.notDownloaded;
- });
- });
- }
-
- /**
- * Get a package timemodified.
- *
- * @param {string} siteId Site ID.
- * @param {string} component Package's component.
- * @param {string|number} [componentId] An ID to use in conjunction with the component.
- * @return {Promise} Promise resolved with the time modified.
- */
- getPackageTimemodified(siteId: string, component: string, componentId?: string|number) : Promise {
+ getPackageStatus(siteId: string, component: string, componentId?: string|number) : Promise {
return this.getPackageData(siteId, component, componentId).then((entry) => {
- return entry.timemodified;
+ return entry.status || CoreConstants.NOT_DOWNLOADED;
}).catch(() => {
- return -1;
+ return CoreConstants.NOT_DOWNLOADED;
});
}
@@ -1874,10 +1946,10 @@ export class CoreFilepoolProvider {
}
/**
- * Get package revision number from a list of files.
+ * Get a revision number from a list of files (highest revision).
*
* @param {any[]} files Package files.
- * @return {number} Package revision.
+ * @return {number} Highest revision.
*/
getRevisionFromFileList(files: any[]) : number {
let revision = 0;
@@ -1931,7 +2003,7 @@ export class CoreFilepoolProvider {
* @param {boolean} [checkSize=true] True if we shouldn't download files if their size is big, false otherwise.
* @param {boolean} [downloadUnknown] True to download file in WiFi if their size is unknown, false otherwise.
* Ignored if checkSize=false.
- * @param {Object} [options] Extra options (isexternalfile, repositorytype).
+ * @param {any} [options] Extra options (isexternalfile, repositorytype).
* @return {Promise} Resolved with the URL to use.
* @description
* This will return a URL pointing to the content of the requested URL.
@@ -1947,7 +2019,7 @@ export class CoreFilepoolProvider {
* Get time modified from a list of files.
*
* @param {any[]} files List of files.
- * @return {number} Rime modified.
+ * @return {number} Time modified.
*/
getTimemodifiedFromFileList(files: any[]) : number {
let timemodified = 0;
@@ -1973,7 +2045,7 @@ export class CoreFilepoolProvider {
* @param {boolean} [checkSize=true] True if we shouldn't download files if their size is big, false otherwise.
* @param {boolean} [downloadUnknown] True to download file in WiFi if their size is unknown, false otherwise.
* Ignored if checkSize=false.
- * @param {Object} [options] Extra options (isexternalfile, repositorytype).
+ * @param {any} [options] Extra options (isexternalfile, repositorytype).
* @return {Promise} Resolved with the URL to use.
* @description
* This will return a URL pointing to the content of the requested URL.
@@ -2225,17 +2297,15 @@ export class CoreFilepoolProvider {
* @param {any[]} fileList List of files to download.
* @param {string} component The component to link the file to.
* @param {string|number} [componentId] An ID to identify the download.
- * @param {string} [revision] Package's revision. If not defined, it will be calculated using the list of files.
- * @param {number} [timemodified] Package's time modified. If not defined, it will be calculated using the list of files.
+ * @param {string} [extra] Extra data to store for the package.
* @param {string} [dirPath] Name of the directory where to store the files (inside filepool dir). If not defined, store
* the files directly inside the filepool folder.
* @param {Function} [onProgress] Function to call on progress.
* @return {Promise} Promise resolved when all files are downloaded.
*/
- prefetchPackage(siteId: string, fileList: any[], component: string, componentId?: string|number, revision?: string,
- timemodified?: number, dirPath?: string, onProgress?: (event: any) => any) : Promise {
- return this.downloadOrPrefetchPackage(
- siteId, fileList, true, component, componentId, revision, timemodified, dirPath, onProgress);
+ prefetchPackage(siteId: string, fileList: any[], component: string, componentId?: string|number, extra?: string,
+ dirPath?: string, onProgress?: (event: any) => any) : Promise {
+ return this.downloadOrPrefetchPackage(siteId, fileList, true, component, componentId, extra, dirPath, onProgress);
}
/**
@@ -2511,11 +2581,11 @@ export class CoreFilepoolProvider {
// Get current stored data, we'll only update 'status' and 'updated' fields.
return site.getDb().getRecord(this.PACKAGES_TABLE, {id: packageId}).then((entry: CoreFilepoolPackageEntry) => {
let newData: CoreFilepoolPackageEntry = {};
- if (entry.status == CoreConstants.downloading) {
+ if (entry.status == CoreConstants.DOWNLOADING) {
// Going back from downloading to previous status, restore previous download time.
newData.downloadTime = entry.previousDownloadTime;
}
- newData.status = entry.previous || CoreConstants.downloaded;
+ newData.status = entry.previous || CoreConstants.DOWNLOADED;
newData.updated = Date.now();
this.logger.debug(`Set previous status '${entry.status}' for package ${component} ${componentId}`);
@@ -2538,7 +2608,7 @@ export class CoreFilepoolProvider {
* Convenience function to check if a file should be downloaded before opening it.
*
* The default behaviour in the app is to download first and then open the local file in the following cases:
- * - The file is small (less than mmFilepoolDownloadThreshold).
+ * - The file is small (less than DOWNLOAD_THRESHOLD).
* - The file cannot be streamed.
* If the file is big and can be streamed, the promise returned by this function will be rejected.
*/
@@ -2568,12 +2638,11 @@ export class CoreFilepoolProvider {
* @param {string} status New package status.
* @param {string} component Package's component.
* @param {string|number} [componentId] An ID to use in conjunction with the component.
- * @param {string} [revision] Package's revision. If not provided, try to use the current value.
- * @param {number} [timemodified] Package's time modified. If not provided, try to use the current value.
+ * @param {string} [extra] Extra data to store for the package. If you want to store more than 1 value, use JSON.stringify.
* @return {Promise} Promise resolved when status is stored.
*/
- storePackageStatus(siteId: string, status: string, component: string, componentId?: string|number, revision?: string,
- timemodified?: number) : Promise {
+ storePackageStatus(siteId: string, status: string, component: string, componentId?: string|number, extra?: string)
+ : Promise {
this.logger.debug(`Set status '${status}'' for package ${component} ${componentId}`);
componentId = this.fixComponentId(componentId);
@@ -2582,18 +2651,15 @@ export class CoreFilepoolProvider {
downloadTime,
previousDownloadTime;
- if (status == CoreConstants.downloading) {
+ if (status == CoreConstants.DOWNLOADING) {
// Set download time if package is now downloading.
downloadTime = this.timeUtils.timestamp();
}
// Search current status to set it as previous status.
return site.getDb().getRecord(this.PACKAGES_TABLE, {id: packageId}).then((entry: CoreFilepoolPackageEntry) => {
- if (typeof revision == 'undefined' || revision === null) {
- revision = entry.revision;
- }
- if (typeof timemodified == 'undefined' || timemodified === null) {
- timemodified = entry.timemodified;
+ if (typeof extra == 'undefined' || extra === null) {
+ extra = entry.extra;
}
if (typeof downloadTime == 'undefined') {
// Keep previous download time.
@@ -2614,11 +2680,10 @@ export class CoreFilepoolProvider {
componentId: componentId,
status: status,
previous: previousStatus,
- revision: revision || '0',
- timemodified: timemodified || 0,
updated: Date.now(),
downloadTime: downloadTime,
- previousDownloadTime: previousDownloadTime
+ previousDownloadTime: previousDownloadTime,
+ extra: extra
},
promise;
diff --git a/src/providers/groups.ts b/src/providers/groups.ts
index 0762719dc..72c2ba57e 100644
--- a/src/providers/groups.ts
+++ b/src/providers/groups.ts
@@ -16,10 +16,27 @@ import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { CoreSitesProvider } from './sites';
+/**
+ * Group info for an activity.
+ */
export interface CoreGroupInfo {
- groups?: any[]; // List of groups.
- separateGroups?: boolean; // Whether it's separate groups.
- visibleGroups?: boolean; // Whether it's visible groups.
+ /**
+ * List of groups.
+ * @type {any[]}
+ */
+ groups?: any[];
+
+ /**
+ * Whether it's separate groups.
+ * @type {boolean}
+ */
+ separateGroups?: boolean;
+
+ /**
+ * Whether it's visible groups.
+ * @type {boolean}
+ */
+ visibleGroups?: boolean;
};
/*
@@ -31,6 +48,7 @@ export class CoreGroupsProvider {
public static NOGROUPS = 0;
public static SEPARATEGROUPS = 1;
public static VISIBLEGROUPS = 2;
+ protected ROOT_CACHE_KEY = 'mmGroups:';
constructor(private sitesProvider: CoreSitesProvider, private translate: TranslateService) {}
@@ -86,7 +104,7 @@ export class CoreGroupsProvider {
* @return {string} Cache key.
*/
protected getActivityAllowedGroupsCacheKey(cmId: number, userId: number) : string {
- return this.getRootCacheKey() + 'allowedgroups:' + cmId + ':' + userId;
+ return this.ROOT_CACHE_KEY + 'allowedgroups:' + cmId + ':' + userId;
}
/**
@@ -178,16 +196,7 @@ export class CoreGroupsProvider {
* @return {string} Cache key.
*/
protected getActivityGroupModeCacheKey(cmId: number) : string {
- return this.getRootCacheKey() + 'groupmode:' + cmId;
- }
-
- /**
- * Get the "root" cache key for WS calls.
- *
- * @return {string} Cache key.
- */
- protected getRootCacheKey() : string {
- return 'mmGroups:';
+ return this.ROOT_CACHE_KEY + 'groupmode:' + cmId;
}
/**
@@ -249,7 +258,7 @@ export class CoreGroupsProvider {
* @return {string} Cache key.
*/
protected getUserGroupsInCourseCacheKey(courseId: number, userId: number) : string {
- return this.getRootCacheKey() + 'courseGroups:' + courseId + ':' + userId;
+ return this.ROOT_CACHE_KEY + 'courseGroups:' + courseId + ':' + userId;
}
/**
diff --git a/src/providers/init.ts b/src/providers/init.ts
index 37dd18ab2..2b9d6c535 100644
--- a/src/providers/init.ts
+++ b/src/providers/init.ts
@@ -17,11 +17,34 @@ import { Platform } from 'ionic-angular';
import { CoreLoggerProvider } from './logger';
import { CoreUtilsProvider } from './utils/utils';
+/**
+ * Interface that all init handlers must implement.
+ */
export interface CoreInitHandler {
- name: string; // Name of the handler.
- load(): Promise; // Function to execute during the init process.
- priority?: number; // The highest priority is executed first. You should use values lower than MAX_RECOMMENDED_PRIORITY.
- blocking?: boolean; // Set this to true when this process should be resolved before any following one.
+ /**
+ * A name to identify the handler.
+ * @type {string}
+ */
+ name: string;
+
+ /**
+ * Function to execute during the init process.
+ *
+ * @return {Promise} Promise resolved when done.
+ */
+ load(): Promise;
+
+ /**
+ * The highest priority is executed first. You should use values lower than MAX_RECOMMENDED_PRIORITY.
+ * @type {number}
+ */
+ priority?: number;
+
+ /**
+ * Set this to true when this process should be resolved before any following one.
+ * @type {boolean}
+ */
+ blocking?: boolean;
};
/*
@@ -32,9 +55,9 @@ export class CoreInitDelegate {
public static DEFAULT_PRIORITY = 100; // Default priority for init processes.
public static MAX_RECOMMENDED_PRIORITY = 600;
- initProcesses = {};
- logger;
- readiness;
+ protected initProcesses = {};
+ protected logger;
+ protected readiness;
constructor(logger: CoreLoggerProvider, platform: Platform, private utils: CoreUtilsProvider) {
this.logger = logger.getInstance('CoreInitDelegate');
diff --git a/src/providers/lang.ts b/src/providers/lang.ts
index 86b241c9a..af62d6268 100644
--- a/src/providers/lang.ts
+++ b/src/providers/lang.ts
@@ -26,10 +26,10 @@ import { CoreConfigConstants } from '../configconstants';
*/
@Injectable()
export class CoreLangProvider {
- fallbackLanguage:string = 'en'; // mmCoreConfigConstants.default_lang || 'en',
- currentLanguage: string; // Save current language in a variable to speed up the get function.
- customStrings = {};
- customStringsRaw: string;
+ protected fallbackLanguage:string = 'en'; // mmCoreConfigConstants.default_lang || 'en',
+ protected currentLanguage: string; // Save current language in a variable to speed up the get function.
+ protected customStrings = {};
+ protected customStringsRaw: string;
constructor(private translate: TranslateService, private configProvider: CoreConfigProvider, platform: Platform,
private globalization: Globalization) {
diff --git a/src/providers/local-notifications.ts b/src/providers/local-notifications.ts
index c95ef78c4..37c9f7bce 100644
--- a/src/providers/local-notifications.ts
+++ b/src/providers/local-notifications.ts
@@ -24,10 +24,22 @@ import { SQLiteDB } from '../classes/sqlitedb';
import { CoreConstants } from '../core/constants';
import { Subject } from 'rxjs';
+/**
+ * Local notification.
+ */
export interface CoreILocalNotification extends ILocalNotification {
+ /**
+ * Number of milliseconds to turn the led on (Android only).
+ * @type {number}
+ */
ledOnTime?: number;
+
+ /**
+ * Number of milliseconds to turn the led off (Android only).
+ * @type {number}
+ */
ledOffTime?: number;
-}
+};
/*
Generated class for the LocalNotificationsProvider provider.
@@ -432,7 +444,7 @@ export class CoreLocalNotificationsProvider {
return this.isTriggered(notification).then((triggered) => {
if (!triggered) {
// Check if sound is enabled for notifications.
- return this.configProvider.get(CoreConstants.settingsNotificationSound, true).then((soundEnabled) => {
+ return this.configProvider.get(CoreConstants.SETTINGS_NOTIFICATION_SOUND, true).then((soundEnabled) => {
if (!soundEnabled) {
notification.sound = null;
} else {
diff --git a/src/providers/plugin-file-delegate.ts b/src/providers/plugin-file-delegate.ts
index 71af4e9a7..3c6af75ec 100644
--- a/src/providers/plugin-file-delegate.ts
+++ b/src/providers/plugin-file-delegate.ts
@@ -15,10 +15,31 @@
import { Injectable } from '@angular/core';
import { CoreLoggerProvider } from './logger';
+/**
+ * Interface that all plugin file handlers must implement.
+ */
export interface CorePluginFileHandler {
- name: string; // Name of the handler.
- getComponentRevisionRegExp?(args: string[]): RegExp; // Should return the RegExp to match revision on pluginfile url.
- getComponentRevisionReplace?(args: string[]): string; // Should return the String to remove the revision on pluginfile url.
+ /**
+ * A name to identify the handler. It should match the "component" of pluginfile URLs.
+ * @type {string}
+ */
+ name: string;
+
+ /**
+ * Return the RegExp to match the revision on pluginfile URLs.
+ *
+ * @param {string[]} args Arguments of the pluginfile URL defining component and filearea at least.
+ * @return {RegExp} RegExp to match the revision on pluginfile URLs.
+ */
+ getComponentRevisionRegExp?(args: string[]): RegExp;
+
+ /**
+ * Should return the string to remove the revision on pluginfile url.
+ *
+ * @param {string[]} args Arguments of the pluginfile URL defining component and filearea at least.
+ * @return {string} String to remove the revision on pluginfile url.
+ */
+ getComponentRevisionReplace?(args: string[]): string;
};
/**
diff --git a/src/providers/sites.ts b/src/providers/sites.ts
index e912ef2bf..d49652049 100644
--- a/src/providers/sites.ts
+++ b/src/providers/sites.ts
@@ -27,26 +27,102 @@ import { CoreSite } from '../classes/site';
import { SQLiteDB } from '../classes/sqlitedb';
import { Md5 } from 'ts-md5/dist/md5';
+/**
+ * Response of checking if a site exists and its configuration.
+ */
export interface CoreSiteCheckResponse {
- code: number; // Code to identify the authentication method to use.
- siteUrl: string; // Site url to use (might have changed during the process).
- service: string; // Service used.
- warning?: string; // Code of the warning message to show to the user.
- config?: any; // Site public config (if available).
-};
+ /**
+ * Code to identify the authentication method to use.
+ * @type {number}
+ */
+ code: number;
-export interface CoreSiteUserTokenResponse {
- token: string; // User token.
- siteUrl: string; // Site URL to use.
- privateToken?: string; // User private token.
-};
-
-export interface CoreSiteBasicInfo {
- id: string;
+ /**
+ * Site url to use (might have changed during the process).
+ * @type {string}
+ */
siteUrl: string;
+
+ /**
+ * Service used.
+ * @type {string}
+ */
+ service: string;
+
+ /**
+ * Code of the warning message to show to the user.
+ * @type {string}
+ */
+ warning?: string;
+
+ /**
+ * Site public config (if available).
+ * @type {any}
+ */
+ config?: any;
+};
+
+/**
+ * Response of getting user token.
+ */
+export interface CoreSiteUserTokenResponse {
+ /**
+ * User token.
+ * @type {string}
+ */
+ token: string;
+
+ /**
+ * Site URL to use.
+ * @type {string}
+ */
+ siteUrl: string;
+
+ /**
+ * User private token.
+ * @type {string}
+ */
+ privateToken?: string;
+};
+
+/**
+ * Site's basic info.
+ */
+export interface CoreSiteBasicInfo {
+ /**
+ * Site ID.
+ * @type {string}
+ */
+ id: string;
+
+ /**
+ * Site URL.
+ * @type {string}
+ */
+ siteUrl: string;
+
+ /**
+ * User's full name.
+ * @type {string}
+ */
fullName: string;
+
+ /**
+ * Site's name.
+ * @type {string}
+ */
siteName: string;
+
+ /**
+ * User's avatar.
+ * @type {string}
+ */
avatar: string;
+
+ /**
+ * Badge to display in the site.
+ * @type {number}
+ */
badge?: number;
};
@@ -234,7 +310,7 @@ export class CoreSitesProvider {
this.services[siteUrl] = data.service; // No need to store it in DB.
if (data.coreSupported ||
- (data.code != CoreConstants.loginSSOCode && data.code != CoreConstants.loginSSOInAppCode)) {
+ (data.code != CoreConstants.LOGIN_SSO_CODE && data.code != CoreConstants.LOGIN_SSO_INAPP_CODE)) {
// SSO using local_mobile not needed, try to get the site public config.
return temporarySite.getPublicConfig().then((config) : any => {
publicConfig = config;
@@ -301,7 +377,7 @@ export class CoreSitesProvider {
data.service = 'c';
}
- const observable = this.http.post(siteUrl + '/login/token.php', data).timeout(CoreConstants.wsTimeout);
+ const observable = this.http.post(siteUrl + '/login/token.php', data).timeout(CoreConstants.WS_TIMEOUT);
return this.utils.observableToPromise(observable).catch((error) => {
return Promise.reject(error.message);
}).then((data: any) => {
@@ -339,7 +415,7 @@ export class CoreSitesProvider {
password: password,
service: service
},
- observable = this.http.post(siteUrl + '/login/token.php', params).timeout(CoreConstants.wsTimeout);
+ observable = this.http.post(siteUrl + '/login/token.php', params).timeout(CoreConstants.WS_TIMEOUT);
return this.utils.observableToPromise(observable).then((data: any) : any => {
if (typeof data == 'undefined') {
diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts
index 5ace19ffb..5e4df4f10 100644
--- a/src/providers/utils/dom.ts
+++ b/src/providers/utils/dom.ts
@@ -13,7 +13,8 @@
// limitations under the License.
import { Injectable } from '@angular/core';
-import { LoadingController, Loading, ToastController, Toast, AlertController, Alert, Platform, Content } from 'ionic-angular';
+import { LoadingController, Loading, ToastController, Toast, AlertController, Alert, Platform, Content,
+ NavController, ModalController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreTextUtilsProvider } from './text';
import { CoreAppProvider } from '../app';
@@ -26,14 +27,17 @@ import { CoreConstants } from '../../core/constants';
*/
@Injectable()
export class CoreDomUtilsProvider {
- element = document.createElement('div'); // Fake element to use in some functions, to prevent re-creating it each time.
- matchesFn: string; // Name of the "matches" function to use when simulating a closest call.
- inputSupportKeyboard = ['date', 'datetime', 'datetime-local', 'email', 'month', 'number', 'password',
+ // List of input types that support keyboard.
+ protected INPUT_SUPPORT_KEYBOARD = ['date', 'datetime', 'datetime-local', 'email', 'month', 'number', 'password',
'search', 'tel', 'text', 'time', 'url', 'week'];
+ protected element = document.createElement('div'); // Fake element to use in some functions, to prevent creating it each time.
+ protected matchesFn: string; // Name of the "matches" function to use when simulating a closest call.
+
constructor(private translate: TranslateService, private loadingCtrl: LoadingController, private toastCtrl: ToastController,
private alertCtrl: AlertController, private textUtils: CoreTextUtilsProvider, private appProvider: CoreAppProvider,
- private platform: Platform, private configProvider: CoreConfigProvider, private urlUtils: CoreUrlUtilsProvider) {}
+ private platform: Platform, private configProvider: CoreConfigProvider, private urlUtils: CoreUrlUtilsProvider,
+ private modalCtrl: ModalController) {}
/**
* Wraps a message with core-format-text if the message contains HTML tags.
@@ -43,6 +47,7 @@ export class CoreDomUtilsProvider {
* @return {string} Result message.
*/
private addFormatTextIfNeeded(message: string) : string {
+ // @todo
if (this.textUtils.hasHTMLTags(message)) {
return '' + message + '';
}
@@ -96,12 +101,13 @@ export class CoreDomUtilsProvider {
* @param {string} [unknownMessage] ID of the message to show if size is unknown.
* @param {number} [wifiThreshold] Threshold to show confirm in WiFi connection. Default: CoreWifiDownloadThreshold.
* @param {number} [limitedThreshold] Threshold to show confirm in limited connection. Default: CoreDownloadThreshold.
+ * @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.
*/
- confirmDownloadSize(size: any, message?: string, unknownMessage?: string, wifiThreshold?: number, limitedThreshold?: number)
- : Promise {
- wifiThreshold = typeof wifiThreshold == 'undefined' ? CoreConstants.wifiDownloadThreshold : wifiThreshold;
- limitedThreshold = typeof limitedThreshold == 'undefined' ? CoreConstants.downloadThreshold : limitedThreshold;
+ confirmDownloadSize(size: any, message?: string, unknownMessage?: string, wifiThreshold?: number, limitedThreshold?: number,
+ alwaysConfirm?: boolean) : Promise