From 81ce1a7d01beff61dbe7c08e73a17026d2ad48a9 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 6 Oct 2020 10:48:26 +0200 Subject: [PATCH] MOBILE-3565 login: Initial implementation of init page --- package-lock.json | 5 + package.json | 3 +- src/app/app.module.ts | 37 ++++- src/app/classes/singletons-factory.ts | 76 +++++++++ src/app/core/constants.ts | 106 ++++++++++++ src/app/core/emulator/emulator.module.ts | 38 +++++ src/app/core/login/login-routing.module.ts | 6 + src/app/core/login/login.module.ts | 2 + src/app/core/login/pages/init/init.page.ts | 75 ++++++++- src/app/core/login/pages/site/site.html | 3 + src/app/core/login/pages/site/site.page.ts | 33 ++++ src/app/core/login/pages/site/site.scss | 2 + src/app/services/app.ts | 112 +++++++++++++ src/app/services/init.ts | 178 +++++++++++++++++++++ src/app/services/utils/utils.ts | 159 ++++++++++++++++++ src/app/singletons/core.singletons.ts | 42 +++++ src/app/singletons/logger.ts | 90 +++++++++++ tslint.json | 16 +- 18 files changed, 963 insertions(+), 20 deletions(-) create mode 100644 src/app/classes/singletons-factory.ts create mode 100644 src/app/core/constants.ts create mode 100644 src/app/core/emulator/emulator.module.ts create mode 100644 src/app/core/login/pages/site/site.html create mode 100644 src/app/core/login/pages/site/site.page.ts create mode 100644 src/app/core/login/pages/site/site.scss create mode 100644 src/app/services/app.ts create mode 100644 src/app/services/init.ts create mode 100644 src/app/services/utils/utils.ts create mode 100644 src/app/singletons/core.singletons.ts create mode 100644 src/app/singletons/logger.ts diff --git a/package-lock.json b/package-lock.json index 0b7b6305e..1059ce093 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9525,6 +9525,11 @@ "minimist": "^1.2.5" } }, + "moment": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.0.tgz", + "integrity": "sha512-z6IJ5HXYiuxvFTI6eiQ9dm77uE0gyy1yXNApVHqTcnIKfY9tIwEjlzsZ6u1LQXvVgKeTnv9Xm7NDvJ7lso3MtA==" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", diff --git a/package.json b/package.json index e31034457..de5560e89 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@angular/platform-browser-dynamic": "~10.0.0", "@angular/router": "~10.0.0", "@ionic-native/core": "^5.0.0", - "@ionic-native/splash-screen": "^5.0.0", + "@ionic-native/splash-screen": "^5.28.0", "@ionic-native/status-bar": "^5.0.0", "@ionic/angular": "^5.0.0", "com-darryncampbell-cordova-plugin-intent": "^2.0.0", @@ -73,6 +73,7 @@ "cordova-support-google-services": "^1.2.1", "cordova.plugins.diagnostic": "^6.0.2", "es6-promise-plugin": "^4.2.2", + "moment": "^2.29.0", "nl.kingsquare.cordova.background-audio": "^1.0.1", "phonegap-plugin-multidex": "^1.0.0", "phonegap-plugin-push": "git+https://github.com/moodlemobile/phonegap-plugin-push.git#moodle-v3", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 06b45d39b..f01239125 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { NgModule } from '@angular/core'; +import { NgModule, Injector } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { RouteReuseStrategy } from '@angular/router'; @@ -21,13 +21,42 @@ import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; +import { CoreAppProvider } from '@services/app'; +import { CoreInitDelegate } from '@services/init'; +import { CoreUtilsProvider } from '@services/utils/utils'; + +import { CoreEmulatorModule } from '@core/emulator/emulator.module'; +import { CoreLoginModule } from '@core/login/login.module'; + +import { setSingletonsInjector } from '@singletons/core.singletons'; + @NgModule({ declarations: [AppComponent], entryComponents: [], - imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule], + imports: [ + BrowserModule, + IonicModule.forRoot(), + AppRoutingModule, + CoreEmulatorModule, + CoreLoginModule, + ], providers: [ - { provide: RouteReuseStrategy, useClass: IonicRouteStrategy } + { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, + CoreAppProvider, + CoreInitDelegate, + CoreUtilsProvider, ], bootstrap: [AppComponent], }) -export class AppModule {} +export class AppModule { + constructor(injector: Injector, + initDelegate: CoreInitDelegate, + ) { + + // Set the injector. + setSingletonsInjector(injector); + + // Execute the init processes. + initDelegate.executeInitProcesses(); + } +} diff --git a/src/app/classes/singletons-factory.ts b/src/app/classes/singletons-factory.ts new file mode 100644 index 000000000..791b192f0 --- /dev/null +++ b/src/app/classes/singletons-factory.ts @@ -0,0 +1,76 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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, Type } from '@angular/core'; + +/** + * Stub class used to type anonymous classes created in CoreSingletonsFactory#makeSingleton method. + */ +class CoreSingleton {} + +/** + * Token that can be used to resolve instances from the injector. + */ +export type CoreInjectionToken = Type | Type | string; + +/** + * Singleton class created using the factory. + */ +export type CoreSingletonClass = typeof CoreSingleton & { instance: Service }; + +/** + * Factory used to create CoreSingleton classes that get instances from an injector. + */ +export class CoreSingletonsFactory { + + /** + * Angular injector used to resolve singleton instances. + */ + private injector: Injector; + + /** + * Set the injector that will be used to resolve instances in the singletons created with this factory. + * + * @param injector Injector. + */ + setInjector(injector: Injector): void { + this.injector = injector; + } + + /** + * Make a singleton that will hold an instance resolved from the factory injector. + * + * @param injectionToken Injection token used to resolve the singleton instance. This is usually the service class if the + * provider was defined using a class or the string used in the `provide` key if it was defined using an object. + */ + makeSingleton(injectionToken: CoreInjectionToken): CoreSingletonClass { + // tslint:disable: no-this-assignment + const factory = this; + + return class { + + private static _instance: Service; + + static get instance(): Service { + // Initialize instances lazily. + if (!this._instance) { + this._instance = factory.injector.get(injectionToken); + } + + return this._instance; + } + + }; + } +} diff --git a/src/app/core/constants.ts b/src/app/core/constants.ts new file mode 100644 index 000000000..c9784ccc3 --- /dev/null +++ b/src/app/core/constants.ts @@ -0,0 +1,106 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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. + +/** + * Context levels enumeration. + */ +export const enum ContextLevel { + SYSTEM = 'system', + USER = 'user', + COURSECAT = 'coursecat', + COURSE = 'course', + MODULE = 'module', + BLOCK = 'block', +} + +/** + * Static class to contain all the core constants. + */ +export class CoreConstants { + static SECONDS_YEAR = 31536000; + static SECONDS_WEEK = 604800; + static SECONDS_DAY = 86400; + static SECONDS_HOUR = 3600; + static SECONDS_MINUTE = 60; + static WIFI_DOWNLOAD_THRESHOLD = 104857600; // 100MB. + static DOWNLOAD_THRESHOLD = 10485760; // 10MB. + static MINIMUM_FREE_SPACE = 10485760; // 10MB. + static IOS_FREE_SPACE_THRESHOLD = 524288000; // 500MB. + static DONT_SHOW_ERROR = 'CoreDontShowError'; + static NO_SITE_ID = 'NoSite'; + + // Settings constants. + static SETTINGS_RICH_TEXT_EDITOR = 'CoreSettingsRichTextEditor'; + static SETTINGS_NOTIFICATION_SOUND = 'CoreSettingsNotificationSound'; + static SETTINGS_SYNC_ONLY_ON_WIFI = 'CoreSettingsSyncOnlyOnWifi'; + static SETTINGS_DEBUG_DISPLAY = 'CoreSettingsDebugDisplay'; + static SETTINGS_REPORT_IN_BACKGROUND = 'CoreSettingsReportInBackground'; // @deprecated since 3.5.0 + static SETTINGS_SEND_ON_ENTER = 'CoreSettingsSendOnEnter'; + static SETTINGS_FONT_SIZE = 'CoreSettingsFontSize'; + static SETTINGS_COLOR_SCHEME = 'CoreSettingsColorScheme'; + static SETTINGS_ANALYTICS_ENABLED = 'CoreSettingsAnalyticsEnabled'; + + // WS constants. + static WS_TIMEOUT = 30000; // Timeout when not in WiFi. + static WS_TIMEOUT_WIFI = 30000; // Timeout when in WiFi. + static WS_PREFIX = 'local_mobile_'; + + // Login constants. + static LOGIN_SSO_CODE = 2; // SSO in browser window is required. + static LOGIN_SSO_INAPP_CODE = 3; // SSO in embedded browser is required. + static LOGIN_LAUNCH_DATA = 'CoreLoginLaunchData'; + + // Download status constants. + static DOWNLOADED = 'downloaded'; + static DOWNLOADING = 'downloading'; + static NOT_DOWNLOADED = 'notdownloaded'; + static OUTDATED = 'outdated'; + static NOT_DOWNLOADABLE = 'notdownloadable'; + + // Constants from Moodle's resourcelib. + static RESOURCELIB_DISPLAY_AUTO = 0; // Try the best way. + static RESOURCELIB_DISPLAY_EMBED = 1; // Display using object tag. + static RESOURCELIB_DISPLAY_FRAME = 2; // Display inside frame. + static RESOURCELIB_DISPLAY_NEW = 3; // Display normal link in new window. + static RESOURCELIB_DISPLAY_DOWNLOAD = 4; // Force download of file instead of display. + static RESOURCELIB_DISPLAY_OPEN = 5; // Open directly. + static RESOURCELIB_DISPLAY_POPUP = 6; // Open in "emulated" pop-up without navigation. + + // Feature constants. Used to report features that are, or are not, supported by a module. + static FEATURE_GRADE_HAS_GRADE = 'grade_has_grade'; // True if module can provide a grade. + static FEATURE_GRADE_OUTCOMES = 'outcomes'; // True if module supports outcomes. + static FEATURE_ADVANCED_GRADING = 'grade_advanced_grading'; // True if module supports advanced grading methods. + static FEATURE_CONTROLS_GRADE_VISIBILITY = 'controlsgradevisbility'; // True if module controls grade visibility over gradebook. + static FEATURE_PLAGIARISM = 'plagiarism'; // True if module supports plagiarism plugins. + static FEATURE_COMPLETION_TRACKS_VIEWS = 'completion_tracks_views'; // True if module tracks whether somebody viewed it. + static FEATURE_COMPLETION_HAS_RULES = 'completion_has_rules'; // True if module has custom completion rules. + static FEATURE_NO_VIEW_LINK = 'viewlink'; // True if module has no 'view' page (like label). + static FEATURE_IDNUMBER = 'idnumber'; // True if module wants support for setting the ID number for grade calculation purposes. + static FEATURE_GROUPS = 'groups'; // True if module supports groups. + static FEATURE_GROUPINGS = 'groupings'; // True if module supports groupings. + static FEATURE_MOD_ARCHETYPE = 'mod_archetype'; // Type of module. + static FEATURE_MOD_INTRO = 'mod_intro'; // True if module supports intro editor. + static FEATURE_MODEDIT_DEFAULT_COMPLETION = 'modedit_default_completion'; // True if module has default completion. + static FEATURE_COMMENT = 'comment'; + static FEATURE_RATE = 'rate'; + static FEATURE_BACKUP_MOODLE2 = 'backup_moodle2'; // True if module supports backup/restore of moodle2 format. + static FEATURE_SHOW_DESCRIPTION = 'showdescription'; // True if module can show description on course main page. + static FEATURE_USES_QUESTIONS = 'usesquestions'; // True if module uses the question bank. + + // Pssobile archetypes for modules. + static MOD_ARCHETYPE_OTHER = 0; // Unspecified module archetype. + static MOD_ARCHETYPE_RESOURCE = 1; // Resource-like type module. + static MOD_ARCHETYPE_ASSIGNMENT = 2; // Assignment module archetype. + static MOD_ARCHETYPE_SYSTEM = 3; // System (not user-addable) module archetype. +} diff --git a/src/app/core/emulator/emulator.module.ts b/src/app/core/emulator/emulator.module.ts new file mode 100644 index 000000000..7c214c233 --- /dev/null +++ b/src/app/core/emulator/emulator.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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'; + +// Ionic Native services. +import { SplashScreen } from '@ionic-native/splash-screen/ngx'; + +/** + * This module handles the emulation of Cordova plugins in browser and desktop. + * + * It includes the "mock" of all the Ionic Native services that should be supported in browser and desktop, + * otherwise those features would only work in a Cordova environment. + * + * This module also determines if the app should use the original service or the mock. In each of the "useFactory" + * functions we check if the app is running in mobile or not, and then provide the right service to use. + */ +@NgModule({ + declarations: [ + ], + imports: [ + ], + providers: [ + SplashScreen, + ] +}) +export class CoreEmulatorModule { } diff --git a/src/app/core/login/login-routing.module.ts b/src/app/core/login/login-routing.module.ts index 858df6bb3..f71d29648 100644 --- a/src/app/core/login/login-routing.module.ts +++ b/src/app/core/login/login-routing.module.ts @@ -14,13 +14,19 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; + import { CoreLoginInitPage } from './pages/init/init.page'; +import { CoreLoginSitePage } from './pages/site/site.page'; const routes: Routes = [ { path: '', component: CoreLoginInitPage, }, + { + path: 'site', + component: CoreLoginSitePage, + }, ]; @NgModule({ diff --git a/src/app/core/login/login.module.ts b/src/app/core/login/login.module.ts index e459f38ba..74707b4fb 100644 --- a/src/app/core/login/login.module.ts +++ b/src/app/core/login/login.module.ts @@ -19,6 +19,7 @@ import { IonicModule } from '@ionic/angular'; import { CoreLoginRoutingModule } from './login-routing.module'; import { CoreLoginInitPage } from './pages/init/init.page'; +import { CoreLoginSitePage } from './pages/site/site.page'; @NgModule({ @@ -29,6 +30,7 @@ import { CoreLoginInitPage } from './pages/init/init.page'; ], declarations: [ CoreLoginInitPage, + CoreLoginSitePage, ], }) export class CoreLoginModule {} diff --git a/src/app/core/login/pages/init/init.page.ts b/src/app/core/login/pages/init/init.page.ts index 8286ea762..f48917648 100644 --- a/src/app/core/login/pages/init/init.page.ts +++ b/src/app/core/login/pages/init/init.page.ts @@ -12,7 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +import { CoreApp } from '@services/app'; +import { CoreInit } from '@services/init'; +import { CoreConstants } from '@core/constants'; +import { SplashScreen } from '@singletons/core.singletons'; /** * Page that displays a "splash screen" while the app is being initialized. @@ -22,4 +28,69 @@ import { Component } from '@angular/core'; templateUrl: 'init.html', styleUrls: ['init.scss'], }) -export class CoreLoginInitPage { } +export class CoreLoginInitPage implements OnInit { + + constructor(protected router: Router) {} + + /** + * Initialize the component. + */ + ngOnInit(): void { + // Wait for the app to be ready. + CoreInit.instance.ready().then(() => { + // Check if there was a pending redirect. + const redirectData = CoreApp.instance.getRedirect(); + if (redirectData.siteId) { + // Unset redirect data. + CoreApp.instance.storeRedirect('', '', ''); + + // Only accept the redirect if it was stored less than 20 seconds ago. + if (Date.now() - redirectData.timemodified < 20000) { + // if (redirectData.siteId != CoreConstants.NO_SITE_ID) { + // // The redirect is pointing to a site, load it. + // return this.sitesProvider.loadSite(redirectData.siteId, redirectData.page, redirectData.params) + // .then((loggedIn) => { + + // if (loggedIn) { + // return this.loginHelper.goToSiteInitialPage(this.navCtrl, redirectData.page, redirectData.params, + // { animate: false }); + // } + // }).catch(() => { + // // Site doesn't exist. + // return this.loadPage(); + // }); + // } else { + // // No site to load, open the page. + // return this.loginHelper.goToNoSitePage(this.navCtrl, redirectData.page, redirectData.params); + // } + } + } + + return this.loadPage(); + }).then(() => { + // If we hide the splash screen now, the init view is still seen for an instant. Wait a bit to make sure it isn't seen. + setTimeout(() => { + SplashScreen.instance.hide(); + }, 100); + }); + } + + /** + * Load the right page. + * + * @return Promise resolved when done. + */ + protected async loadPage(): Promise { + // if (this.sitesProvider.isLoggedIn()) { + // if (this.loginHelper.isSiteLoggedOut()) { + // return this.sitesProvider.logout().then(() => { + // return this.loadPage(); + // }); + // } + + // return this.loginHelper.goToSiteInitialPage(); + // } + + await this.router.navigate(['/login/site']); + } +} diff --git a/src/app/core/login/pages/site/site.html b/src/app/core/login/pages/site/site.html new file mode 100644 index 000000000..49d7a9823 --- /dev/null +++ b/src/app/core/login/pages/site/site.html @@ -0,0 +1,3 @@ + + Site page. + diff --git a/src/app/core/login/pages/site/site.page.ts b/src/app/core/login/pages/site/site.page.ts new file mode 100644 index 000000000..34c863eaa --- /dev/null +++ b/src/app/core/login/pages/site/site.page.ts @@ -0,0 +1,33 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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, OnInit } from '@angular/core'; + +/** + * Page that displays a "splash screen" while the app is being initialized. + */ +@Component({ + selector: 'page-core-login-site', + templateUrl: 'site.html', + styleUrls: ['site.scss'], +}) +export class CoreLoginSitePage implements OnInit { + + /** + * Initialize the component. + */ + ngOnInit(): void { + + } +} diff --git a/src/app/core/login/pages/site/site.scss b/src/app/core/login/pages/site/site.scss new file mode 100644 index 000000000..b61725a0d --- /dev/null +++ b/src/app/core/login/pages/site/site.scss @@ -0,0 +1,2 @@ +app-root page-core-login-init { +} diff --git a/src/app/services/app.ts b/src/app/services/app.ts new file mode 100644 index 000000000..3f2b03e85 --- /dev/null +++ b/src/app/services/app.ts @@ -0,0 +1,112 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { makeSingleton } from '@singletons/core.singletons'; +import { CoreLogger } from '@singletons/logger'; + +/** + * Data stored for a redirect to another page/site. + */ +export type CoreRedirectData = { + /** + * ID of the site to load. + */ + siteId?: string; + + /** + * Name of the page to redirect to. + */ + page?: string; + + /** + * Params to pass to the page. + */ + params?: any; + + /** + * Timestamp when this redirect was last modified. + */ + timemodified?: number; +}; + +/** + * Factory to provide some global functionalities, like access to the global app database. + * @description + * Each service or component should be responsible of creating their own database tables. Example: + * + * constructor(appProvider: CoreAppProvider) { + * this.appDB = appProvider.getDB(); + * this.appDB.createTableFromSchema(this.tableSchema); + * } + */ +@Injectable() +export class CoreAppProvider { + protected logger: CoreLogger; + + constructor() { + this.logger = CoreLogger.getInstance('CoreAppProvider'); + } + + /** + * Retrieve redirect data. + * + * @return Object with siteid, state, params and timemodified. + */ + getRedirect(): CoreRedirectData { + if (localStorage && localStorage.getItem) { + try { + const data: CoreRedirectData = { + siteId: localStorage.getItem('CoreRedirectSiteId'), + page: localStorage.getItem('CoreRedirectState'), + params: localStorage.getItem('CoreRedirectParams'), + timemodified: parseInt(localStorage.getItem('CoreRedirectTime'), 10) + }; + + if (data.params) { + data.params = JSON.parse(data.params); + } + + return data; + } catch (ex) { + this.logger.error('Error loading redirect data:', ex); + } + } + + return {}; + } + + /** + * Store redirect params. + * + * @param siteId Site ID. + * @param page Page to go. + * @param params Page params. + */ + storeRedirect(siteId: string, page: string, params: any): void { + if (localStorage && localStorage.setItem) { + try { + localStorage.setItem('CoreRedirectSiteId', siteId); + localStorage.setItem('CoreRedirectState', page); + localStorage.setItem('CoreRedirectParams', JSON.stringify(params)); + localStorage.setItem('CoreRedirectTime', String(Date.now())); + } catch (ex) { + // Ignore errors. + } + } + } +} + +export class CoreApp extends makeSingleton(CoreAppProvider) {} diff --git a/src/app/services/init.ts b/src/app/services/init.ts new file mode 100644 index 000000000..1126c935b --- /dev/null +++ b/src/app/services/init.ts @@ -0,0 +1,178 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { CoreUtils } from '@services/utils/utils'; +import { CoreLogger } from '@singletons/logger'; +import { makeSingleton } from '@singletons/core.singletons'; + +/** + * Interface that all init handlers must implement. + */ +export type CoreInitHandler = { + /** + * A name to identify the handler. + */ + name: string; + + /** + * The highest priority is executed first. You should use values lower than MAX_RECOMMENDED_PRIORITY. + */ + priority?: number; + + /** + * Set this to true when this process should be resolved before any following one. + */ + blocking?: boolean; + + /** + * Function to execute during the init process. + * + * @return Promise resolved when done. + */ + load(): Promise; +}; + +/* + * Provider for initialisation mechanisms. + */ +@Injectable() +export class CoreInitDelegate { + static DEFAULT_PRIORITY = 100; // Default priority for init processes. + static MAX_RECOMMENDED_PRIORITY = 600; + + protected initProcesses = {}; + protected logger: CoreLogger; + protected readiness; + + constructor() { + this.logger = CoreLogger.getInstance('CoreInitDelegate'); + } + + /** + * Executes the registered init processes. + * + * Reserved for core use, do not call directly. + */ + executeInitProcesses(): void { + let ordered = []; + + if (typeof this.readiness == 'undefined') { + this.initReadiness(); + } + + // Re-ordering by priority. + for (const name in this.initProcesses) { + ordered.push(this.initProcesses[name]); + } + ordered.sort((a, b) => { + return b.priority - a.priority; + }); + + ordered = ordered.map((data: CoreInitHandler) => { + return { + func: this.prepareProcess.bind(this, data), + blocking: !!data.blocking, + }; + }); + + // Execute all the processes in order to solve dependencies. + CoreUtils.instance.executeOrderedPromises(ordered).finally(this.readiness.resolve); + } + + /** + * Init the readiness promise. + */ + protected initReadiness(): void { + this.readiness = CoreUtils.instance.promiseDefer(); + this.readiness.promise.then(() => this.readiness.resolved = true); + } + + /** + * Instantly returns if the app is ready. + * + * @return Whether it's ready. + */ + isReady(): boolean { + return this.readiness.resolved; + } + + /** + * Convenience function to return a function that executes the process. + * + * @param data The data of the process. + * @return Promise of the process. + */ + protected prepareProcess(data: CoreInitHandler): Promise { + let promise; + + this.logger.debug(`Executing init process '${data.name}'`); + + try { + promise = data.load(); + } catch (e) { + this.logger.error('Error while calling the init process \'' + data.name + '\'. ' + e); + + return; + } + + return promise; + } + + /** + * Notifies when the app is ready. This returns a promise that is resolved when the app is initialised. + * + * @return Resolved when the app is initialised. Never rejected. + */ + ready(): Promise { + if (typeof this.readiness == 'undefined') { + // Prevent race conditions if this is called before executeInitProcesses. + this.initReadiness(); + } + + return this.readiness.promise; + } + + /** + * Registers an initialisation process. + * + * @description + * Init processes can be used to add initialisation logic to the app. Anything that should block the user interface while + * some processes are done should be an init process. It is recommended to use a priority lower than MAX_RECOMMENDED_PRIORITY + * to make sure that your process does not happen before some essential other core processes. + * + * An init process should never change state or prompt user interaction. + * + * This delegate cannot be used by site plugins. + * + * @param instance The instance of the handler. + */ + registerProcess(handler: CoreInitHandler): void { + if (typeof handler.priority == 'undefined') { + handler.priority = CoreInitDelegate.DEFAULT_PRIORITY; + } + + if (typeof this.initProcesses[handler.name] != 'undefined') { + this.logger.log(`Process '${handler.name}' already registered.`); + + return; + } + + this.logger.log(`Registering process '${handler.name}'.`); + this.initProcesses[handler.name] = handler; + } +} + +export class CoreInit extends makeSingleton(CoreInitDelegate) {} diff --git a/src/app/services/utils/utils.ts b/src/app/services/utils/utils.ts new file mode 100644 index 000000000..6bb8618f8 --- /dev/null +++ b/src/app/services/utils/utils.ts @@ -0,0 +1,159 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { CoreLogger } from '@singletons/logger'; +import { makeSingleton } from '@singletons/core.singletons'; + +/* + * "Utils" service with helper functions. + */ +@Injectable() +export class CoreUtilsProvider { + protected logger: CoreLogger; + + constructor() { + this.logger = CoreLogger.getInstance('CoreUtilsProvider'); + } + + /** + * Similar to Promise.all, but if a promise fails this function's promise won't be rejected until ALL promises have finished. + * + * @param promises Promises. + * @return Promise resolved if all promises are resolved and rejected if at least 1 promise fails. + */ + allPromises(promises: Promise[]): Promise { + if (!promises || !promises.length) { + return Promise.resolve(); + } + + return new Promise((resolve, reject): void => { + const total = promises.length; + let count = 0; + let hasFailed = false; + let error; + + promises.forEach((promise) => { + promise.catch((err) => { + hasFailed = true; + error = err; + }).finally(() => { + count++; + + if (count === total) { + // All promises have finished, reject/resolve. + if (hasFailed) { + reject(error); + } else { + resolve(); + } + } + }); + }); + }); + } + + /** + * Execute promises one depending on the previous. + * + * @param orderedPromisesData Functions to be executed. + * @return Promise resolved when all promises are resolved. + */ + executeOrderedPromises(orderedPromisesData: OrderedPromiseData[]): Promise { + const promises = []; + let dependency = Promise.resolve(); + + // Execute all the processes in order. + for (const i in orderedPromisesData) { + const data = orderedPromisesData[i]; + + // Add the process to the dependency stack. + const promise = dependency.finally(() => { + try { + return data.function(); + } catch (e) { + this.logger.error(e.message); + + return; + } + }); + promises.push(promise); + + // If the new process is blocking, we set it as the dependency. + if (data.blocking) { + dependency = promise; + } + } + + // Return when all promises are done. + return this.allPromises(promises); + } + + /** + * Similar to AngularJS $q.defer(). + * + * @return The deferred promise. + */ + promiseDefer(): PromiseDefer { + const deferred: PromiseDefer = {}; + + deferred.promise = new Promise((resolve, reject): void => { + deferred.resolve = resolve; + deferred.reject = reject; + }); + + return deferred; + } +} + +export class CoreUtils extends makeSingleton(CoreUtilsProvider) {} + +/** + * Data for each entry of executeOrderedPromises. + */ +export type OrderedPromiseData = { + /** + * Function to execute. + */ + function: () => Promise; + + /** + * Whether the promise should block the following one. + */ + blocking?: boolean; +}; + +/** + * Deferred promise. It's similar to the result of $q.defer() in AngularJS. + */ +export type PromiseDefer = { + /** + * The promise. + */ + promise?: Promise; + + /** + * Function to resolve the promise. + * + * @param value The resolve value. + */ + resolve?: (value?: T) => void; + + /** + * Function to reject the promise. + * + * @param reason The reject param. + */ + reject?: (reason?: any) => void; +}; diff --git a/src/app/singletons/core.singletons.ts b/src/app/singletons/core.singletons.ts new file mode 100644 index 000000000..819fda31c --- /dev/null +++ b/src/app/singletons/core.singletons.ts @@ -0,0 +1,42 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { SplashScreen as SplashScreenPlugin } from '@ionic-native/splash-screen/ngx'; + +import { CoreSingletonsFactory, CoreInjectionToken, CoreSingletonClass } from '@classes/singletons-factory'; + +const factory = new CoreSingletonsFactory(); + +/** + * Set the injector that will be used to resolve instances in the singletons of this module. + * + * @param injector Module injector. + */ +export function setSingletonsInjector(injector: Injector): void { + factory.setInjector(injector); +} + +/** + * Make a singleton for this module. + * + * @param injectionToken Injection token used to resolve the singleton instance. This is usually the service class if the + * provider was defined using a class or the string used in the `provide` key if it was defined using an object. + */ +export function makeSingleton(injectionToken: CoreInjectionToken): CoreSingletonClass { + return factory.makeSingleton(injectionToken); +} + +export class SplashScreen extends makeSingleton(SplashScreenPlugin) {} diff --git a/src/app/singletons/logger.ts b/src/app/singletons/logger.ts new file mode 100644 index 000000000..6ed914d17 --- /dev/null +++ b/src/app/singletons/logger.ts @@ -0,0 +1,90 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 * as moment from 'moment'; +import { environment } from '@/environments/environment'; + +/** + * Helper service to display messages in the console. + * + * @description + * This service is meant to improve log messages, adding a timestamp and a name to all log messages. + * + * In your class constructor, call getInstance to configure your class name: + * CoreLogger.getInstance('InitPage'); + * + * Then you can call the log function you want to use in this logger instance. + */ +export class CoreLogger { + log: LogFunction; + info: LogFunction; + warn: LogFunction; + debug: LogFunction; + error: LogFunction; + + /** + * Get a logger instance for a certain class, service or component. + * + * @param className Name to use in the messages. + * @return Instance. + */ + static getInstance(className: string): CoreLogger { + // Disable log on production. + if (environment.production) { + /* tslint:next-line no-console */ + console.warn('Log is disabled in production app'); + + return { + log: () => {}, + info: () => {}, + warn: () => {}, + debug: () => {}, + error: () => {}, + }; + } + + className = className || ''; + + /* tslint:disable no-console */ + + return { + log: CoreLogger.prepareLogFn(console.log.bind(console), className), + info: CoreLogger.prepareLogFn(console.info.bind(console), className), + warn: CoreLogger.prepareLogFn(console.warn.bind(console), className), + debug: CoreLogger.prepareLogFn(console.debug.bind(console), className), + error: CoreLogger.prepareLogFn(console.error.bind(console), className), + }; + } + + /** + * Prepare a logging function, concatenating the timestamp and class name to all messages. + * + * @param logFn Log function to use. + * @param className Name to use in the messages. + * @return Prepared function. + */ + private static prepareLogFn(logFn: LogFunction, className: string): LogFunction { + // Return our own function that will call the logging function with the treated message. + return (...args): void => { + const now = moment().format('l LTS'); + args[0] = now + ' ' + className + ': ' + args[0]; // Prepend timestamp and className to the original message. + logFn.apply(null, args); + }; + } +} + +/** + * Log function type. + */ +type LogFunction = (...data: any[]) => void; diff --git a/tslint.json b/tslint.json index 21329f4e9..72d472aa3 100644 --- a/tslint.json +++ b/tslint.json @@ -1,12 +1,6 @@ { "extends": "tslint:recommended", "rules": { - "align": { - "options": [ - "parameters", - "statements" - ] - }, "array-type": false, "arrow-return-shorthand": true, "curly": true, @@ -22,12 +16,6 @@ "app", "camelCase" ], - "component-selector": [ - true, - "element", - "app", - "kebab-case" - ], "eofline": true, "import-blacklist": [ true, @@ -141,7 +129,9 @@ "template-no-negated-async": true, "use-lifecycle-interface": true, "use-pipe-transform-interface": true, - "object-literal-sort-keys": false + "object-literal-sort-keys": false, + "forin": false, + "triple-equals": false }, "rulesDirectory": [ "codelyzer"