Merge pull request #2576 from dpalou/MOBILE-3565

Mobile 3565
main
Dani Palou 2020-10-26 15:05:58 +01:00 committed by GitHub
commit ca95f53134
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 2268 additions and 147 deletions

View File

@ -27,7 +27,11 @@ const routes: Routes = [
},
{
path: 'settings',
loadChildren: () => import('./core/settings/settings.module').then( m => m.CoreAppSettingsPageModule),
loadChildren: () => import('./core/settings/settings.module').then( m => m.CoreSettingsModule),
},
{
path: 'mainmenu',
loadChildren: () => import('./core/mainmenu/mainmenu.module').then( m => m.CoreMainMenuModule),
},
];

View File

@ -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();
});
}

View File

@ -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 },

View File

@ -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');

View File

@ -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.

View File

@ -0,0 +1,30 @@
// (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 { CoreMainMenuDelegate } from '@core/mainmenu/services/delegate';
import { CoreHomeMainMenuHandler } from './handlers/mainmenu';
@NgModule({
imports: [],
declarations: [],
})
export class CoreCoursesModule {
constructor(mainMenuDelegate: CoreMainMenuDelegate) {
mainMenuDelegate.registerHandler(new CoreHomeMainMenuHandler());
}
}

View File

@ -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<boolean> {
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<boolean> {
// @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',
};
}
}

View File

@ -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}}"
}

View File

@ -0,0 +1,20 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>
<core-format-text [text]="siteName" contextLevel="system" [contextInstanceId]="0"></core-format-text>
<img src="assets/img/login_logo.png" class="core-header-logo" [alt]="siteName">
</ion-title>
<ion-buttons slot="end">
<!-- @todo -->
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<!-- @todo -->
Home page.
</ion-content>

View File

@ -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 {}

View File

@ -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
}
}

View File

@ -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);
}

View File

@ -15,27 +15,27 @@
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: '',
component: CoreLoginInitPage,
redirectTo: 'init',
pathMatch: 'full',
},
{
path: 'init',
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),
},
];

View File

@ -13,42 +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';
import { CoreLoginHelperProvider } from './services/helper';
@NgModule({
imports: [
CommonModule,
IonicModule,
CoreLoginRoutingModule,
CoreComponentsModule,
TranslateModule.forChild(),
FormsModule,
ReactiveFormsModule,
CoreComponentsModule,
CoreDirectivesModule,
],
declarations: [
CoreLoginCredentialsPage,
CoreLoginInitPage,
CoreLoginSitePage,
CoreLoginSitesPage,
],
providers: [
CoreLoginHelperProvider,
],
declarations: [],
})
export class CoreLoginModule {}

View File

@ -7,7 +7,10 @@
<ion-title>{{ 'core.login.login' | translate }}</ion-title>
<ion-buttons slot="end">
<!-- @todo: Settings button. -->
<ion-button router-direction="forward" routerLink="/settings/app"
[attr.aria-label]="'core.settings.appsettings' | translate">
<core-icon slot="icon-only" name="fa-cog"></core-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>

View File

@ -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 {}

View File

@ -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);

View File

@ -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 {}

View File

@ -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<void> {
// 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<void> {
// 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');
}

View File

@ -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 {}

View File

