From a332faa81e0445a176bb2aa5d78348a953f9d212 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 22 Oct 2020 13:04:57 +0200 Subject: [PATCH 1/6] MOBILE-3565 mainmenu: Initial implementation of main menu --- src/app/app-routing.module.ts | 4 + src/app/app.component.ts | 12 +- src/app/core/login/login-routing.module.ts | 5 + src/app/core/login/login.module.ts | 4 - .../pages/credentials/credentials.page.ts | 2 +- src/app/core/login/pages/init/init.page.ts | 92 +++--- src/app/core/login/services/helper.ts | 59 ++-- src/app/core/mainmenu/lang/en.json | 6 + .../core/mainmenu/mainmenu-routing.module.ts | 38 +++ src/app/core/mainmenu/mainmenu.module.ts | 42 +++ src/app/core/mainmenu/pages/menu/menu.html | 23 ++ src/app/core/mainmenu/pages/menu/menu.page.ts | 225 ++++++++++++++ src/app/core/mainmenu/pages/menu/menu.scss | 93 ++++++ src/app/core/mainmenu/pages/more/more.html | 84 ++++++ src/app/core/mainmenu/pages/more/more.page.ts | 182 ++++++++++++ src/app/core/mainmenu/pages/more/more.scss | 97 ++++++ src/app/core/mainmenu/services/delegate.ts | 175 +++++++++++ src/app/core/mainmenu/services/mainmenu.ts | 275 ++++++++++++++++++ src/app/services/sites.ts | 2 +- src/app/singletons/events.ts | 8 + src/assets/lang/en.json | 4 + 21 files changed, 1366 insertions(+), 66 deletions(-) create mode 100644 src/app/core/mainmenu/lang/en.json create mode 100644 src/app/core/mainmenu/mainmenu-routing.module.ts create mode 100644 src/app/core/mainmenu/mainmenu.module.ts create mode 100644 src/app/core/mainmenu/pages/menu/menu.html create mode 100644 src/app/core/mainmenu/pages/menu/menu.page.ts create mode 100644 src/app/core/mainmenu/pages/menu/menu.scss create mode 100644 src/app/core/mainmenu/pages/more/more.html create mode 100644 src/app/core/mainmenu/pages/more/more.page.ts create mode 100644 src/app/core/mainmenu/pages/more/more.scss create mode 100644 src/app/core/mainmenu/services/delegate.ts create mode 100644 src/app/core/mainmenu/services/mainmenu.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 52ae40f71..681958412 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -29,6 +29,10 @@ const routes: Routes = [ path: 'settings', loadChildren: () => import('./core/settings/settings.module').then( m => m.CoreAppSettingsPageModule), }, + { + path: 'mainmenu', + loadChildren: () => import('./core/mainmenu/mainmenu.module').then( m => m.CoreMainMenuModule), + }, ]; @NgModule({ diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 7a18e7723..3296ee02d 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -13,6 +13,8 @@ // limitations under the License. import { Component, OnInit } from '@angular/core'; +import { NavController } from '@ionic/angular'; + import { CoreLangProvider } from '@services/lang'; import { CoreEvents } from '@singletons/events'; @@ -24,7 +26,8 @@ import { CoreEvents } from '@singletons/events'; export class AppComponent implements OnInit { constructor( - private langProvider: CoreLangProvider, + protected langProvider: CoreLangProvider, + protected navCtrl: NavController, ) { } @@ -34,16 +37,13 @@ export class AppComponent implements OnInit { ngOnInit(): void { CoreEvents.on(CoreEvents.LOGOUT, () => { // Go to sites page when user is logged out. - // Due to DeepLinker, we need to use the ViewCtrl instead of name. - // Otherwise some pages are re-created when they shouldn't. - // TODO - // CoreApp.instance.getRootNavController().setRoot(CoreLoginSitesPage); + this.navCtrl.navigateRoot('/login/sites'); // Unload lang custom strings. this.langProvider.clearCustomStrings(); // Remove version classes from body. - // TODO + // @todo // this.removeVersionClass(); }); } diff --git a/src/app/core/login/login-routing.module.ts b/src/app/core/login/login-routing.module.ts index 8b1ae6f9a..5c528b09b 100644 --- a/src/app/core/login/login-routing.module.ts +++ b/src/app/core/login/login-routing.module.ts @@ -23,6 +23,11 @@ import { CoreLoginSitesPage } from './pages/sites/sites.page'; const routes: Routes = [ { path: '', + redirectTo: 'init', + pathMatch: 'full', + }, + { + path: 'init', component: CoreLoginInitPage, }, { diff --git a/src/app/core/login/login.module.ts b/src/app/core/login/login.module.ts index 5bfd7fd32..090f44698 100644 --- a/src/app/core/login/login.module.ts +++ b/src/app/core/login/login.module.ts @@ -27,7 +27,6 @@ import { CoreLoginCredentialsPage } from './pages/credentials/credentials.page'; import { CoreLoginInitPage } from './pages/init/init.page'; import { CoreLoginSitePage } from './pages/site/site.page'; import { CoreLoginSitesPage } from './pages/sites/sites.page'; -import { CoreLoginHelperProvider } from './services/helper'; @NgModule({ imports: [ @@ -47,8 +46,5 @@ import { CoreLoginHelperProvider } from './services/helper'; CoreLoginSitePage, CoreLoginSitesPage, ], - providers: [ - CoreLoginHelperProvider, - ], }) export class CoreLoginModule {} diff --git a/src/app/core/login/pages/credentials/credentials.page.ts b/src/app/core/login/pages/credentials/credentials.page.ts index d189c504f..8ff4ad4e2 100644 --- a/src/app/core/login/pages/credentials/credentials.page.ts +++ b/src/app/core/login/pages/credentials/credentials.page.ts @@ -243,7 +243,7 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy { this.siteId = id; - await CoreLoginHelper.instance.goToSiteInitialPage(undefined, undefined, undefined, undefined, this.urlToOpen); + await CoreLoginHelper.instance.goToSiteInitialPage({ urlToOpen: this.urlToOpen }); } catch (error) { CoreLoginHelper.instance.treatUserTokenError(siteUrl, error, username, password); diff --git a/src/app/core/login/pages/init/init.page.ts b/src/app/core/login/pages/init/init.page.ts index 2391f6c98..889a7178f 100644 --- a/src/app/core/login/pages/init/init.page.ts +++ b/src/app/core/login/pages/init/init.page.ts @@ -15,9 +15,13 @@ import { Component, OnInit } from '@angular/core'; import { NavController } from '@ionic/angular'; -import { CoreApp } from '@services/app'; +import { CoreApp, CoreRedirectData } from '@services/app'; import { CoreInit } from '@services/init'; import { SplashScreen } from '@singletons/core.singletons'; +import { CoreConstants } from '@core/constants'; +import { CoreSite } from '@/app/classes/site'; +import { CoreSites } from '@/app/services/sites'; +import { CoreLoginHelper, CoreLoginHelperProvider } from '../../services/helper'; /** * Page that displays a "splash screen" while the app is being initialized. @@ -40,55 +44,75 @@ export class CoreLoginInitPage implements OnInit { // 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 (redirectData.timemodified && 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); - // } - } + await this.handleRedirect(redirectData); + } else { + await this.loadPage(); } - await this.loadPage(); - // 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); } + /** + * Treat redirect data. + * + * @param redirectData Redirect data. + */ + protected async handleRedirect(redirectData: CoreRedirectData): Promise { + // Unset redirect data. + CoreApp.instance.storeRedirect('', '', {}); + + // Only accept the redirect if it was stored less than 20 seconds ago. + if (redirectData.timemodified && Date.now() - redirectData.timemodified < 20000) { + if (redirectData.siteId != CoreConstants.NO_SITE_ID) { + // The redirect is pointing to a site, load it. + try { + const loggedIn = await CoreSites.instance.loadSite( + redirectData.siteId!, + redirectData.page, + redirectData.params, + ); + + if (!loggedIn) { + return; + } + + return CoreLoginHelper.instance.goToSiteInitialPage({ + redirectPage: redirectData.page, + redirectParams: redirectData.params, + }); + } catch (error) { + // Site doesn't exist. + return this.loadPage(); + } + } else { + // No site to load, open the page. + return CoreLoginHelper.instance.goToNoSitePage(redirectData.page, redirectData.params); + } + } + + return this.loadPage(); + } + /** * 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(); - // }); - // } + if (CoreSites.instance.isLoggedIn()) { + if (CoreLoginHelper.instance.isSiteLoggedOut()) { + await CoreSites.instance.logout(); - // return this.loginHelper.goToSiteInitialPage(); - // } + return this.loadPage(); + } + + return CoreLoginHelper.instance.goToSiteInitialPage(); + } await this.navCtrl.navigateRoot('/login/sites'); } diff --git a/src/app/core/login/services/helper.ts b/src/app/core/login/services/helper.ts index e510e487f..5c7d9609b 100644 --- a/src/app/core/login/services/helper.ts +++ b/src/app/core/login/services/helper.ts @@ -34,11 +34,14 @@ import { CoreWSError } from '@classes/errors/wserror'; import { makeSingleton, Translate } from '@singletons/core.singletons'; import { CoreLogger } from '@singletons/logger'; import { CoreUrl } from '@singletons/url'; +import { NavigationOptions } from '@ionic/angular/providers/nav-controller'; /** * Helper provider that provides some common features regarding authentication. */ -@Injectable() +@Injectable({ + providedIn: 'root', +}) export class CoreLoginHelperProvider { static readonly OPEN_COURSE = 'open_course'; @@ -448,7 +451,7 @@ export class CoreLoginHelperProvider { * @return Promise resolved when done. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - goToNoSitePage(navCtrl: NavController, page: string, params?: Params): Promise { + goToNoSitePage(page?: string, params?: Params): Promise { // @todo return Promise.resolve(); } @@ -456,17 +459,11 @@ export class CoreLoginHelperProvider { /** * Go to the initial page of a site depending on 'userhomepage' setting. * - * @param navCtrl NavController to use. Defaults to app root NavController. - * @param page Name of the page to load after loading the main page. - * @param params Params to pass to the page. - * @param options Navigation options. - * @param url URL to open once the main menu is loaded. + * @param options Options. * @return Promise resolved when done. */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - goToSiteInitialPage(navCtrl?: NavController, page?: string, params?: Params, options?: any, url?: string): Promise { - // @todo - return Promise.resolve(); + goToSiteInitialPage(options?: OpenMainMenuOptions): Promise { + return this.openMainMenu(options); } /** @@ -664,17 +661,32 @@ export class CoreLoginHelperProvider { /** * Open the main menu, loading a certain page. * - * @param navCtrl NavController. - * @param page Name of the page to load. - * @param params Params to pass to the page. - * @param options Navigation options. - * @param url URL to open once the main menu is loaded. + * @param options Options. * @return Promise resolved when done. */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected openMainMenu(navCtrl: NavController, page: string, params: Params, options?: any, url?: string): Promise { - // @todo - return Promise.resolve(); + protected async openMainMenu(options?: OpenMainMenuOptions): Promise { + + // Due to DeepLinker, we need to remove the path from the URL before going to main menu. + // IonTabs checks the URL to determine which path to load for deep linking, so we clear the URL. + // @todo this.location.replaceState(''); + + if (options?.redirectPage == CoreLoginHelperProvider.OPEN_COURSE) { + // Load the main menu first, and then open the course. + try { + await this.navCtrl.navigateRoot('/mainmenu'); + } finally { + // @todo: Open course. + } + } else { + // Open the main menu. + const queryParams: Params = Object.assign({}, options); + delete queryParams.navigationOptions; + + await this.navCtrl.navigateRoot('/mainmenu', { + queryParams, + ...options?.navigationOptions, + }); + } } /** @@ -1375,3 +1387,10 @@ type StoredLoginLaunchData = { pageParams: Params; ssoUrlParams: CoreUrlParams; }; + +type OpenMainMenuOptions = { + redirectPage?: string; // Route of the page to open in main menu. If not defined, default tab will be selected. + redirectParams?: Params; // Params to pass to the selected tab if any. + urlToOpen?: string; // URL to open once the main menu is loaded. + navigationOptions?: NavigationOptions; // Navigation options. +}; diff --git a/src/app/core/mainmenu/lang/en.json b/src/app/core/mainmenu/lang/en.json new file mode 100644 index 000000000..4ff96fbf7 --- /dev/null +++ b/src/app/core/mainmenu/lang/en.json @@ -0,0 +1,6 @@ +{ + "changesite": "Change site", + "help": "Help", + "logout": "Log out", + "website": "Website" +} \ No newline at end of file diff --git a/src/app/core/mainmenu/mainmenu-routing.module.ts b/src/app/core/mainmenu/mainmenu-routing.module.ts new file mode 100644 index 000000000..2ad58ff75 --- /dev/null +++ b/src/app/core/mainmenu/mainmenu-routing.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'; +import { RouterModule, Routes } from '@angular/router'; + +import { CoreMainMenuPage } from './pages/menu/menu.page'; +import { CoreMainMenuMorePage } from './pages/more/more.page'; + +const routes: Routes = [ + { + path: '', + component: CoreMainMenuPage, + children: [ + { + path: 'more', + component: CoreMainMenuMorePage, + }, + ], + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class CoreMainMenuRoutingModule {} diff --git a/src/app/core/mainmenu/mainmenu.module.ts b/src/app/core/mainmenu/mainmenu.module.ts new file mode 100644 index 000000000..1cd8f561e --- /dev/null +++ b/src/app/core/mainmenu/mainmenu.module.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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; + +import { CoreComponentsModule } from '@/app/components/components.module'; +import { CoreDirectivesModule } from '@/app/directives/directives.module'; + +import { CoreMainMenuRoutingModule } from './mainmenu-routing.module'; +import { CoreMainMenuPage } from './pages/menu/menu.page'; +import { CoreMainMenuMorePage } from './pages/more/more.page'; + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + CoreMainMenuRoutingModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + ], + declarations: [ + CoreMainMenuPage, + CoreMainMenuMorePage, + ], +}) +export class CoreMainMenuModule {} diff --git a/src/app/core/mainmenu/pages/menu/menu.html b/src/app/core/mainmenu/pages/menu/menu.html new file mode 100644 index 000000000..8c53535dc --- /dev/null +++ b/src/app/core/mainmenu/pages/menu/menu.html @@ -0,0 +1,23 @@ + + + + + + + {{ tab.title | translate }} + + + + + {{ 'core.more' | translate }} + + + +
+
+ {{ "core.youreonline" | translate }} +
+
+ {{ "core.youreoffline" | translate }} +
+
diff --git a/src/app/core/mainmenu/pages/menu/menu.page.ts b/src/app/core/mainmenu/pages/menu/menu.page.ts new file mode 100644 index 000000000..364853780 --- /dev/null +++ b/src/app/core/mainmenu/pages/menu/menu.page.ts @@ -0,0 +1,225 @@ +// (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, OnDestroy, ViewChild, ChangeDetectorRef } from '@angular/core'; +import { ActivatedRoute, Params } from '@angular/router'; +import { NavController } from '@ionic/angular'; +import { Subscription } from 'rxjs'; + +import { CoreApp } from '@services/app'; +import { CoreSites } from '@services/sites'; +import { CoreEvents, CoreEventObserver, CoreEventLoadPageMainMenuData } from '@singletons/events'; +import { CoreMainMenu } from '../../services/mainmenu'; +import { CoreMainMenuDelegate, CoreMainMenuHandlerToDisplay } from '../../services/delegate'; + +/** + * Page that displays the main menu of the app. + */ +@Component({ + selector: 'page-core-mainmenu', + templateUrl: 'menu.html', + styleUrls: ['menu.scss'], +}) +export class CoreMainMenuPage implements OnInit, OnDestroy { + + tabs: CoreMainMenuHandlerToDisplay[] = []; + allHandlers?: CoreMainMenuHandlerToDisplay[]; + loaded = false; + redirectPage?: string; + redirectParams?: Params; + showTabs = false; + tabsPlacement = 'bottom'; + + protected subscription?: Subscription; + protected redirectObs?: CoreEventObserver; + protected pendingRedirect?: CoreEventLoadPageMainMenuData; + protected urlToOpen?: string; + protected mainMenuId: number; + protected keyboardObserver?: CoreEventObserver; + + @ViewChild('mainTabs') mainTabs?: any; // CoreIonTabsComponent; + + constructor( + protected route: ActivatedRoute, + protected navCtrl: NavController, + protected menuDelegate: CoreMainMenuDelegate, + protected changeDetector: ChangeDetectorRef, + ) { + this.mainMenuId = CoreApp.instance.getMainMenuId(); + } + + /** + * Initialize the component. + */ + ngOnInit(): void { + if (!CoreSites.instance.isLoggedIn()) { + this.navCtrl.navigateRoot('/login/init'); + + return; + } + + this.route.queryParams.subscribe(params => { + const redirectPage = params['redirectPage']; + if (redirectPage) { + this.pendingRedirect = { + redirectPage: redirectPage, + redirectParams: params['redirectParams'], + }; + } + + this.urlToOpen = params['urlToOpen']; + }); + + this.showTabs = true; + + this.redirectObs = CoreEvents.on(CoreEvents.LOAD_PAGE_MAIN_MENU, (data: CoreEventLoadPageMainMenuData) => { + if (!this.loaded) { + // View isn't ready yet, wait for it to be ready. + this.pendingRedirect = data; + } else { + delete this.pendingRedirect; + this.handleRedirect(data); + } + }); + + this.subscription = this.menuDelegate.getHandlers().subscribe((handlers) => { + // Remove the handlers that should only appear in the More menu. + this.allHandlers = handlers.filter((handler) => !handler.onlyInMore); + + this.initHandlers(); + + if (this.loaded && this.pendingRedirect) { + // Wait for tabs to be initialized and then handle the redirect. + setTimeout(() => { + if (this.pendingRedirect) { + this.handleRedirect(this.pendingRedirect); + delete this.pendingRedirect; + } + }); + } + }); + + window.addEventListener('resize', this.initHandlers.bind(this)); + + if (CoreApp.instance.isIOS()) { + // In iOS, the resize event is triggered before the keyboard is opened/closed and not triggered again once done. + // Init handlers again once keyboard is closed since the resize event doesn't have the updated height. + this.keyboardObserver = CoreEvents.on(CoreEvents.KEYBOARD_CHANGE, (kbHeight: number) => { + if (kbHeight === 0) { + this.initHandlers(); + + // If the device is slow it can take a bit more to update the window height. Retry in a few ms. + setTimeout(() => { + this.initHandlers(); + }, 250); + } + }); + } + + CoreApp.instance.setMainMenuOpen(this.mainMenuId, true); + } + + /** + * Init handlers on change (size or handlers). + */ + initHandlers(): void { + if (this.allHandlers) { + this.tabsPlacement = CoreMainMenu.instance.getTabPlacement(); + + const handlers = this.allHandlers.slice(0, CoreMainMenu.instance.getNumItems()); // Get main handlers. + + // Re-build the list of tabs. If a handler is already in the list, use existing object to prevent re-creating the tab. + const newTabs: CoreMainMenuHandlerToDisplay[] = []; + + for (let i = 0; i < handlers.length; i++) { + const handler = handlers[i]; + + // Check if the handler is already in the tabs list. If so, use it. + const tab = this.tabs.find((tab) => tab.title == handler.title && tab.icon == handler.icon); + + tab ? tab.hide = false : null; + handler.hide = false; + + newTabs.push(tab || handler); + } + + // Maintain tab in phantom mode in case is not visible. + const selectedTab = this.mainTabs?.getSelected(); + if (selectedTab) { + const oldTab = this.tabs.find((tab) => tab.page == selectedTab.root && tab.icon == selectedTab.tabIcon); + + if (oldTab) { + // Check if the selected handler is visible. + const isVisible = newTabs.some((newTab) => oldTab.title == newTab.title && oldTab.icon == newTab.icon); + + if (!isVisible) { + oldTab.hide = true; + newTabs.push(oldTab); + } + } + } + + this.tabs = newTabs; + + // Sort them by priority so new handlers are in the right position. + this.tabs.sort((a, b) => (b.priority || 0) - (a.priority || 0)); + + this.loaded = this.menuDelegate.areHandlersLoaded(); + } + + if (this.urlToOpen) { + // There's a content link to open. + // @todo: Treat URL. + } + } + + /** + * Handle a redirect. + * + * @param data Data received. + */ + protected handleRedirect(data: CoreEventLoadPageMainMenuData): void { + // Check if the redirect page is the root page of any of the tabs. + const i = this.tabs.findIndex((tab) => tab.page == data.redirectPage); + + if (i >= 0) { + // Tab found. Set the params. + this.tabs[i].pageParams = Object.assign({}, data.redirectParams); + } else { + // Tab not found, use a phantom tab. + this.redirectPage = data.redirectPage; + this.redirectParams = data.redirectParams; + } + + // Force change detection, otherwise sometimes the tab was selected before the params were applied. + this.changeDetector.detectChanges(); + + setTimeout(() => { + // Let the tab load the params before navigating. + this.mainTabs?.selectTabRootByIndex(i + 1); + }); + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.subscription?.unsubscribe(); + this.redirectObs?.off(); + window.removeEventListener('resize', this.initHandlers.bind(this)); + CoreApp.instance.setMainMenuOpen(this.mainMenuId, false); + this.keyboardObserver?.off(); + } + +} diff --git a/src/app/core/mainmenu/pages/menu/menu.scss b/src/app/core/mainmenu/pages/menu/menu.scss new file mode 100644 index 000000000..608068077 --- /dev/null +++ b/src/app/core/mainmenu/pages/menu/menu.scss @@ -0,0 +1,93 @@ +ion-icon.tab-button-icon { + text-overflow: unset; + overflow: visible; + text-align: center; + transition: margin 500ms ease-in-out, transform 300ms ease-in-out; +} + +.ion-md-fa-graduation-cap, +.ion-ios-fa-graduation-cap, +.ion-ios-fa-graduation-cap-outline, +.ion-fa-graduation-cap { + // @todo @extend .fa-graduation-cap; + // @todo @extend .fa; + font-size: 21px; + height: 21px; + +} + +.ion-ios-fa-graduation-cap-outline { + color: transparent; + -webkit-text-stroke-width: 0.8px; + // @todo -webkit-text-stroke-color: $tabs-tab-color-inactive; + font-size: 23px; + height: 23px; +} + +.ion-md-fa-newspaper-o, +.ion-ios-fa-newspaper-o, +.ion-ios-fa-newspaper-o-outline, +.ion-fa-newspaper-o { + // @todo @extend .fa-newspaper-o; + // @todo @extend .fa; + font-size: 22px; + height: 22px; +} + +.ion-ios-fa-newspaper-o-outline { + font-size: 23px; + height: 23px; +} + +.core-network-message { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding-left: 10px; + padding-right: 10px; + text-align: center; + color: white; + visibility: hidden; + height: 0; + transition: all 500ms ease-in-out; + opacity: .8; +} + +.core-online-message, +.core-offline-message { + display: none; +} + + +/** +.core-online ion-app.app-root page-core-mainmenu, +.core-offline ion-app.app-root page-core-mainmenu { + + core-ion-tabs[tabsplacement="bottom"] ion-icon.tab-button-icon { + margin-bottom: $core-network-message-height / 2; + + &.icon-ios { + margin-bottom: 14px; + } + } + + .core-network-message { + visibility: visible; + height: $core-network-message-height; + pointer-events: none; + } +} + +.core-offline ion-app.app-root page-core-mainmenu .core-offline-message, +.core-online ion-app.app-root page-core-mainmenu .core-online-message { + display: block; +} + +.core-online ion-app.app-root page-core-mainmenu .core-network-message { + background: $green; +} + +.core-offline ion-app.app-root page-core-mainmenu .core-network-message { + background: $red; +}*/ diff --git a/src/app/core/mainmenu/pages/more/more.html b/src/app/core/mainmenu/pages/more/more.html new file mode 100644 index 000000000..c349498a4 --- /dev/null +++ b/src/app/core/mainmenu/pages/more/more.html @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + +

{{siteInfo.fullname}}

+

+

{{ siteUrl }}

+
+
+ + + + + + + +

{{ handler.title | translate}}

+
+ {{handler.badge}} + +
+
+ + + +

{{item.label}}

+
+
+ + + +

{{item.label}}

+
+
+
+ + + +

{{ 'core.scanqr' | translate }}

+
+
+ + + +

{{ 'core.mainmenu.website' | translate }}

+
+
+ + + +

{{ 'core.mainmenu.help' | translate }}

+
+
+ + + +

{{ 'core.settings.preferences' | translate }}

+
+
+ + + +

{{ logoutLabel | translate }}

+
+
+ + + + +

{{ 'core.settings.appsettings' | translate }}

+
+
+
+
diff --git a/src/app/core/mainmenu/pages/more/more.page.ts b/src/app/core/mainmenu/pages/more/more.page.ts new file mode 100644 index 000000000..bd47f45d8 --- /dev/null +++ b/src/app/core/mainmenu/pages/more/more.page.ts @@ -0,0 +1,182 @@ +// (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, OnDestroy } from '@angular/core'; +import { Subscription } from 'rxjs'; + +import { CoreSites } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreSiteInfo } from '@classes/site'; +import { CoreLoginHelper } from '@core/login/services/helper'; +import { CoreMainMenuDelegate, CoreMainMenuHandlerData } from '../../services/delegate'; +import { CoreMainMenu, CoreMainMenuCustomItem } from '../../services/mainmenu'; +import { CoreEventObserver, CoreEvents } from '@singletons/events'; + +/** + * Page that displays the main menu of the app. + */ +@Component({ + selector: 'page-core-mainmenu-more', + templateUrl: 'more.html', + styleUrls: ['more.scss'], +}) +export class CoreMainMenuMorePage implements OnInit, OnDestroy { + + handlers?: CoreMainMenuHandlerData[]; + allHandlers?: CoreMainMenuHandlerData[]; + handlersLoaded = false; + siteInfo?: CoreSiteInfo; + siteName?: string; + logoutLabel?: string; + showScanQR: boolean; + showWeb?: boolean; + showHelp?: boolean; + docsUrl?: string; + customItems?: CoreMainMenuCustomItem[]; + siteUrl?: string; + + protected subscription!: Subscription; + protected langObserver: CoreEventObserver; + protected updateSiteObserver: CoreEventObserver; + + constructor( + protected menuDelegate: CoreMainMenuDelegate, + ) { + + this.langObserver = CoreEvents.on(CoreEvents.LANGUAGE_CHANGED, this.loadSiteInfo.bind(this)); + this.updateSiteObserver = CoreEvents.on( + CoreEvents.SITE_UPDATED, + this.loadSiteInfo.bind(this), + CoreSites.instance.getCurrentSiteId(), + ); + this.loadSiteInfo(); + this.showScanQR = CoreUtils.instance.canScanQR() && + !CoreSites.instance.getCurrentSite()?.isFeatureDisabled('CoreMainMenuDelegate_QrReader'); + } + + /** + * Initialize component. + */ + ngOnInit(): void { + // Load the handlers. + this.subscription = this.menuDelegate.getHandlers().subscribe((handlers) => { + this.allHandlers = handlers; + + this.initHandlers(); + }); + + window.addEventListener('resize', this.initHandlers.bind(this)); + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + window.removeEventListener('resize', this.initHandlers.bind(this)); + this.langObserver?.off(); + this.updateSiteObserver?.off(); + this.subscription?.unsubscribe(); + } + + /** + * Init handlers on change (size or handlers). + */ + initHandlers(): void { + if (!this.allHandlers) { + return; + } + + // Calculate the main handlers not to display them in this view. + const mainHandlers = this.allHandlers + .filter((handler) => !handler.onlyInMore) + .slice(0, CoreMainMenu.instance.getNumItems()); + + // Get only the handlers that don't appear in the main view. + this.handlers = this.allHandlers.filter((handler) => mainHandlers.indexOf(handler) == -1); + + this.handlersLoaded = this.menuDelegate.areHandlersLoaded(); + } + + /** + * Load the site info required by the view. + */ + protected async loadSiteInfo(): Promise { + const currentSite = CoreSites.instance.getCurrentSite(); + + if (!currentSite) { + return; + } + + this.siteInfo = currentSite.getInfo(); + this.siteName = currentSite.getSiteName(); + this.siteUrl = currentSite.getURL(); + this.logoutLabel = CoreLoginHelper.instance.getLogoutLabel(currentSite); + this.showWeb = !currentSite.isFeatureDisabled('CoreMainMenuDelegate_website'); + this.showHelp = !currentSite.isFeatureDisabled('CoreMainMenuDelegate_help'); + + this.docsUrl = await currentSite.getDocsUrl(); + + this.customItems = await CoreMainMenu.instance.getCustomMenuItems(); + } + + /** + * Open a handler. + * + * @param handler Handler to open. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + openHandler(handler: CoreMainMenuHandlerData): void { + // @todo + } + + /** + * Open an embedded custom item. + * + * @param item Item to open. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + openItem(item: CoreMainMenuCustomItem): void { + // @todo + } + + /** + * Open app settings page. + */ + openAppSettings(): void { + // @todo + } + + /** + * Open site settings page. + */ + openSitePreferences(): void { + // @todo + } + + /** + * Scan and treat a QR code. + */ + async scanQR(): Promise { + // Scan for a QR code. + // @todo + } + + /** + * Logout the user. + */ + logout(): void { + CoreSites.instance.logout(); + } + +} diff --git a/src/app/core/mainmenu/pages/more/more.scss b/src/app/core/mainmenu/pages/more/more.scss new file mode 100644 index 000000000..301c56874 --- /dev/null +++ b/src/app/core/mainmenu/pages/more/more.scss @@ -0,0 +1,97 @@ +/* +$core-more-icon: $gray-darker !default; +$core-more-background-ios: $list-ios-background-color !default; +$core-more-background-md: $list-md-background-color !default; +$core-more-activated-background-ios: color-shade($core-more-background-ios) !default; +$core-more-activated-background-md: color-shade($core-more-background-md) !default; +$core-more-divider-ios: $item-ios-divider-background !default; +$core-more-divider-md: $item-md-divider-background !default; +$core-more-border-ios: $list-ios-border-color !default; +$core-more-border-md: $list-md-border-color !default; +$core-more-color-ios: $list-ios-text-color!default; +$core-more-color-md: $list-md-text-color !default; + +.item-block { + &.item-ios { + background-color: $core-more-background-ios; + color: $core-more-color-ios; + p { + color: $core-more-color-ios; + } + + .item-inner { + border-bottom: $hairlines-width solid $core-more-border-ios; + } + } + &.item-md { + background-color: $core-more-background-md; + color: $core-more-color-md; + p { + color: $core-more-color-md; + } + + .item-inner { + border-bottom: 1px solid $core-more-border-md; + } + } + + &.activated { + &.item-ios { + background-color: $core-more-activated-background-ios; + } + &.item-md { + background-color: $core-more-activated-background-md; + } + } +} + +ion-icon { + color: $core-more-icon; +} + +.item-divider { + &.item-ios { + background-color: $core-more-divider-ios; + } + + &.item-md { + background-color: $core-more-divider-md; + border-bottom: $core-more-border-md; + } +} + +@include darkmode() { + ion-icon { + color: $core-dark-text-color; + } + + .item-divider { + &.item-ios, + &.item-md { + color: $core-dark-text-color; + background-color: $core-dark-item-divider-bg-color; + } + } + + .item-block { + &.item-ios, + &.item-md { + color: $core-dark-text-color; + background-color: $core-dark-item-bg-color; + p { + color: $core-dark-text-color; + } + + } + + &.activated { + &.item-ios { + background-color: $core-more-activated-background-ios; + } + &.item-md { + background-color: $core-more-activated-background-md; + } + } + } +} +*/ \ No newline at end of file diff --git a/src/app/core/mainmenu/services/delegate.ts b/src/app/core/mainmenu/services/delegate.ts new file mode 100644 index 000000000..41de92992 --- /dev/null +++ b/src/app/core/mainmenu/services/delegate.ts @@ -0,0 +1,175 @@ +// (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 { Params } from '@angular/router'; +import { Subject, BehaviorSubject } from 'rxjs'; + +import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; +import { CoreEvents } from '@singletons/events'; + +/** + * Interface that all main menu handlers must implement. + */ +export interface CoreMainMenuHandler extends CoreDelegateHandler { + /** + * The highest priority is displayed first. + */ + priority?: number; + + /** + * Returns the data needed to render the handler. + * + * @return Data. + */ + getDisplayData(): CoreMainMenuHandlerData; +} + +/** + * Data needed to render a main menu handler. It's returned by the handler. + */ +export interface CoreMainMenuHandlerData { + /** + * Name of the page to load for the handler. + */ + page: string; + + /** + * Title to display for the handler. + */ + title: string; + + /** + * Name of the icon to display for the handler. + */ + icon: string; // Name of the icon to display in the tab. + + /** + * Class to add to the displayed handler. + */ + class?: string; + + /** + * If the handler has badge to show or not. + */ + showBadge?: boolean; + + /** + * Text to display on the badge. Only used if showBadge is true. + */ + badge?: string; + + /** + * If true, the badge number is being loaded. Only used if showBadge is true. + */ + loading?: boolean; + + /** + * Params to pass to the page. + */ + pageParams?: Params; + + /** + * Whether the handler should only appear in More menu. + */ + onlyInMore?: boolean; +} + +/** + * Data returned by the delegate for each handler. + */ +export interface CoreMainMenuHandlerToDisplay extends CoreMainMenuHandlerData { + /** + * Name of the handler. + */ + name?: string; + + /** + * Priority of the handler. + */ + priority?: number; + + /** + * Hide tab. Used then resizing. + */ + hide?: boolean; +} + +/** + * Service to interact with plugins to be shown in the main menu. Provides functions to register a plugin + * and notify an update in the data. + */ +@Injectable({ + providedIn: 'root', +}) +export class CoreMainMenuDelegate extends CoreDelegate { + + protected loaded = false; + protected siteHandlers: Subject = new BehaviorSubject([]); + protected featurePrefix = 'CoreMainMenuDelegate_'; + + constructor() { + super('CoreMainMenuDelegate', true); + + CoreEvents.on(CoreEvents.LOGOUT, this.clearSiteHandlers.bind(this)); + } + + /** + * Check if handlers are loaded. + * + * @return True if handlers are loaded, false otherwise. + */ + areHandlersLoaded(): boolean { + return this.loaded; + } + + /** + * Clear current site handlers. Reserved for core use. + */ + protected clearSiteHandlers(): void { + this.loaded = false; + this.siteHandlers.next([]); + } + + /** + * Get the handlers for the current site. + * + * @return An observable that will receive the handlers. + */ + getHandlers(): Subject { + return this.siteHandlers; + } + + /** + * Update handlers Data. + */ + updateData(): void { + const displayData: CoreMainMenuHandlerToDisplay[] = []; + + for (const name in this.enabledHandlers) { + const handler = this.enabledHandlers[name]; + const data = handler.getDisplayData(); + + data.name = name; + data.priority = handler.priority; + } + + // Sort them by priority. + displayData.sort((a, b) => (b.priority || 0) - (a.priority || 0)); + + this.loaded = true; + this.siteHandlers.next(displayData); + } + +} diff --git a/src/app/core/mainmenu/services/mainmenu.ts b/src/app/core/mainmenu/services/mainmenu.ts new file mode 100644 index 000000000..2385272d1 --- /dev/null +++ b/src/app/core/mainmenu/services/mainmenu.ts @@ -0,0 +1,275 @@ +// (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 { CoreApp } from '@services/app'; +import { CoreLang } from '@services/lang'; +import { CoreSites } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreConstants } from '@core/constants'; +import { CoreMainMenuDelegate, CoreMainMenuHandlerToDisplay } from './delegate'; +import { makeSingleton } from '@singletons/core.singletons'; + +/** + * Service that provides some features regarding Main Menu. + */ +@Injectable({ + providedIn: 'root', +}) +export class CoreMainMenuProvider { + + static readonly NUM_MAIN_HANDLERS = 4; + static readonly ITEM_MIN_WIDTH = 72; // Min with of every item, based on 5 items on a 360 pixel wide screen. + + protected tablet = false; + + constructor(protected menuDelegate: CoreMainMenuDelegate) { + this.tablet = !!(window?.innerWidth && window.innerWidth >= 576 && window.innerHeight >= 576); + } + + /** + * Get the current main menu handlers. + * + * @return Promise resolved with the current main menu handlers. + */ + getCurrentMainMenuHandlers(): Promise { + const deferred = CoreUtils.instance.promiseDefer(); + + const subscription = this.menuDelegate.getHandlers().subscribe((handlers) => { + subscription?.unsubscribe(); + + // Remove the handlers that should only appear in the More menu. + handlers = handlers.filter(handler => !handler.onlyInMore); + + // Return main handlers. + deferred.resolve(handlers.slice(0, this.getNumItems())); + }); + + return deferred.promise; + } + + /** + * Get a list of custom menu items for a certain site. + * + * @param siteId Site ID. If not defined, current site. + * @return List of custom menu items. + */ + async getCustomMenuItems(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + const itemsString = site.getStoredConfig('tool_mobile_custommenuitems'); + const map: CustomMenuItemsMap = {}; + const result: CoreMainMenuCustomItem[] = []; + + let position = 0; // Position of each item, to keep the same order as it's configured. + + if (!itemsString || typeof itemsString != 'string') { + // Setting not valid. + return result; + } + + // Add items to the map. + const items = itemsString.split(/(?:\r\n|\r|\n)/); + items.forEach((item) => { + const values = item.split('|'); + const label = values[0] ? values[0].trim() : values[0]; + const url = values[1] ? values[1].trim() : values[1]; + const type = values[2] ? values[2].trim() : values[2]; + const lang = (values[3] ? values[3].trim() : values[3]) || 'none'; + let icon = values[4] ? values[4].trim() : values[4]; + + if (!label || !url || !type) { + // Invalid item, ignore it. + return; + } + + const id = url + '#' + type; + if (!icon) { + // Icon not defined, use default one. + icon = type == 'embedded' ? 'fa-square-o' : 'fa-link'; // @todo: Find a better icon for embedded. + } + + if (!map[id]) { + // New entry, add it to the map. + map[id] = { + url: url, + type: type, + position: position, + labels: {}, + }; + position++; + } + + map[id].labels[lang.toLowerCase()] = { + label: label, + icon: icon, + }; + }); + + if (!position) { + // No valid items found, stop. + return result; + } + + const currentLang = await CoreLang.instance.getCurrentLanguage(); + + const fallbackLang = CoreConstants.CONFIG.default_lang || 'en'; + + // Get the right label for each entry and add it to the result. + for (const id in map) { + const entry = map[id]; + let data = entry.labels[currentLang] || entry.labels[currentLang + '_only'] || + entry.labels.none || entry.labels[fallbackLang]; + + if (!data) { + // No valid label found, get the first one that is not "_only". + for (const lang in entry.labels) { + if (lang.indexOf('_only') == -1) { + data = entry.labels[lang]; + break; + } + } + + if (!data) { + // No valid label, ignore this entry. + continue; + } + } + + result[entry.position] = { + url: entry.url, + type: entry.type, + label: data.label, + icon: data.icon, + }; + } + + // Remove undefined values. + return result.filter((entry) => typeof entry != 'undefined'); + } + + /** + * Get the number of items to be shown on the main menu bar. + * + * @return Number of items depending on the device width. + */ + getNumItems(): number { + if (!this.isResponsiveMainMenuItemsDisabledInCurrentSite() && window && window.innerWidth) { + let numElements: number; + + if (this.tablet) { + // Tablet, menu will be displayed vertically. + numElements = Math.floor(window.innerHeight / CoreMainMenuProvider.ITEM_MIN_WIDTH); + } else { + numElements = Math.floor(window.innerWidth / CoreMainMenuProvider.ITEM_MIN_WIDTH); + + // Set a maximum elements to show and skip more button. + numElements = numElements >= 5 ? 5 : numElements; + } + + // Set a mínimum elements to show and skip more button. + return numElements > 1 ? numElements - 1 : 1; + } + + return CoreMainMenuProvider.NUM_MAIN_HANDLERS; + } + + /** + * Get tabs placement depending on the device size. + * + * @return Tabs placement including side value. + */ + getTabPlacement(): string { + const tablet = !!(window.innerWidth && window.innerWidth >= 576 && (window.innerHeight >= 576 || + ((CoreApp.instance.isKeyboardVisible() || CoreApp.instance.isKeyboardOpening()) && window.innerHeight >= 200))); + + if (tablet != this.tablet) { + this.tablet = tablet; + + // @todo Resize so content margins can be updated. + } + + return tablet ? 'side' : 'bottom'; + } + + /** + * Check if a certain page is the root of a main menu handler currently displayed. + * + * @param page Name of the page. + * @param pageParams Page params. + * @return Promise resolved with boolean: whether it's the root of a main menu handler. + */ + async isCurrentMainMenuHandler(pageName: string): Promise { + const handlers = await this.getCurrentMainMenuHandlers(); + + const handler = handlers.find((handler) => handler.page == pageName); + + return !!handler; + } + + /** + * Check if responsive main menu items is disabled in the current site. + * + * @return Whether it's disabled. + */ + protected isResponsiveMainMenuItemsDisabledInCurrentSite(): boolean { + const site = CoreSites.instance.getCurrentSite(); + + return !!site?.isFeatureDisabled('NoDelegate_ResponsiveMainMenuItems'); + } + +} + +export class CoreMainMenu extends makeSingleton(CoreMainMenuProvider) {} + +/** + * Custom main menu item. + */ +export interface CoreMainMenuCustomItem { + /** + * Type of the item: app, inappbrowser, browser or embedded. + */ + type: string; + + /** + * Url of the item. + */ + url: string; + + /** + * Label to display for the item. + */ + label: string; + + /** + * Name of the icon to display for the item. + */ + icon: string; +} + +/** + * Map of custom menu items. + */ +type CustomMenuItemsMap = Record; diff --git a/src/app/services/sites.ts b/src/app/services/sites.ts index ed32a9dbc..9292eacf4 100644 --- a/src/app/services/sites.ts +++ b/src/app/services/sites.ts @@ -1312,7 +1312,7 @@ export class CoreSitesProvider { async logout(): Promise { await this.dbReady; - let siteId; + let siteId: string | undefined; const promises: Promise[] = []; if (this.currentSite) { diff --git a/src/app/singletons/events.ts b/src/app/singletons/events.ts index 51152d929..4bc192fa3 100644 --- a/src/app/singletons/events.ts +++ b/src/app/singletons/events.ts @@ -208,3 +208,11 @@ export type CoreEventLoadingChangedData = { loaded: boolean; uniqueId: string; }; + +/** + * Data passed to LOAD_PAGE_MAIN_MENU event. + */ +export type CoreEventLoadPageMainMenuData = { + redirectPage: string; + redirectParams?: Params; +}; diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 2c01ee081..29e9ac335 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -304,6 +304,10 @@ "core.browser": "Browser", "core.copiedtoclipboard": "Text copied to clipboard", "core.login.yourenteredsite": "Connect to your site", + "core.mainmenu.changesite": "Change site", + "core.mainmenu.help": "Help", + "core.mainmenu.logout": "Log out", + "core.mainmenu.website": "Website", "core.no": "No", "core.offline": "Offline", "core.ok": "OK", From 0c3d5293d4e368b44e991385d276f3710e0af845 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 23 Oct 2020 11:02:56 +0200 Subject: [PATCH 2/6] MOBILE-3565 settings: Fix settings module name --- src/app/app-routing.module.ts | 2 +- src/app/core/settings/settings.module.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 681958412..2045643b0 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -27,7 +27,7 @@ const routes: Routes = [ }, { path: 'settings', - loadChildren: () => import('./core/settings/settings.module').then( m => m.CoreAppSettingsPageModule), + loadChildren: () => import('./core/settings/settings.module').then( m => m.CoreAppSettingsModule), }, { path: 'mainmenu', diff --git a/src/app/core/settings/settings.module.ts b/src/app/core/settings/settings.module.ts index cef43ab4c..7e8f5cf66 100644 --- a/src/app/core/settings/settings.module.ts +++ b/src/app/core/settings/settings.module.ts @@ -43,4 +43,4 @@ import { CoreSettingsDeviceInfoPage } from './pages/deviceinfo/deviceinfo.page'; CoreSettingsDeviceInfoPage, ], }) -export class CoreAppSettingsPageModule {} +export class CoreAppSettingsModule {} From 1d794f42b41d5eb585b1107135e34ec5285d6d3f Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 26 Oct 2020 08:41:46 +0100 Subject: [PATCH 3/6] MOBILE-3565 directives: Implement core-link directive --- src/app/core/mainmenu/pages/more/more.html | 2 +- src/app/directives/directives.module.ts | 5 +- src/app/directives/format-text.ts | 5 +- src/app/directives/link.ts | 198 +++++++++++++++++++++ src/types/global.d.ts | 1 + 5 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 src/app/directives/link.ts diff --git a/src/app/core/mainmenu/pages/more/more.html b/src/app/core/mainmenu/pages/more/more.html index c349498a4..206208638 100644 --- a/src/app/core/mainmenu/pages/more/more.html +++ b/src/app/core/mainmenu/pages/more/more.html @@ -30,7 +30,7 @@
- +

{{item.label}}

diff --git a/src/app/directives/directives.module.ts b/src/app/directives/directives.module.ts index 0d389ce22..40c292f36 100644 --- a/src/app/directives/directives.module.ts +++ b/src/app/directives/directives.module.ts @@ -16,16 +16,18 @@ import { NgModule } from '@angular/core'; import { CoreAutoFocusDirective } from './auto-focus'; import { CoreExternalContentDirective } from './external-content'; +import { CoreFabDirective } from './fab'; import { CoreFormatTextDirective } from './format-text'; +import { CoreLinkDirective } from './link'; import { CoreLongPressDirective } from './long-press'; import { CoreSupressEventsDirective } from './supress-events'; -import { CoreFabDirective } from './fab'; @NgModule({ declarations: [ CoreAutoFocusDirective, CoreExternalContentDirective, CoreFormatTextDirective, + CoreLinkDirective, CoreLongPressDirective, CoreSupressEventsDirective, CoreFabDirective, @@ -35,6 +37,7 @@ import { CoreFabDirective } from './fab'; CoreAutoFocusDirective, CoreExternalContentDirective, CoreFormatTextDirective, + CoreLinkDirective, CoreLongPressDirective, CoreSupressEventsDirective, CoreFabDirective, diff --git a/src/app/directives/format-text.ts b/src/app/directives/format-text.ts index 56ac42cee..da1baf562 100644 --- a/src/app/directives/format-text.ts +++ b/src/app/directives/format-text.ts @@ -24,6 +24,7 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreSite } from '@classes/site'; import { Translate } from '@singletons/core.singletons'; import { CoreExternalContentDirective } from './external-content'; +import { CoreLinkDirective } from './link'; /** * Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective @@ -454,7 +455,9 @@ export class CoreFormatTextDirective implements OnChanges { // Important: We need to look for links first because in 'img' we add new links without core-link. anchors.forEach((anchor) => { // Angular 2 doesn't let adding directives dynamically. Create the CoreLinkDirective manually. - // @todo + const linkDir = new CoreLinkDirective(new ElementRef(anchor), this.content); + linkDir.capture = true; + linkDir.ngOnInit(); this.addExternalContent(anchor); }); diff --git a/src/app/directives/link.ts b/src/app/directives/link.ts new file mode 100644 index 000000000..c220ffc39 --- /dev/null +++ b/src/app/directives/link.ts @@ -0,0 +1,198 @@ +// (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 { Directive, Input, OnInit, ElementRef, Optional } from '@angular/core'; +import { IonContent } from '@ionic/angular'; + +import { CoreFileHelper } from '@services/file-helper'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreConstants } from '@core/constants'; + +/** + * Directive to open a link in external browser or in the app. + */ +@Directive({ + selector: '[core-link]', +}) +export class CoreLinkDirective implements OnInit { + + @Input() capture?: boolean | string; // If the link needs to be captured by the app. + @Input() inApp?: boolean | string; // True to open in embedded browser, false to open in system browser. + /* Whether the link should be opened with auto-login. Accepts the following values: + "yes" -> Always auto-login. + "no" -> Never auto-login. + "check" -> Auto-login only if it points to the current site. Default value. */ + @Input() autoLogin = 'check'; + + protected element: Element; + + constructor( + element: ElementRef, + @Optional() protected content: IonContent, + ) { + this.element = element.nativeElement; + } + + /** + * Function executed when the component is initialized. + */ + ngOnInit(): void { + this.inApp = typeof this.inApp == 'undefined' ? this.inApp : CoreUtils.instance.isTrueOrOne(this.inApp); + + // @todo: Handle split view? + + this.element.addEventListener('click', (event) => { + if (event.defaultPrevented) { + return; // Link already treated, stop. + } + + let href = this.element.getAttribute('href') || this.element.getAttribute('ng-reflect-href') || + this.element.getAttribute('xlink:href'); + + if (!href || CoreUrlUtils.instance.getUrlScheme(href) == 'javascript') { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + const openIn = this.element.getAttribute('data-open-in'); + + if (CoreUtils.instance.isTrueOrOne(this.capture)) { + href = CoreTextUtils.instance.decodeURI(href); + + // @todo: Handle link. + this.navigate(href, openIn); + } else { + this.navigate(href, openIn); + } + }); + } + + /** + * Convenience function to correctly navigate, open file or url in the browser. + * + * @param href HREF to be opened. + * @param openIn Open In App value coming from data-open-in attribute. + * @return Promise resolved when done. + */ + protected async navigate(href: string, openIn?: string | null): Promise { + + if (CoreUrlUtils.instance.isLocalFileUrl(href)) { + return this.openLocalFile(href); + } + + if (href.charAt(0) == '#') { + // Look for id or name. + href = href.substr(1); + CoreDomUtils.instance.scrollToElementBySelector(this.content, '#' + href + ', [name=\'' + href + '\']'); + + return; + } + + // @todo: Custom URL schemes. + + return this.openExternalLink(href, openIn); + } + + /** + * Open a local file. + * + * @param path Path to the file. + * @return Promise resolved when done. + */ + protected async openLocalFile(path: string): Promise { + const filename = path.substr(path.lastIndexOf('/') + 1); + + if (!CoreFileHelper.instance.isOpenableInApp({ filename })) { + try { + await CoreFileHelper.instance.showConfirmOpenUnsupportedFile(); + } catch (error) { + return; // Cancelled, stop. + } + } + + try { + await CoreUtils.instance.openFile(path); + } catch (error) { + CoreDomUtils.instance.showErrorModal(error); + } + } + + /** + * Open an external link in the app or in browser. + * + * @param href HREF to be opened. + * @param openIn Open In App value coming from data-open-in attribute. + * @return Promise resolved when done. + */ + protected async openExternalLink(href: string, openIn?: string | null): Promise { + // It's an external link, we will open with browser. Check if we need to auto-login. + if (!CoreSites.instance.isLoggedIn()) { + // Not logged in, cannot auto-login. + if (this.inApp) { + CoreUtils.instance.openInApp(href); + } else { + CoreUtils.instance.openInBrowser(href); + } + + return; + } + + // Check if URL does not have any protocol, so it's a relative URL. + if (!CoreUrlUtils.instance.isAbsoluteURL(href)) { + // Add the site URL at the begining. + if (href.charAt(0) == '/') { + href = CoreSites.instance.getCurrentSite()!.getURL() + href; + } else { + href = CoreSites.instance.getCurrentSite()!.getURL() + '/' + href; + } + } + + if (this.autoLogin == 'yes') { + if (this.inApp) { + await CoreSites.instance.getCurrentSite()!.openInAppWithAutoLogin(href); + } else { + await CoreSites.instance.getCurrentSite()!.openInBrowserWithAutoLogin(href); + } + } else if (this.autoLogin == 'no') { + if (this.inApp) { + CoreUtils.instance.openInApp(href); + } else { + CoreUtils.instance.openInBrowser(href); + } + } else { + // Priority order is: core-link inApp attribute > forceOpenLinksIn setting > data-open-in HTML attribute. + let openInApp = this.inApp; + if (typeof this.inApp == 'undefined') { + if (CoreConstants.CONFIG.forceOpenLinksIn == 'browser') { + openInApp = false; + } else if (CoreConstants.CONFIG.forceOpenLinksIn == 'app' || openIn == 'app') { + openInApp = true; + } + } + + if (openInApp) { + await CoreSites.instance.getCurrentSite()!.openInAppWithAutoLoginIfSameSite(href); + } else { + await CoreSites.instance.getCurrentSite()!.openInBrowserWithAutoLoginIfSameSite(href); + } + } + } + +} diff --git a/src/types/global.d.ts b/src/types/global.d.ts index fe9f98a26..1e05ea6d4 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -66,6 +66,7 @@ declare global { appstores: Record; displayqroncredentialscreen?: boolean; displayqronsitescreen?: boolean; + forceOpenLinksIn: 'app' | 'browser'; }; BUILD: { From 06c7035834e2145b2104c3778682edface10435b Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 26 Oct 2020 10:03:32 +0100 Subject: [PATCH 4/6] MOBILE-3565 core: Fix errors when compiling in prod mode --- src/app/components/loading/loading.ts | 8 ++++---- src/app/components/show-password/show-password.ts | 4 ++-- src/app/core/login/pages/site/site.page.ts | 4 ++-- src/app/core/mainmenu/pages/more/more.html | 2 +- src/app/core/mainmenu/pages/more/more.page.ts | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/app/components/loading/loading.ts b/src/app/components/loading/loading.ts index 31298fa19..a255e52d7 100644 --- a/src/app/components/loading/loading.ts +++ b/src/app/components/loading/loading.ts @@ -50,20 +50,20 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit { @Input() message?: string; // Message to show while loading. @ViewChild('content') content?: ElementRef; - protected uniqueId!: string; + uniqueId: string; protected element: HTMLElement; // Current element. constructor(element: ElementRef) { this.element = element.nativeElement; + + // Calculate the unique ID. + this.uniqueId = 'core-loading-content-' + CoreUtils.instance.getUniqueId('CoreLoadingComponent'); } /** * Component being initialized. */ ngOnInit(): void { - // Calculate the unique ID. - this.uniqueId = 'core-loading-content-' + CoreUtils.instance.getUniqueId('CoreLoadingComponent'); - if (!this.message) { // Default loading message. this.message = Translate.instance.instant('core.loading'); diff --git a/src/app/components/show-password/show-password.ts b/src/app/components/show-password/show-password.ts index 12dafefe4..6cec76522 100644 --- a/src/app/components/show-password/show-password.ts +++ b/src/app/components/show-password/show-password.ts @@ -45,8 +45,8 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit { @ContentChild(IonInput) ionInput?: IonInput; shown!: boolean; // Whether the password is shown. - label?: string; // Label for the button to show/hide. - iconName?: string; // Name of the icon of the button to show/hide. + label!: string; // Label for the button to show/hide. + iconName!: string; // Name of the icon of the button to show/hide. selector = ''; // Selector to identify the input. protected input?: HTMLInputElement | null; // Input affected. diff --git a/src/app/core/login/pages/site/site.page.ts b/src/app/core/login/pages/site/site.page.ts index 4ce8d7381..486d9796e 100644 --- a/src/app/core/login/pages/site/site.page.ts +++ b/src/app/core/login/pages/site/site.page.ts @@ -417,8 +417,8 @@ export class CoreLoginSitePage implements OnInit { * @param event Received Event. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any - filterChanged(event: any): void { - const newValue = event.target.value?.trim().toLowerCase(); + filterChanged(event?: any): void { + const newValue = event?.target.value?.trim().toLowerCase(); if (!newValue || !this.fixedSites) { this.filteredSites = this.fixedSites; } else { diff --git a/src/app/core/mainmenu/pages/more/more.html b/src/app/core/mainmenu/pages/more/more.html index 206208638..2426cab14 100644 --- a/src/app/core/mainmenu/pages/more/more.html +++ b/src/app/core/mainmenu/pages/more/more.html @@ -49,7 +49,7 @@

{{ 'core.scanqr' | translate }}

- +

{{ 'core.mainmenu.website' | translate }}

diff --git a/src/app/core/mainmenu/pages/more/more.page.ts b/src/app/core/mainmenu/pages/more/more.page.ts index bd47f45d8..b26b751cc 100644 --- a/src/app/core/mainmenu/pages/more/more.page.ts +++ b/src/app/core/mainmenu/pages/more/more.page.ts @@ -38,7 +38,7 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy { handlersLoaded = false; siteInfo?: CoreSiteInfo; siteName?: string; - logoutLabel?: string; + logoutLabel = 'core.mainmenu.changesite'; showScanQR: boolean; showWeb?: boolean; showHelp?: boolean; From 3a831c3ef1ec44c3cc7e24b1e8ae3a8c3e153cff Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 26 Oct 2020 13:10:46 +0100 Subject: [PATCH 5/6] MOBILE-3565 home: Add a Hello World as Home page --- src/app/app.module.ts | 2 + src/app/core/courses/courses.module.ts | 45 ++++++++++ src/app/core/courses/handlers/mainmenu.ts | 60 +++++++++++++ src/app/core/courses/lang/en.json | 39 +++++++++ src/app/core/courses/pages/home/home.html | 20 +++++ src/app/core/courses/pages/home/home.page.ts | 36 ++++++++ src/app/core/courses/pages/home/home.scss | 26 ++++++ .../core/mainmenu/mainmenu-routing.module.ts | 12 ++- src/app/core/mainmenu/pages/menu/menu.html | 14 +-- src/app/core/mainmenu/pages/menu/menu.page.ts | 86 +++++++++++++------ src/app/core/mainmenu/services/delegate.ts | 2 + src/assets/lang/en.json | 37 ++++++++ 12 files changed, 345 insertions(+), 34 deletions(-) create mode 100644 src/app/core/courses/courses.module.ts create mode 100644 src/app/core/courses/handlers/mainmenu.ts create mode 100644 src/app/core/courses/lang/en.json create mode 100644 src/app/core/courses/pages/home/home.html create mode 100644 src/app/core/courses/pages/home/home.page.ts create mode 100644 src/app/core/courses/pages/home/home.scss diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 03e57faf0..16208a998 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -53,6 +53,7 @@ import { CoreUtilsProvider } from '@services/utils/utils'; // Import core modules. import { CoreEmulatorModule } from '@core/emulator/emulator.module'; import { CoreLoginModule } from '@core/login/login.module'; +import { CoreCoursesModule } from '@core/courses/courses.module'; import { setSingletonsInjector } from '@singletons/core.singletons'; @@ -81,6 +82,7 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { AppRoutingModule, CoreEmulatorModule, CoreLoginModule, + CoreCoursesModule, ], providers: [ { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, diff --git a/src/app/core/courses/courses.module.ts b/src/app/core/courses/courses.module.ts new file mode 100644 index 000000000..c35f74b4f --- /dev/null +++ b/src/app/core/courses/courses.module.ts @@ -0,0 +1,45 @@ +// (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 { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; + +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CoreMainMenuDelegate } from '@core/mainmenu/services/delegate'; + +import { CoreHomeMainMenuHandler } from './handlers/mainmenu'; +import { CoreCoursesHomePage } from './pages/home/home.page'; + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + ], + declarations: [ + CoreCoursesHomePage, + ], +}) +export class CoreCoursesModule { + + constructor(mainMenuDelegate: CoreMainMenuDelegate) { + mainMenuDelegate.registerHandler(new CoreHomeMainMenuHandler()); + } + +} diff --git a/src/app/core/courses/handlers/mainmenu.ts b/src/app/core/courses/handlers/mainmenu.ts new file mode 100644 index 000000000..337ddaed5 --- /dev/null +++ b/src/app/core/courses/handlers/mainmenu.ts @@ -0,0 +1,60 @@ +// (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 { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@core/mainmenu/services/delegate'; + +/** + * Handler to add Home into main menu. + */ +export class CoreHomeMainMenuHandler implements CoreMainMenuHandler { + + name = 'CoreHome'; + priority = 1100; + + /** + * Check if the handler is enabled on a site level. + * + * @return Whether or not the handler is enabled on a site level. + */ + isEnabled(): Promise { + return this.isEnabledForSite(); + } + + /** + * Check if the handler is enabled on a certain site. + * + * @param siteId Site ID. If not defined, current site. + * @return Whether or not the handler is enabled on a site level. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async isEnabledForSite(siteId?: string): Promise { + // @todo + return true; + } + + /** + * Returns the data needed to render the handler. + * + * @return Data needed to render the handler. + */ + getDisplayData(): CoreMainMenuHandlerData { + return { + icon: 'fa-home', + title: 'core.courses.mymoodle', + page: 'home', + class: 'core-home-handler', + }; + } + +} diff --git a/src/app/core/courses/lang/en.json b/src/app/core/courses/lang/en.json new file mode 100644 index 000000000..f9aa10d0c --- /dev/null +++ b/src/app/core/courses/lang/en.json @@ -0,0 +1,39 @@ +{ + "addtofavourites": "Star this course", + "allowguests": "This course allows guest users to enter", + "availablecourses": "Available courses", + "cannotretrievemorecategories": "Categories deeper than level {{$a}} cannot be retrieved.", + "categories": "Course categories", + "confirmselfenrol": "Are you sure you want to enrol yourself in this course?", + "courses": "Courses", + "downloadcourses": "Download courses", + "enrolme": "Enrol me", + "errorloadcategories": "An error occurred while loading categories.", + "errorloadcourses": "An error occurred while loading courses.", + "errorloadplugins": "The plugins required by this course could not be loaded correctly. Please reload the app to try again.", + "errorsearching": "An error occurred while searching.", + "errorselfenrol": "An error occurred while self enrolling.", + "filtermycourses": "Filter my courses", + "frontpage": "Front page", + "hidecourse": "Remove from view", + "ignore": "Ignore", + "mycourses": "My courses", + "mymoodle": "Dashboard", + "nocourses": "No course information to show.", + "nocoursesyet": "No courses in this category", + "nosearchresults": "No results", + "notenroled": "You are not enrolled in this course", + "notenrollable": "You cannot enrol yourself in this course.", + "password": "Enrolment key", + "paymentrequired": "This course requires a payment for entry.", + "paypalaccepted": "PayPal payments accepted", + "reload": "Reload", + "removefromfavourites": "Unstar this course", + "search": "Search", + "searchcourses": "Search courses", + "searchcoursesadvice": "You can use the search courses button to find courses to access as a guest or enrol yourself in courses that allow it.", + "selfenrolment": "Self enrolment", + "sendpaymentbutton": "Send payment via PayPal", + "show": "Restore to view", + "totalcoursesearchresults": "Total courses: {{$a}}" +} \ No newline at end of file diff --git a/src/app/core/courses/pages/home/home.html b/src/app/core/courses/pages/home/home.html new file mode 100644 index 000000000..79b79100d --- /dev/null +++ b/src/app/core/courses/pages/home/home.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + Home page. + \ No newline at end of file diff --git a/src/app/core/courses/pages/home/home.page.ts b/src/app/core/courses/pages/home/home.page.ts new file mode 100644 index 000000000..1664c9d14 --- /dev/null +++ b/src/app/core/courses/pages/home/home.page.ts @@ -0,0 +1,36 @@ +// (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 the Home. + */ +@Component({ + selector: 'page-core-courses-home', + templateUrl: 'home.html', + styleUrls: ['home.scss'], +}) +export class CoreCoursesHomePage implements OnInit { + + siteName = 'Hello world'; + + /** + * Initialize the component. + */ + ngOnInit(): void { + // @todo + } + +} diff --git a/src/app/core/courses/pages/home/home.scss b/src/app/core/courses/pages/home/home.scss new file mode 100644 index 000000000..4f17ae77c --- /dev/null +++ b/src/app/core/courses/pages/home/home.scss @@ -0,0 +1,26 @@ +$core-dashboard-logo: false !default; + +@if $core-dashboard-logo { + .toolbar-title-default.md .title-default .core-header-logo { + max-height: $toolbar-md-height - 24; + } + + .toolbar-title-default.ios .title-default .core-header-logo { + max-height: $toolbar-ios-height - 24; + } + + .toolbar-title-default .title-default core-format-text { + display: none; + } +} @else { + .toolbar-title-default .core-header-logo { + display: none; + } +} + +ion-badge.core-course-download-courses-progress { + display: block; + // @include float(start); + // @include margin(12px, 12px, null, 12px); +} + diff --git a/src/app/core/mainmenu/mainmenu-routing.module.ts b/src/app/core/mainmenu/mainmenu-routing.module.ts index 2ad58ff75..fb2998542 100644 --- a/src/app/core/mainmenu/mainmenu-routing.module.ts +++ b/src/app/core/mainmenu/mainmenu-routing.module.ts @@ -17,15 +17,25 @@ import { RouterModule, Routes } from '@angular/router'; import { CoreMainMenuPage } from './pages/menu/menu.page'; import { CoreMainMenuMorePage } from './pages/more/more.page'; +import { CoreCoursesHomePage } from '../courses/pages/home/home.page'; const routes: Routes = [ { path: '', component: CoreMainMenuPage, children: [ + { + path: 'home', // @todo: Add this route dynamically. + component: CoreCoursesHomePage, + }, { path: 'more', - component: CoreMainMenuMorePage, + children: [ + { + path: '', + component: CoreMainMenuMorePage, + }, + ], }, ], }, diff --git a/src/app/core/mainmenu/pages/menu/menu.html b/src/app/core/mainmenu/pages/menu/menu.html index 8c53535dc..97dc5dcc1 100644 --- a/src/app/core/mainmenu/pages/menu/menu.html +++ b/src/app/core/mainmenu/pages/menu/menu.html @@ -1,13 +1,17 @@ - - - + - + + + + + + {{ tab.title | translate }} + {{ tab.badge }} - + {{ 'core.more' | translate }} diff --git a/src/app/core/mainmenu/pages/menu/menu.page.ts b/src/app/core/mainmenu/pages/menu/menu.page.ts index 364853780..47dddd926 100644 --- a/src/app/core/mainmenu/pages/menu/menu.page.ts +++ b/src/app/core/mainmenu/pages/menu/menu.page.ts @@ -13,8 +13,8 @@ // limitations under the License. import { Component, OnInit, OnDestroy, ViewChild, ChangeDetectorRef } from '@angular/core'; -import { ActivatedRoute, Params } from '@angular/router'; -import { NavController } from '@ionic/angular'; +import { ActivatedRoute, Params, Router } from '@angular/router'; +import { NavController, IonTabs } from '@ionic/angular'; import { Subscription } from 'rxjs'; import { CoreApp } from '@services/app'; @@ -22,6 +22,9 @@ import { CoreSites } from '@services/sites'; import { CoreEvents, CoreEventObserver, CoreEventLoadPageMainMenuData } from '@singletons/events'; import { CoreMainMenu } from '../../services/mainmenu'; import { CoreMainMenuDelegate, CoreMainMenuHandlerToDisplay } from '../../services/delegate'; +import { CoreUtils } from '@/app/services/utils/utils'; +import { CoreDomUtils } from '@/app/services/utils/dom'; +import { Translate } from '@/app/singletons/core.singletons'; /** * Page that displays the main menu of the app. @@ -48,13 +51,14 @@ export class CoreMainMenuPage implements OnInit, OnDestroy { protected mainMenuId: number; protected keyboardObserver?: CoreEventObserver; - @ViewChild('mainTabs') mainTabs?: any; // CoreIonTabsComponent; + @ViewChild('mainTabs') mainTabs?: IonTabs; constructor( protected route: ActivatedRoute, protected navCtrl: NavController, protected menuDelegate: CoreMainMenuDelegate, protected changeDetector: ChangeDetectorRef, + protected router: Router, ) { this.mainMenuId = CoreApp.instance.getMainMenuId(); } @@ -154,28 +158,19 @@ export class CoreMainMenuPage implements OnInit, OnDestroy { newTabs.push(tab || handler); } - // Maintain tab in phantom mode in case is not visible. - const selectedTab = this.mainTabs?.getSelected(); - if (selectedTab) { - const oldTab = this.tabs.find((tab) => tab.page == selectedTab.root && tab.icon == selectedTab.tabIcon); - - if (oldTab) { - // Check if the selected handler is visible. - const isVisible = newTabs.some((newTab) => oldTab.title == newTab.title && oldTab.icon == newTab.icon); - - if (!isVisible) { - oldTab.hide = true; - newTabs.push(oldTab); - } - } - } - this.tabs = newTabs; // Sort them by priority so new handlers are in the right position. this.tabs.sort((a, b) => (b.priority || 0) - (a.priority || 0)); this.loaded = this.menuDelegate.areHandlersLoaded(); + + if (this.loaded && this.mainTabs && !this.mainTabs.getSelected()) { + // Select the first tab. + setTimeout(() => { + this.mainTabs!.select(this.tabs[0]?.page || 'more'); + }); + } } if (this.urlToOpen) { @@ -194,21 +189,18 @@ export class CoreMainMenuPage implements OnInit, OnDestroy { const i = this.tabs.findIndex((tab) => tab.page == data.redirectPage); if (i >= 0) { - // Tab found. Set the params. - this.tabs[i].pageParams = Object.assign({}, data.redirectParams); + // Tab found. Open it with the params. + this.navCtrl.navigateForward(data.redirectPage, { + queryParams: data.redirectParams, + animated: false, + }); } else { // Tab not found, use a phantom tab. - this.redirectPage = data.redirectPage; - this.redirectParams = data.redirectParams; + // @todo } // Force change detection, otherwise sometimes the tab was selected before the params were applied. this.changeDetector.detectChanges(); - - setTimeout(() => { - // Let the tab load the params before navigating. - this.mainTabs?.selectTabRootByIndex(i + 1); - }); } /** @@ -222,4 +214,42 @@ export class CoreMainMenuPage implements OnInit, OnDestroy { this.keyboardObserver?.off(); } + /** + * Tab clicked. + * + * @param e Event. + * @param page Page of the tab. + */ + async tabClicked(e: Event, page: string): Promise { + if (this.mainTabs?.getSelected() != page) { + // Just change the tab. + return; + } + + // Current tab was clicked. Check if user is already at root level. + if (this.router.url == '/mainmenu/' + page) { + // Already at root level, nothing to do. + return; + } + + // Ask the user if he wants to go back to the root page of the tab. + e.preventDefault(); + e.stopPropagation(); + + try { + const tab = this.tabs.find((tab) => tab.page == page); + + if (tab?.title) { + await CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.confirmgotabroot', { name: tab.title })); + } else { + await CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.confirmgotabrootdefault')); + } + + // User confirmed, go to root. + this.mainTabs?.select(page); + } catch (error) { + // User canceled. + } + } + } diff --git a/src/app/core/mainmenu/services/delegate.ts b/src/app/core/mainmenu/services/delegate.ts index 41de92992..dd2867fa4 100644 --- a/src/app/core/mainmenu/services/delegate.ts +++ b/src/app/core/mainmenu/services/delegate.ts @@ -163,6 +163,8 @@ export class CoreMainMenuDelegate extends CoreDelegate { data.name = name; data.priority = handler.priority; + + displayData.push(data); } // Sort them by priority. diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 29e9ac335..183d1f99f 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -303,6 +303,43 @@ "core.back": "Back", "core.browser": "Browser", "core.copiedtoclipboard": "Text copied to clipboard", + "core.courses.addtofavourites": "Star this course", + "core.courses.allowguests": "This course allows guest users to enter", + "core.courses.availablecourses": "Available courses", + "core.courses.cannotretrievemorecategories": "Categories deeper than level {{$a}} cannot be retrieved.", + "core.courses.categories": "Course categories", + "core.courses.confirmselfenrol": "Are you sure you want to enrol yourself in this course?", + "core.courses.courses": "Courses", + "core.courses.downloadcourses": "Download courses", + "core.courses.enrolme": "Enrol me", + "core.courses.errorloadcategories": "An error occurred while loading categories.", + "core.courses.errorloadcourses": "An error occurred while loading courses.", + "core.courses.errorloadplugins": "The plugins required by this course could not be loaded correctly. Please reload the app to try again.", + "core.courses.errorsearching": "An error occurred while searching.", + "core.courses.errorselfenrol": "An error occurred while self enrolling.", + "core.courses.filtermycourses": "Filter my courses", + "core.courses.frontpage": "Front page", + "core.courses.hidecourse": "Remove from view", + "core.courses.ignore": "Ignore", + "core.courses.mycourses": "My courses", + "core.courses.mymoodle": "Dashboard", + "core.courses.nocourses": "No course information to show.", + "core.courses.nocoursesyet": "No courses in this category", + "core.courses.nosearchresults": "No results", + "core.courses.notenroled": "You are not enrolled in this course", + "core.courses.notenrollable": "You cannot enrol yourself in this course.", + "core.courses.password": "Enrolment key", + "core.courses.paymentrequired": "This course requires a payment for entry.", + "core.courses.paypalaccepted": "PayPal payments accepted", + "core.courses.reload": "Reload", + "core.courses.removefromfavourites": "Unstar this course", + "core.courses.search": "Search", + "core.courses.searchcourses": "Search courses", + "core.courses.searchcoursesadvice": "You can use the search courses button to find courses to access as a guest or enrol yourself in courses that allow it.", + "core.courses.selfenrolment": "Self enrolment", + "core.courses.sendpaymentbutton": "Send payment via PayPal", + "core.courses.show": "Restore to view", + "core.courses.totalcoursesearchresults": "Total courses: {{$a}}", "core.login.yourenteredsite": "Connect to your site", "core.mainmenu.changesite": "Change site", "core.mainmenu.help": "Help", From ded36482d73490aae6750df13a82f6d51c1b4be6 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 26 Oct 2020 14:17:27 +0100 Subject: [PATCH 6/6] MOBILE-3565 modules: Create a module for each page --- src/app/app-routing.module.ts | 2 +- src/app/core/courses/courses.module.ts | 19 +------ .../courses/pages/home/home.page.module.ts | 47 +++++++++++++++++ src/app/core/login/login-routing.module.ts | 13 ++--- src/app/core/login/login.module.ts | 28 +---------- .../login/pages/credentials/credentials.html | 5 +- .../credentials/credentials.page.module.ts | 50 +++++++++++++++++++ .../core/login/pages/init/init.page.module.ts | 38 ++++++++++++++ .../core/login/pages/site/site.page.module.ts | 50 +++++++++++++++++++ .../login/pages/sites/sites.page.module.ts | 47 +++++++++++++++++ .../core/mainmenu/mainmenu-routing.module.ts | 3 +- .../settings/pages/about/about.page.module.ts | 47 +++++++++++++++++ .../settings/pages/app/app.page.module.ts | 47 +++++++++++++++++ src/app/core/settings/pages/app/app.page.ts | 2 +- .../deviceinfo/deviceinfo.page.module.ts | 47 +++++++++++++++++ .../core/settings/settings-routing.module.ts | 11 ++-- src/app/core/settings/settings.module.ts | 29 ++--------- 17 files changed, 395 insertions(+), 90 deletions(-) create mode 100644 src/app/core/courses/pages/home/home.page.module.ts create mode 100644 src/app/core/login/pages/credentials/credentials.page.module.ts create mode 100644 src/app/core/login/pages/init/init.page.module.ts create mode 100644 src/app/core/login/pages/site/site.page.module.ts create mode 100644 src/app/core/login/pages/sites/sites.page.module.ts create mode 100644 src/app/core/settings/pages/about/about.page.module.ts create mode 100644 src/app/core/settings/pages/app/app.page.module.ts create mode 100644 src/app/core/settings/pages/deviceinfo/deviceinfo.page.module.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 2045643b0..ed4cf9987 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -27,7 +27,7 @@ const routes: Routes = [ }, { path: 'settings', - loadChildren: () => import('./core/settings/settings.module').then( m => m.CoreAppSettingsModule), + loadChildren: () => import('./core/settings/settings.module').then( m => m.CoreSettingsModule), }, { path: 'mainmenu', diff --git a/src/app/core/courses/courses.module.ts b/src/app/core/courses/courses.module.ts index c35f74b4f..38449fa0a 100644 --- a/src/app/core/courses/courses.module.ts +++ b/src/app/core/courses/courses.module.ts @@ -12,29 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; -import { IonicModule } from '@ionic/angular'; -import { TranslateModule } from '@ngx-translate/core'; -import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreMainMenuDelegate } from '@core/mainmenu/services/delegate'; - import { CoreHomeMainMenuHandler } from './handlers/mainmenu'; -import { CoreCoursesHomePage } from './pages/home/home.page'; @NgModule({ - imports: [ - CommonModule, - IonicModule, - TranslateModule.forChild(), - CoreComponentsModule, - CoreDirectivesModule, - ], - declarations: [ - CoreCoursesHomePage, - ], + imports: [], + declarations: [], }) export class CoreCoursesModule { diff --git a/src/app/core/courses/pages/home/home.page.module.ts b/src/app/core/courses/pages/home/home.page.module.ts new file mode 100644 index 000000000..9397ce678 --- /dev/null +++ b/src/app/core/courses/pages/home/home.page.module.ts @@ -0,0 +1,47 @@ +// (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'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; + +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +import { CoreCoursesHomePage } from './home.page'; + +const routes: Routes = [ + { + path: '', + component: CoreCoursesHomePage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + ], + declarations: [ + CoreCoursesHomePage, + ], + exports: [RouterModule], +}) +export class CoreCoursesHomePageModule {} diff --git a/src/app/core/login/login-routing.module.ts b/src/app/core/login/login-routing.module.ts index 5c528b09b..384546ba8 100644 --- a/src/app/core/login/login-routing.module.ts +++ b/src/app/core/login/login-routing.module.ts @@ -15,11 +15,6 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { CoreLoginCredentialsPage } from './pages/credentials/credentials.page'; -import { CoreLoginInitPage } from './pages/init/init.page'; -import { CoreLoginSitePage } from './pages/site/site.page'; -import { CoreLoginSitesPage } from './pages/sites/sites.page'; - const routes: Routes = [ { path: '', @@ -28,19 +23,19 @@ const routes: Routes = [ }, { path: 'init', - component: CoreLoginInitPage, + loadChildren: () => import('./pages/init/init.page.module').then( m => m.CoreLoginInitPageModule), }, { path: 'site', - component: CoreLoginSitePage, + loadChildren: () => import('./pages/site/site.page.module').then( m => m.CoreLoginSitePageModule), }, { path: 'credentials', - component: CoreLoginCredentialsPage, + loadChildren: () => import('./pages/credentials/credentials.page.module').then( m => m.CoreLoginCredentialsPageModule), }, { path: 'sites', - component: CoreLoginSitesPage, + loadChildren: () => import('./pages/sites/sites.page.module').then( m => m.CoreLoginSitesPageModule), }, ]; diff --git a/src/app/core/login/login.module.ts b/src/app/core/login/login.module.ts index 090f44698..fdfb74a9c 100644 --- a/src/app/core/login/login.module.ts +++ b/src/app/core/login/login.module.ts @@ -13,38 +13,12 @@ // limitations under the License. import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; - -import { IonicModule } from '@ionic/angular'; -import { TranslateModule } from '@ngx-translate/core'; - -import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives/directives.module'; - import { CoreLoginRoutingModule } from './login-routing.module'; -import { CoreLoginCredentialsPage } from './pages/credentials/credentials.page'; -import { CoreLoginInitPage } from './pages/init/init.page'; -import { CoreLoginSitePage } from './pages/site/site.page'; -import { CoreLoginSitesPage } from './pages/sites/sites.page'; @NgModule({ imports: [ - CommonModule, - IonicModule, CoreLoginRoutingModule, - CoreComponentsModule, - TranslateModule.forChild(), - FormsModule, - ReactiveFormsModule, - CoreComponentsModule, - CoreDirectivesModule, - ], - declarations: [ - CoreLoginCredentialsPage, - CoreLoginInitPage, - CoreLoginSitePage, - CoreLoginSitesPage, ], + declarations: [], }) export class CoreLoginModule {} diff --git a/src/app/core/login/pages/credentials/credentials.html b/src/app/core/login/pages/credentials/credentials.html index 13d91cb9d..e01cec562 100644 --- a/src/app/core/login/pages/credentials/credentials.html +++ b/src/app/core/login/pages/credentials/credentials.html @@ -7,7 +7,10 @@ {{ 'core.login.login' | translate }} - + + + diff --git a/src/app/core/login/pages/credentials/credentials.page.module.ts b/src/app/core/login/pages/credentials/credentials.page.module.ts new file mode 100644 index 000000000..50b2026c0 --- /dev/null +++ b/src/app/core/login/pages/credentials/credentials.page.module.ts @@ -0,0 +1,50 @@ +// (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'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; + +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +import { CoreLoginCredentialsPage } from './credentials.page'; + +const routes: Routes = [ + { + path: '', + component: CoreLoginCredentialsPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CommonModule, + IonicModule, + TranslateModule.forChild(), + FormsModule, + ReactiveFormsModule, + CoreComponentsModule, + CoreDirectivesModule, + ], + declarations: [ + CoreLoginCredentialsPage, + ], + exports: [RouterModule], +}) +export class CoreLoginCredentialsPageModule {} diff --git a/src/app/core/login/pages/init/init.page.module.ts b/src/app/core/login/pages/init/init.page.module.ts new file mode 100644 index 000000000..8cd9d79f0 --- /dev/null +++ b/src/app/core/login/pages/init/init.page.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'; +import { RouterModule, Routes } from '@angular/router'; +import { IonicModule } from '@ionic/angular'; + +import { CoreLoginInitPage } from './init.page'; + +const routes: Routes = [ + { + path: '', + component: CoreLoginInitPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + IonicModule, + ], + declarations: [ + CoreLoginInitPage, + ], + exports: [RouterModule], +}) +export class CoreLoginInitPageModule {} diff --git a/src/app/core/login/pages/site/site.page.module.ts b/src/app/core/login/pages/site/site.page.module.ts new file mode 100644 index 000000000..c73699573 --- /dev/null +++ b/src/app/core/login/pages/site/site.page.module.ts @@ -0,0 +1,50 @@ +// (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'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; + +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +import { CoreLoginSitePage } from './site.page'; + +const routes: Routes = [ + { + path: '', + component: CoreLoginSitePage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CommonModule, + IonicModule, + TranslateModule.forChild(), + FormsModule, + ReactiveFormsModule, + CoreComponentsModule, + CoreDirectivesModule, + ], + declarations: [ + CoreLoginSitePage, + ], + exports: [RouterModule], +}) +export class CoreLoginSitePageModule {} diff --git a/src/app/core/login/pages/sites/sites.page.module.ts b/src/app/core/login/pages/sites/sites.page.module.ts new file mode 100644 index 000000000..b70dd625f --- /dev/null +++ b/src/app/core/login/pages/sites/sites.page.module.ts @@ -0,0 +1,47 @@ +// (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'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; + +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +import { CoreLoginSitesPage } from './sites.page'; + +const routes: Routes = [ + { + path: '', + component: CoreLoginSitesPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + ], + declarations: [ + CoreLoginSitesPage, + ], + exports: [RouterModule], +}) +export class CoreLoginSitesPageModule {} diff --git a/src/app/core/mainmenu/mainmenu-routing.module.ts b/src/app/core/mainmenu/mainmenu-routing.module.ts index fb2998542..9633807c5 100644 --- a/src/app/core/mainmenu/mainmenu-routing.module.ts +++ b/src/app/core/mainmenu/mainmenu-routing.module.ts @@ -17,7 +17,6 @@ import { RouterModule, Routes } from '@angular/router'; import { CoreMainMenuPage } from './pages/menu/menu.page'; import { CoreMainMenuMorePage } from './pages/more/more.page'; -import { CoreCoursesHomePage } from '../courses/pages/home/home.page'; const routes: Routes = [ { @@ -26,7 +25,7 @@ const routes: Routes = [ children: [ { path: 'home', // @todo: Add this route dynamically. - component: CoreCoursesHomePage, + loadChildren: () => import('../courses/pages/home/home.page.module').then( m => m.CoreCoursesHomePageModule), }, { path: 'more', diff --git a/src/app/core/settings/pages/about/about.page.module.ts b/src/app/core/settings/pages/about/about.page.module.ts new file mode 100644 index 000000000..b63096fc8 --- /dev/null +++ b/src/app/core/settings/pages/about/about.page.module.ts @@ -0,0 +1,47 @@ +// (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'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; + +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +import { CoreSettingsAboutPage } from './about.page'; + +const routes: Routes = [ + { + path: '', + component: CoreSettingsAboutPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + ], + declarations: [ + CoreSettingsAboutPage, + ], + exports: [RouterModule], +}) +export class CoreSettingsAboutPageModule {} diff --git a/src/app/core/settings/pages/app/app.page.module.ts b/src/app/core/settings/pages/app/app.page.module.ts new file mode 100644 index 000000000..2a7379c5d --- /dev/null +++ b/src/app/core/settings/pages/app/app.page.module.ts @@ -0,0 +1,47 @@ +// (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'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; + +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +import { CoreSettingsAppPage } from './app.page'; + +const routes: Routes = [ + { + path: '', + component: CoreSettingsAppPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + ], + declarations: [ + CoreSettingsAppPage, + ], + exports: [RouterModule], +}) +export class CoreSettingsAppPageModule {} diff --git a/src/app/core/settings/pages/app/app.page.ts b/src/app/core/settings/pages/app/app.page.ts index 8d1dac8a4..01b9fd442 100644 --- a/src/app/core/settings/pages/app/app.page.ts +++ b/src/app/core/settings/pages/app/app.page.ts @@ -20,7 +20,7 @@ import { ActivatedRoute, Params, Router } from '@angular/router'; selector: 'app-settings', templateUrl: 'app.html', }) -export class CoreAppSettingsPage { +export class CoreSettingsAppPage { // @ViewChild(CoreSplitViewComponent) splitviewCtrl?: CoreSplitViewComponent; diff --git a/src/app/core/settings/pages/deviceinfo/deviceinfo.page.module.ts b/src/app/core/settings/pages/deviceinfo/deviceinfo.page.module.ts new file mode 100644 index 000000000..e94de2f4c --- /dev/null +++ b/src/app/core/settings/pages/deviceinfo/deviceinfo.page.module.ts @@ -0,0 +1,47 @@ +// (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'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; + +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +import { CoreSettingsDeviceInfoPage } from './deviceinfo.page'; + +const routes: Routes = [ + { + path: '', + component: CoreSettingsDeviceInfoPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + ], + declarations: [ + CoreSettingsDeviceInfoPage, + ], + exports: [RouterModule], +}) +export class CoreSettingsDeviceInfoPageModule {} diff --git a/src/app/core/settings/settings-routing.module.ts b/src/app/core/settings/settings-routing.module.ts index b00808bee..a391ebf1b 100644 --- a/src/app/core/settings/settings-routing.module.ts +++ b/src/app/core/settings/settings-routing.module.ts @@ -14,22 +14,19 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { CoreAppSettingsPage } from './pages/app/app.page'; -import { CoreSettingsAboutPage } from './pages/about/about.page'; -import { CoreSettingsDeviceInfoPage } from './pages/deviceinfo/deviceinfo.page'; const routes: Routes = [ { path: 'about', - component: CoreSettingsAboutPage, + loadChildren: () => import('./pages/about/about.page.module').then( m => m.CoreSettingsAboutPageModule), }, { path: 'deviceinfo', - component: CoreSettingsDeviceInfoPage, + loadChildren: () => import('./pages/deviceinfo/deviceinfo.page.module').then( m => m.CoreSettingsDeviceInfoPageModule), }, { path: 'app', - component: CoreAppSettingsPage, + loadChildren: () => import('./pages/app/app.page.module').then( m => m.CoreSettingsAppPageModule), }, { path: '', @@ -42,4 +39,4 @@ const routes: Routes = [ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) -export class CoreAppSettingsRoutingModule {} +export class CoreSettingsRoutingModule {} diff --git a/src/app/core/settings/settings.module.ts b/src/app/core/settings/settings.module.ts index 7e8f5cf66..961772eb7 100644 --- a/src/app/core/settings/settings.module.ts +++ b/src/app/core/settings/settings.module.ts @@ -13,34 +13,13 @@ // 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 { CoreAppSettingsRoutingModule } from './settings-routing.module'; -import { CorePipesModule } from '@pipes/pipes.module'; -import { CoreComponentsModule } from '@components/components.module'; -import { CoreDirectivesModule } from '@directives/directives.module'; - -import { CoreAppSettingsPage } from './pages/app/app.page'; -import { CoreSettingsAboutPage } from './pages/about/about.page'; -import { CoreSettingsDeviceInfoPage } from './pages/deviceinfo/deviceinfo.page'; +import { CoreSettingsRoutingModule } from './settings-routing.module'; @NgModule({ imports: [ - CommonModule, - IonicModule, - CoreAppSettingsRoutingModule, - CorePipesModule, - CoreComponentsModule, - CoreDirectivesModule, - TranslateModule.forChild(), - ], - declarations: [ - CoreAppSettingsPage, - CoreSettingsAboutPage, - CoreSettingsDeviceInfoPage, + CoreSettingsRoutingModule, ], + declarations: [], }) -export class CoreAppSettingsModule {} +export class CoreSettingsModule {}