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",