@ -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 {

View File

@ -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 {}

View File

@ -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<any> {
goToNoSitePage(page?: string, params?: Params): Promise<any> {
// @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<any> {
// @todo
return Promise.resolve();
goToSiteInitialPage(options?: OpenMainMenuOptions): Promise<void> {
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<any> {
// @todo
return Promise.resolve();
protected async openMainMenu(options?: OpenMainMenuOptions): Promise<void> {
// 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.
};

View File

@ -0,0 +1,6 @@
{
"changesite": "Change site",
"help": "Help",
"logout": "Log out",
"website": "Website"
}

View File

@ -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 { 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: 'home', // @todo: Add this route dynamically.
loadChildren: () => import('../courses/pages/home/home.page.module').then( m => m.CoreCoursesHomePageModule),
},
{
path: 'more',
children: [
{
path: '',
component: CoreMainMenuMorePage,
},
],
},
],
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class CoreMainMenuRoutingModule {}

View File

@ -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 {}

View File

@ -0,0 +1,27 @@
<ion-tabs #mainTabs [hidden]="!showTabs">
<ion-tab-bar slot="bottom">
<ion-spinner *ngIf="!loaded"></ion-spinner>
<ion-tab-button tab="redirect" [disabled]="true" [hidden]="true"></ion-tab-button> <!-- [root]="redirectPage" [rootParams]="redirectParams" -->
<ion-tab-button (ionTabButtonClick)="tabClicked($event, tab.page)" [hidden]="!loaded && tab.hide" *ngFor="let tab of tabs" [tab]="tab.page" [disabled]="tab.hide" layout="label-hide" class="{{tab.class}}">
<core-icon [name]="tab.icon"></core-icon>
<ion-label>{{ tab.title | translate }}</ion-label>
<ion-badge *ngIf="tab.badge">{{ tab.badge }}</ion-badge>
</ion-tab-button>
<ion-tab-button (ionTabButtonClick)="tabClicked($event, 'more')" [hidden]="!loaded" tab="more" layout="label-hide">
<core-icon name="fa-bars"></core-icon>
<ion-label>{{ 'core.more' | translate }}</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>
<div class="core-network-message" [hidden]="!showTabs">
<div class="core-online-message">
{{ "core.youreonline" | translate }}
</div>
<div class="core-offline-message">
{{ "core.youreoffline" | translate }}
</div>
</div>

View File

@ -0,0 +1,255 @@
// (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, Router } from '@angular/router';
import { NavController, IonTabs } 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';
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.
*/
@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?: IonTabs;
constructor(
protected route: ActivatedRoute,
protected navCtrl: NavController,
protected menuDelegate: CoreMainMenuDelegate,
protected changeDetector: ChangeDetectorRef,
protected router: Router,
) {
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);
}
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) {
// 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. Open it with the params.
this.navCtrl.navigateForward(data.redirectPage, {
queryParams: data.redirectParams,
animated: false,
});
} else {
// Tab not found, use a phantom tab.
// @todo
}
// Force change detection, otherwise sometimes the tab was selected before the params were applied.
this.changeDetector.detectChanges();
}
/**
* 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();
}
/**
* Tab clicked.
*
* @param e Event.
* @param page Page of the tab.
*/
async tabClicked(e: Event, page: string): Promise<void> {
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.
}
}
}

View File

@ -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;
}*/

View File

@ -0,0 +1,84 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title><core-format-text [text]="siteName" contextLevel="system" [contextInstanceId]="0"></core-format-text></ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item *ngIf="siteInfo" class="ion-text-wrap"> <!-- @todo core-user-link [userId]="siteInfo.userid" -->
<ion-avatar slot="start"></ion-avatar> <!-- @todo core-user-avatar [user]="siteInfo" -->
<ion-label>
<h2>{{siteInfo.fullname}}</h2>
<p><core-format-text [text]="siteName" contextLevel="system" [contextInstanceId]="0" [wsNotFiltered]="true"></core-format-text></p>
<p>{{ siteUrl }}</p>
</ion-label>
</ion-item>
<ion-item-divider></ion-item-divider>
<ion-item class="ion-text-center" *ngIf="(!handlers || !handlers.length) && !handlersLoaded">
<ion-spinner></ion-spinner>
</ion-item>
<ion-item *ngFor="let handler of handlers" [ngClass]="['core-moremenu-handler', handler.class || '']" (click)="openHandler(handler)" title="{{ handler.title | translate }}" detail="true">
<core-icon [name]="handler.icon" slot="start"></core-icon>
<ion-label>
<h2>{{ handler.title | translate}}</h2>
</ion-label>
<ion-badge slot="end" *ngIf="handler.showBadge" [hidden]="handler.loading || !handler.badge">{{handler.badge}}</ion-badge>
<ion-spinner slot="end" *ngIf="handler.showBadge && handler.loading"></ion-spinner>
</ion-item>
<div *ngFor="let item of customItems" class="core-moremenu-customitem">
<ion-item *ngIf="item.type != 'embedded'" [href]="item.url" title="{{item.label}}" core-link [capture]="item.type == 'app'" [inApp]="item.type == 'inappbrowser'">
<core-icon [name]="item.icon" slot="start"></core-icon>
<ion-label>
<h2>{{item.label}}</h2>
</ion-label>
</ion-item>
<ion-item *ngIf="item.type == 'embedded'" (click)="openItem(item)" title="{{item.label}}">
<core-icon [name]="item.icon" slot="start"></core-icon>
<ion-label>
<h2>{{item.label}}</h2>
</ion-label>
</ion-item>
</div>
<ion-item *ngIf="showScanQR" (click)="scanQR()">
<core-icon name="fa-qrcode" slot="start" aria-hidden="true"></core-icon>
<ion-label>
<h2>{{ 'core.scanqr' | translate }}</h2>
</ion-label>
</ion-item>
<ion-item *ngIf="showWeb && siteInfo" [href]="siteInfo.siteurl" core-link autoLogin="yes" title="{{ 'core.mainmenu.website' | translate }}">
<ion-icon name="globe" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<h2>{{ 'core.mainmenu.website' | translate }}</h2>
</ion-label>
</ion-item>
<ion-item *ngIf="showHelp" [href]="docsUrl" core-link autoLogin="no" title="{{ 'core.mainmenu.help' | translate }}">
<ion-icon name="help-buoy" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<h2>{{ 'core.mainmenu.help' | translate }}</h2>
</ion-label>
</ion-item>
<ion-item (click)="openSitePreferences()" title="{{ 'core.settings.preferences' | translate }}">
<core-icon name="fa-wrench" slot="start"></core-icon>
<ion-label>
<h2>{{ 'core.settings.preferences' | translate }}</h2>
</ion-label>
</ion-item>
<ion-item (click)="logout()" title="{{ logoutLabel | translate }}">
<ion-icon name="log-out" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<h2>{{ logoutLabel | translate }}</h2>
</ion-label>
</ion-item>
<ion-item-divider></ion-item-divider>
<ion-item (click)="openAppSettings()" title="{{ 'core.settings.appsettings' | translate }}">
<core-icon name="fa-cogs" slot="start"></core-icon>
<ion-label>
<h2>{{ 'core.settings.appsettings' | translate }}</h2>
</ion-label>
</ion-item>
</ion-list>
</ion-content>

View File

@ -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 = 'core.mainmenu.changesite';
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<void> {
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<void> {
// Scan for a QR code.
// @todo
}
/**
* Logout the user.
*/
logout(): void {
CoreSites.instance.logout();
}
}

View File

@ -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;
}
}
}
}
*/

View File

@ -0,0 +1,177 @@
// (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<CoreMainMenuHandlerToDisplay[]> = new BehaviorSubject<CoreMainMenuHandlerToDisplay[]>([]);
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<CoreMainMenuHandlerToDisplay[]> {
return this.siteHandlers;
}
/**
* Update handlers Data.
*/
updateData(): void {
const displayData: CoreMainMenuHandlerToDisplay[] = [];
for (const name in this.enabledHandlers) {
const handler = <CoreMainMenuHandler> this.enabledHandlers[name];
const data = <CoreMainMenuHandlerToDisplay> handler.getDisplayData();
data.name = name;
data.priority = handler.priority;
displayData.push(data);
}
// Sort them by priority.
displayData.sort((a, b) => (b.priority || 0) - (a.priority || 0));
this.loaded = true;
this.siteHandlers.next(displayData);
}
}

View File

@ -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<CoreMainMenuHandlerToDisplay[]> {
const deferred = CoreUtils.instance.promiseDefer<CoreMainMenuHandlerToDisplay[]>();
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<CoreMainMenuCustomItem[]> {
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<boolean> {
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<string, {
url: string;
type: string;
position: number;
labels: {
[lang: string]: {
label: string;
icon: string;
};
};
}>;

View File

@ -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 {}

View File

@ -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 {}

View File

@ -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;

View File

@ -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 {}

View File

@ -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 {}

View File

@ -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 CoreAppSettingsPageModule {}
export class CoreSettingsModule {}

View File

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

View File

@ -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);
});

View File

@ -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<void> {
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<void> {
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<void> {
// 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);
}
}
}
}

View File

@ -1312,7 +1312,7 @@ export class CoreSitesProvider {
async logout(): Promise<void> {
await this.dbReady;
let siteId;
let siteId: string | undefined;
const promises: Promise<unknown>[] = [];
if (this.currentSite) {

View File

@ -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;
};

View File

@ -303,7 +303,48 @@
"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",
"core.mainmenu.logout": "Log out",
"core.mainmenu.website": "Website",
"core.no": "No",
"core.offline": "Offline",
"core.ok": "OK",

View File

@ -66,6 +66,7 @@ declare global {
appstores: Record<string, string>;
displayqroncredentialscreen?: boolean;
displayqronsitescreen?: boolean;
forceOpenLinksIn: 'app' | 'browser';
};
BUILD: {