commit
7618da4bfa
|
@ -1798,6 +1798,8 @@
|
|||
"core.loading": "moodle",
|
||||
"core.loadmore": "local_moodlemobileapp",
|
||||
"core.location": "moodle",
|
||||
"core.login.accounts": "admin",
|
||||
"core.login.add": "moodle",
|
||||
"core.login.auth_email": "auth_email/pluginname",
|
||||
"core.login.authenticating": "local_moodlemobileapp",
|
||||
"core.login.cancel": "moodle",
|
||||
|
@ -1894,6 +1896,7 @@
|
|||
"core.login.reconnect": "local_moodlemobileapp",
|
||||
"core.login.reconnectdescription": "local_moodlemobileapp",
|
||||
"core.login.reconnectssodescription": "local_moodlemobileapp",
|
||||
"core.login.removeaccount": "local_moodlemobileapp",
|
||||
"core.login.resendemail": "moodle",
|
||||
"core.login.searchby": "local_moodlemobileapp",
|
||||
"core.login.security_question": "auth",
|
||||
|
@ -1913,6 +1916,7 @@
|
|||
"core.login.startsignup": "moodle",
|
||||
"core.login.stillcantconnect": "local_moodlemobileapp",
|
||||
"core.login.supplyinfo": "moodle",
|
||||
"core.login.toggleremove": "local_moodlemobileapp",
|
||||
"core.login.username": "moodle",
|
||||
"core.login.usernameoremail": "moodle",
|
||||
"core.login.usernamerequired": "local_moodlemobileapp",
|
||||
|
@ -1922,11 +1926,9 @@
|
|||
"core.login.youcanstillconnectwithcredentials": "local_moodlemobileapp",
|
||||
"core.login.yourenteredsite": "local_moodlemobileapp",
|
||||
"core.lostconnection": "local_moodlemobileapp",
|
||||
"core.mainmenu.changesite": "local_moodlemobileapp",
|
||||
"core.mainmenu.help": "moodle",
|
||||
"core.mainmenu.home": "moodle",
|
||||
"core.mainmenu.logout": "moodle",
|
||||
"core.mainmenu.website": "local_moodlemobileapp",
|
||||
"core.mainmenu.switchaccount": "local_moodlemobileapp",
|
||||
"core.maxfilesize": "moodle",
|
||||
"core.maxsizeandattachments": "moodle",
|
||||
"core.min": "moodle",
|
||||
|
@ -1960,7 +1962,7 @@
|
|||
"core.mod_wiki": "wiki/pluginname",
|
||||
"core.mod_workshop": "workshop/pluginname",
|
||||
"core.moduleintro": "moodle",
|
||||
"core.more": "moodle",
|
||||
"core.more": "moodle/moremenu",
|
||||
"core.mygroups": "group",
|
||||
"core.name": "moodle",
|
||||
"core.needhelp": "local_moodlemobileapp",
|
||||
|
@ -2232,6 +2234,7 @@
|
|||
"core.updaterequireddesc": "local_moodlemobileapp",
|
||||
"core.upgraderunning": "error",
|
||||
"core.user": "moodle",
|
||||
"core.user.account": "local_moodlemobileapp",
|
||||
"core.user.address": "moodle",
|
||||
"core.user.city": "moodle",
|
||||
"core.user.contact": "local_moodlemobileapp",
|
||||
|
@ -2252,6 +2255,7 @@
|
|||
"core.user.participants": "moodle",
|
||||
"core.user.phone1": "moodle",
|
||||
"core.user.phone2": "moodle",
|
||||
"core.user.profile": "moodle",
|
||||
"core.user.roles": "moodle",
|
||||
"core.user.sendemail": "local_moodlemobileapp",
|
||||
"core.user.student": "moodle/defaultcoursestudent",
|
||||
|
|
|
@ -16,7 +16,7 @@ import { Injectable } from '@angular/core';
|
|||
import { CoreBlockHandlerData } from '@features/block/services/block-delegate';
|
||||
import { CoreBlockOnlyTitleComponent } from '@features/block/components/only-title-block/only-title-block';
|
||||
import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler';
|
||||
import { AddonPrivateFilesMainMenuHandlerService } from '@/addons/privatefiles/services/handlers/mainmenu';
|
||||
import { AddonPrivateFilesUserHandlerService } from '@addons/privatefiles/services/handlers/user';
|
||||
import { makeSingleton } from '@singletons';
|
||||
|
||||
/**
|
||||
|
@ -39,7 +39,7 @@ export class AddonBlockPrivateFilesHandlerService extends CoreBlockBaseHandler {
|
|||
title: 'addon.block_privatefiles.pluginname',
|
||||
class: 'addon-block-private-files',
|
||||
component: CoreBlockOnlyTitleComponent,
|
||||
link: AddonPrivateFilesMainMenuHandlerService.PAGE_NAME,
|
||||
link: AddonPrivateFilesUserHandlerService.PAGE_NAME,
|
||||
linkParams: { root: 'my' },
|
||||
navOptions: {
|
||||
preferCurrentTab: false,
|
||||
|
|
|
@ -22,6 +22,7 @@ import { CoreCommentsComponentsModule } from '@features/comments/components/comp
|
|||
import { CoreTagComponentsModule } from '@features/tag/components/components.module';
|
||||
import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module';
|
||||
import { AddonBlogMainMenuHandlerService } from './services/handlers/mainmenu';
|
||||
import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module';
|
||||
|
||||
function buildRoutes(injector: Injector): Routes {
|
||||
return [
|
||||
|
@ -39,6 +40,7 @@ function buildRoutes(injector: Injector): Routes {
|
|||
CoreSharedModule,
|
||||
CoreCommentsComponentsModule,
|
||||
CoreTagComponentsModule,
|
||||
CoreMainMenuComponentsModule,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
providers: [
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<h1>{{ title | translate }}</h1>
|
||||
<ion-buttons slot="end"></ion-buttons>
|
||||
<ion-buttons slot="end">
|
||||
<core-user-menu-button></core-user-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
[content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(undefined, $event, true)"
|
||||
[iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
</core-context-menu>
|
||||
<core-user-menu-button></core-user-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
|
|
@ -19,6 +19,7 @@ import { CoreSharedModule } from '@/core/shared.module';
|
|||
import { AddonCalendarComponentsModule } from '../../components/components.module';
|
||||
|
||||
import { AddonCalendarIndexPage } from './index.page';
|
||||
import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
|
@ -32,6 +33,7 @@ const routes: Routes = [
|
|||
RouterModule.forChild(routes),
|
||||
CoreSharedModule,
|
||||
AddonCalendarComponentsModule,
|
||||
CoreMainMenuComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
AddonCalendarIndexPage,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
</ion-buttons>
|
||||
<h1>{{ 'addon.messages.messages' | translate }}</h1>
|
||||
<ion-buttons slot="end">
|
||||
<core-user-menu-button></core-user-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
|
|
@ -23,6 +23,7 @@ import { CoreSearchComponentsModule } from '@features/search/components/componen
|
|||
|
||||
import { AddonMessagesDiscussions35Page } from './discussions.page';
|
||||
import { AddonMessagesMainMenuHandlerService } from '@addons/messages/services/handlers/mainmenu';
|
||||
import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module';
|
||||
|
||||
const mobileRoutes: Routes = [
|
||||
{
|
||||
|
@ -58,6 +59,7 @@ const routes: Routes = [
|
|||
RouterModule.forChild(routes),
|
||||
CoreSharedModule,
|
||||
CoreSearchComponentsModule,
|
||||
CoreMainMenuComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
AddonMessagesDiscussions35Page,
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<ion-button (click)="gotoSettings()" [attr.aria-label]="'addon.messages.messagepreferences' | translate">
|
||||
<ion-icon name="fas-cog" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
<core-user-menu-button></core-user-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
|
|
@ -22,6 +22,7 @@ import { CoreSharedModule } from '@/core/shared.module';
|
|||
|
||||
import { AddonMessagesGroupConversationsPage } from './group-conversations.page';
|
||||
import { AddonMessagesMainMenuHandlerService } from '@addons/messages/services/handlers/mainmenu';
|
||||
import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module';
|
||||
|
||||
const mobileRoutes: Routes = [
|
||||
{
|
||||
|
@ -56,6 +57,7 @@ const routes: Routes = [
|
|||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CoreSharedModule,
|
||||
CoreMainMenuComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
AddonMessagesGroupConversationsPage,
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<h1>{{ 'addon.notifications.notifications' | translate }}</h1>
|
||||
<ion-buttons slot="end">
|
||||
<core-user-menu-button></core-user-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
|
|
|
@ -18,6 +18,7 @@ import { RouterModule, Routes } from '@angular/router';
|
|||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { AddonNotificationsComponentsModule } from '../../components/components.module';
|
||||
import { AddonNotificationsListPage } from './list';
|
||||
import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
|
@ -31,6 +32,7 @@ const routes: Routes = [
|
|||
RouterModule.forChild(routes),
|
||||
CoreSharedModule,
|
||||
AddonNotificationsComponentsModule,
|
||||
CoreMainMenuComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
AddonNotificationsListPage,
|
||||
|
|
|
@ -16,14 +16,14 @@ import { Injector, NgModule } from '@angular/core';
|
|||
import { RouterModule, ROUTES, Routes } from '@angular/router';
|
||||
|
||||
import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module';
|
||||
import { AddonPrivateFilesMainMenuHandlerService } from './services/handlers/mainmenu';
|
||||
import { AddonPrivateFilesUserHandlerService } from './services/handlers/user';
|
||||
|
||||
function buildRoutes(injector: Injector): Routes {
|
||||
return [
|
||||
{
|
||||
path: 'root',
|
||||
data: {
|
||||
mainMenuTabRoot: AddonPrivateFilesMainMenuHandlerService.PAGE_NAME,
|
||||
mainMenuTabRoot: AddonPrivateFilesUserHandlerService.PAGE_NAME,
|
||||
},
|
||||
loadChildren: () => import('./pages/index/index.module').then(m => m.AddonPrivateFilesIndexPageModule),
|
||||
},
|
||||
|
|
|
@ -15,12 +15,12 @@
|
|||
import { APP_INITIALIZER, NgModule, Type } from '@angular/core';
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-delegate';
|
||||
import { CoreMainMenuRoutingModule } from '@features/mainmenu/mainmenu-routing.module';
|
||||
import { AddonPrivateFilesMainMenuHandler, AddonPrivateFilesMainMenuHandlerService } from './services/handlers/mainmenu';
|
||||
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
|
||||
import { AddonPrivateFilesProvider } from './services/privatefiles';
|
||||
import { AddonPrivateFilesHelperProvider } from './services/privatefiles-helper';
|
||||
import { CoreUserDelegate } from '@features/user/services/user-delegate';
|
||||
import { AddonPrivateFilesUserHandler, AddonPrivateFilesUserHandlerService } from './services/handlers/user';
|
||||
|
||||
export const ADDON_PRIVATEFILES_SERVICES: Type<unknown>[] = [
|
||||
AddonPrivateFilesProvider,
|
||||
|
@ -29,7 +29,7 @@ export const ADDON_PRIVATEFILES_SERVICES: Type<unknown>[] = [
|
|||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: AddonPrivateFilesMainMenuHandlerService.PAGE_NAME,
|
||||
path: AddonPrivateFilesUserHandlerService.PAGE_NAME,
|
||||
loadChildren: () => import('@/addons/privatefiles/privatefiles-lazy.module').then(m => m.AddonPrivateFilesLazyModule),
|
||||
},
|
||||
];
|
||||
|
@ -45,7 +45,7 @@ const routes: Routes = [
|
|||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
useValue: () => {
|
||||
CoreMainMenuDelegate.registerHandler(AddonPrivateFilesMainMenuHandler.instance);
|
||||
CoreUserDelegate.registerHandler(AddonPrivateFilesUserHandler.instance);
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -14,44 +14,57 @@
|
|||
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@features/mainmenu/services/mainmenu-delegate';
|
||||
import { AddonPrivateFiles } from '@/addons/privatefiles/services/privatefiles';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { CoreUserDelegateService, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@features/user/services/user-delegate';
|
||||
import { CoreUserProfile } from '@features/user/services/user';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreSites } from '@services/sites';
|
||||
|
||||
/**
|
||||
* Handler to inject an option into main menu.
|
||||
* Handler to inject an option into user menu.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonPrivateFilesMainMenuHandlerService implements CoreMainMenuHandler {
|
||||
export class AddonPrivateFilesUserHandlerService implements CoreUserProfileHandler {
|
||||
|
||||
static readonly PAGE_NAME = 'private';
|
||||
|
||||
name = 'AddonPrivateFiles';
|
||||
priority = 400;
|
||||
priority = 300;
|
||||
type = CoreUserDelegateService.TYPE_NEW_PAGE;
|
||||
cacheEnabled = true;
|
||||
|
||||
/**
|
||||
* Check if the handler is enabled on a site level.
|
||||
*
|
||||
* @return Whether or not the handler is enabled on a site level.
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabled(): Promise<boolean> {
|
||||
return AddonPrivateFiles.isPluginEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data needed to render the handler.
|
||||
*
|
||||
* @return Data needed to render the handler.
|
||||
* @inheritdoc
|
||||
*/
|
||||
getDisplayData(): CoreMainMenuHandlerData {
|
||||
async isEnabledForUser(user: CoreUserProfile): Promise<boolean> {
|
||||
// Private files only available for the current user.
|
||||
return user.id == CoreSites.getCurrentSiteUserId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getDisplayData(): CoreUserProfileHandlerData {
|
||||
return {
|
||||
icon: 'fas-folder',
|
||||
title: 'addon.privatefiles.files',
|
||||
page: AddonPrivateFilesMainMenuHandlerService.PAGE_NAME,
|
||||
class: 'addon-privatefiles-handler',
|
||||
action: (event): void => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
CoreNavigator.navigateToSitePath(AddonPrivateFilesUserHandlerService.PAGE_NAME);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const AddonPrivateFilesMainMenuHandler = makeSingleton(AddonPrivateFilesMainMenuHandlerService);
|
||||
export const AddonPrivateFilesUserHandler = makeSingleton(AddonPrivateFilesUserHandlerService);
|
|
@ -22,37 +22,61 @@ import { Platform } from '@singletons';
|
|||
export function CoreModalLateralTransitionEnter(baseEl: HTMLElement): Animation {
|
||||
const OFF_RIGHT = Platform.isRTL ? '-100%' : '100%';
|
||||
|
||||
const backdropAnimation = createAnimation()
|
||||
.addElement(baseEl.querySelector('ion-backdrop')!)
|
||||
.fromTo('opacity', 0.01, 0.4);
|
||||
const otherAnimations: Animation[] = [];
|
||||
|
||||
const wrapperAnimation = createAnimation()
|
||||
.addElement(baseEl.querySelector('.modal-wrapper')!)
|
||||
.fromTo('transform', 'translateX(' + OFF_RIGHT + ')', 'translateX(0)')
|
||||
.fromTo('opacity', 0.8, 1);
|
||||
const backdrop = baseEl.querySelector('ion-backdrop');
|
||||
if (backdrop) {
|
||||
const backdropAnimation = createAnimation()
|
||||
.addElement(backdrop)
|
||||
.fromTo('opacity', 0.01, 0.4);
|
||||
|
||||
otherAnimations.push(backdropAnimation);
|
||||
}
|
||||
|
||||
const wrapper = baseEl.querySelector('.modal-wrapper');
|
||||
if (wrapper) {
|
||||
const wrapperAnimation = createAnimation()
|
||||
.addElement(wrapper)
|
||||
.fromTo('transform', 'translateX(' + OFF_RIGHT + ')', 'translateX(0)')
|
||||
.fromTo('opacity', 0.8, 1);
|
||||
|
||||
otherAnimations.push(wrapperAnimation);
|
||||
}
|
||||
|
||||
return createAnimation()
|
||||
.addElement(baseEl)
|
||||
.easing('cubic-bezier(0.36,0.66,0.04,1)')
|
||||
.duration(300)
|
||||
.addAnimation([backdropAnimation, wrapperAnimation]);
|
||||
.addAnimation(otherAnimations);
|
||||
}
|
||||
|
||||
export function CoreModalLateralTransitionLeave(baseEl: HTMLElement): Animation {
|
||||
const OFF_RIGHT = Platform.isRTL ? '-100%' : '100%';
|
||||
|
||||
const backdropAnimation = createAnimation()
|
||||
.addElement(baseEl.querySelector('ion-backdrop')!)
|
||||
.fromTo('opacity', 0.4, 0.0);
|
||||
const otherAnimations: Animation[] = [];
|
||||
|
||||
const wrapperAnimation = createAnimation()
|
||||
.addElement(baseEl.querySelector('.modal-wrapper')!)
|
||||
.beforeStyles({ opacity: 1 })
|
||||
.fromTo('transform', 'translateX(0)', 'translateX(' + OFF_RIGHT + ')');
|
||||
const backdrop = baseEl.querySelector('ion-backdrop');
|
||||
if (backdrop) {
|
||||
const backdropAnimation = createAnimation()
|
||||
.addElement(backdrop)
|
||||
.fromTo('opacity', 0.4, 0.0);
|
||||
|
||||
otherAnimations.push(backdropAnimation);
|
||||
}
|
||||
|
||||
const wrapper = baseEl.querySelector('.modal-wrapper');
|
||||
if (wrapper) {
|
||||
const wrapperAnimation = createAnimation()
|
||||
.addElement(wrapper)
|
||||
.beforeStyles({ opacity: 1 })
|
||||
.fromTo('transform', 'translateX(0)', 'translateX(' + OFF_RIGHT + ')');
|
||||
|
||||
otherAnimations.push(wrapperAnimation);
|
||||
}
|
||||
|
||||
return createAnimation()
|
||||
.addElement(baseEl)
|
||||
.easing('cubic-bezier(0.36,0.66,0.04,1)')
|
||||
.duration(300)
|
||||
.addAnimation([backdropAnimation, wrapperAnimation]);
|
||||
.addAnimation(otherAnimations);
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ const BUTTON_HIDDEN_CLASS = 'core-navbar-button-hidden';
|
|||
})
|
||||
export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
|
||||
|
||||
@ViewChild('contextMenuContainer', { read: ViewContainerRef }) container?: ViewContainerRef;
|
||||
@ViewChild('contextMenuContainer', { read: ViewContainerRef }) container!: ViewContainerRef;
|
||||
|
||||
// If the hidden input is true, hide all buttons.
|
||||
// eslint-disable-next-line @angular-eslint/no-input-rename
|
||||
|
@ -113,7 +113,13 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
|
|||
|
||||
// Make sure that context-menu is always at the end of buttons if any.
|
||||
const contextMenu = buttonsContainer.querySelector('core-context-menu');
|
||||
contextMenu?.parentElement?.appendChild(contextMenu);
|
||||
const userMenu = buttonsContainer.querySelector('core-user-menu-button');
|
||||
|
||||
if (userMenu) {
|
||||
contextMenu?.parentElement?.insertBefore(contextMenu, userMenu);
|
||||
} else {
|
||||
contextMenu?.parentElement?.appendChild(contextMenu);
|
||||
}
|
||||
} else {
|
||||
this.logger.warn('The header was found, but it didn\'t have the right ion-buttons.', selector);
|
||||
}
|
||||
|
@ -177,7 +183,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
|
|||
*/
|
||||
protected createMainContextMenu(): CoreContextMenuComponent {
|
||||
const factory = this.factoryResolver.resolveComponentFactory(CoreContextMenuComponent);
|
||||
const componentRef = this.container!.createComponent<CoreContextMenuComponent>(factory);
|
||||
const componentRef = this.container.createComponent<CoreContextMenuComponent>(factory);
|
||||
|
||||
this.createdMainContextMenuElement = componentRef.location.nativeElement;
|
||||
|
||||
|
|
|
@ -36,13 +36,19 @@
|
|||
content: "";
|
||||
}
|
||||
}
|
||||
&.core-bar-button-image img {
|
||||
&.core-bar-button-image {
|
||||
padding: 0;
|
||||
width: var(--core-header-toolbar-button-image-size);
|
||||
height: var(--core-header-toolbar-button-image-size);
|
||||
max-width: var(--core-header-toolbar-button-image-size);
|
||||
max-height: var(--core-header-toolbar-button-image-size);
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
|
||||
img {
|
||||
padding: 4px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.contact-status {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<core-navbar-buttons slot="end">
|
||||
<core-navbar-buttons slot="end" prepend>
|
||||
<ion-button *ngIf="searchEnabled" (click)="openSearch()" [attr.aria-label]="'core.courses.searchcourses' | translate">
|
||||
<ion-icon name="fas-search" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
|
|
|
@ -5,20 +5,20 @@
|
|||
</ion-buttons>
|
||||
<h1 *ngIf="!showOnlyEnrolled">{{ 'core.courses.availablecourses' | translate }}</h1>
|
||||
<h1 *ngIf="showOnlyEnrolled">{{ 'core.courses.mycourses' | translate }}</h1>
|
||||
<ion-buttons slot="end"></ion-buttons>
|
||||
<ion-buttons slot="end">
|
||||
<core-context-menu>
|
||||
<core-context-menu-item *ngIf="downloadCourseEnabled || downloadCoursesEnabled" [priority]="1000"
|
||||
[content]="'core.settings.showdownloadoptions' | translate" (action)="toggleDownload()"
|
||||
iconAction="toggle" [(toggle)]="downloadEnabled"></core-context-menu-item>
|
||||
<core-context-menu-item [priority]="900"
|
||||
[content]="'core.courses.showonlyenrolled' | translate" (action)="toggleEnrolled()"
|
||||
iconAction="toggle" [(toggle)]="showOnlyEnrolled"></core-context-menu-item>
|
||||
</core-context-menu>
|
||||
<core-user-menu-button></core-user-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<core-navbar-buttons slot="end">
|
||||
<core-context-menu>
|
||||
<core-context-menu-item *ngIf="downloadCourseEnabled || downloadCoursesEnabled" [priority]="1000"
|
||||
[content]="'core.settings.showdownloadoptions' | translate" (action)="toggleDownload()"
|
||||
iconAction="toggle" [(toggle)]="downloadEnabled"></core-context-menu-item>
|
||||
<core-context-menu-item [priority]="900"
|
||||
[content]="'core.courses.showonlyenrolled' | translate" (action)="toggleEnrolled()"
|
||||
iconAction="toggle" [(toggle)]="showOnlyEnrolled"></core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</core-navbar-buttons>
|
||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshCourses($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
|
|
@ -20,6 +20,7 @@ import { CoreCoursesComponentsModule } from '../../components/components.module'
|
|||
import { CoreSearchComponentsModule } from '@features/search/components/components.module';
|
||||
|
||||
import { CoreCoursesListPage } from './list';
|
||||
import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
|
@ -34,6 +35,7 @@ const routes: Routes = [
|
|||
CoreSharedModule,
|
||||
CoreCoursesComponentsModule,
|
||||
CoreSearchComponentsModule,
|
||||
CoreMainMenuComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
CoreCoursesListPage,
|
||||
|
|
|
@ -23,13 +23,13 @@ import { CoreGradesCoursePage } from './pages/course/course.page';
|
|||
import { CoreGradesCoursePageModule } from './pages/course/course.module';
|
||||
import { CoreGradesCoursesPage } from './pages/courses/courses.page';
|
||||
import { CoreGradesGradePage } from './pages/grade/grade.page';
|
||||
import { CoreGradesMainMenuHandlerService } from './services/handlers/mainmenu';
|
||||
import { CoreGradesUserHandlerService } from './services/handlers/user';
|
||||
|
||||
const mobileRoutes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
data: {
|
||||
mainMenuTabRoot: CoreGradesMainMenuHandlerService.PAGE_NAME,
|
||||
mainMenuTabRoot: CoreGradesUserHandlerService.PAGE_NAME,
|
||||
},
|
||||
component: CoreGradesCoursesPage,
|
||||
},
|
||||
|
@ -47,7 +47,7 @@ const tabletRoutes: Routes = [
|
|||
{
|
||||
path: '',
|
||||
data: {
|
||||
mainMenuTabRoot: CoreGradesMainMenuHandlerService.PAGE_NAME,
|
||||
mainMenuTabRoot: CoreGradesUserHandlerService.PAGE_NAME,
|
||||
},
|
||||
component: CoreGradesCoursesPage,
|
||||
children: [
|
||||
|
|
|
@ -19,14 +19,12 @@ import { CoreCourseIndexRoutingModule } from '@features/course/pages/index/index
|
|||
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
|
||||
import { CoreMainMenuRoutingModule } from '@features/mainmenu/mainmenu-routing.module';
|
||||
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
|
||||
import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-delegate';
|
||||
import { CoreUserDelegate } from '@features/user/services/user-delegate';
|
||||
import { CoreGradesProvider } from './services/grades';
|
||||
import { CoreGradesHelperProvider } from './services/grades-helper';
|
||||
import { CoreGradesCourseOptionHandler } from './services/handlers/course-option';
|
||||
import { CoreGradesMainMenuHandler, CoreGradesMainMenuHandlerService } from './services/handlers/mainmenu';
|
||||
import { CoreGradesOverviewLinkHandler } from './services/handlers/overview-link';
|
||||
import { CoreGradesUserHandler } from './services/handlers/user';
|
||||
import { CoreGradesUserHandler, CoreGradesUserHandlerService } from './services/handlers/user';
|
||||
import { CoreGradesUserLinkHandler } from './services/handlers/user-link';
|
||||
|
||||
export const CORE_GRADES_SERVICES: Type<unknown>[] = [
|
||||
|
@ -36,7 +34,7 @@ export const CORE_GRADES_SERVICES: Type<unknown>[] = [
|
|||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: CoreGradesMainMenuHandlerService.PAGE_NAME,
|
||||
path: CoreGradesUserHandlerService.PAGE_NAME,
|
||||
loadChildren: () => import('@features/grades/grades-lazy.module').then(m => m.CoreGradesLazyModule),
|
||||
},
|
||||
{
|
||||
|
@ -63,7 +61,6 @@ const courseIndexRoutes: Routes = [
|
|||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
useValue: () => {
|
||||
CoreMainMenuDelegate.registerHandler(CoreGradesMainMenuHandler.instance);
|
||||
CoreUserDelegate.registerHandler(CoreGradesUserHandler.instance);
|
||||
CoreContentLinksDelegate.registerHandler(CoreGradesUserLinkHandler.instance);
|
||||
CoreContentLinksDelegate.registerHandler(CoreGradesOverviewLinkHandler.instance);
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
// (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 { CoreGrades } from '@features/grades/services/grades';
|
||||
import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@features/mainmenu/services/mainmenu-delegate';
|
||||
import { makeSingleton } from '@singletons';
|
||||
|
||||
/**
|
||||
* Handler to inject an option into main menu.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CoreGradesMainMenuHandlerService implements CoreMainMenuHandler {
|
||||
|
||||
static readonly PAGE_NAME = 'grades';
|
||||
|
||||
name = 'CoreGrades';
|
||||
priority = 600;
|
||||
|
||||
/**
|
||||
* 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 CoreGrades.isCourseGradesEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data needed to render the handler.
|
||||
*
|
||||
* @return Data needed to render the handler.
|
||||
*/
|
||||
getDisplayData(): CoreMainMenuHandlerData {
|
||||
return {
|
||||
icon: 'fas-chart-bar',
|
||||
title: 'core.grades.grades',
|
||||
page: CoreGradesMainMenuHandlerService.PAGE_NAME,
|
||||
class: 'core-grades-coursesgrades-handler',
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const CoreGradesMainMenuHandler = makeSingleton(CoreGradesMainMenuHandlerService);
|
|
@ -22,6 +22,7 @@ import {
|
|||
CoreUserProfileHandlerData,
|
||||
} from '@features/user/services/user-delegate';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { makeSingleton } from '@singletons';
|
||||
|
||||
|
@ -31,6 +32,8 @@ import { makeSingleton } from '@singletons';
|
|||
@Injectable({ providedIn: 'root' })
|
||||
export class CoreGradesUserHandlerService implements CoreUserProfileHandler {
|
||||
|
||||
static readonly PAGE_NAME = 'grades';
|
||||
|
||||
name = 'CoreGrades:viewGrades';
|
||||
priority = 400;
|
||||
type = CoreUserDelegateService.TYPE_NEW_PAGE;
|
||||
|
@ -47,32 +50,54 @@ export class CoreGradesUserHandlerService implements CoreUserProfileHandler {
|
|||
* @inheritdoc
|
||||
*/
|
||||
async isEnabledForCourse(courseId?: number): Promise<boolean> {
|
||||
return CoreUtils.ignoreErrors(CoreGrades.isPluginEnabledForCourse(courseId), false);
|
||||
if (courseId) {
|
||||
return CoreUtils.ignoreErrors(CoreGrades.isPluginEnabledForCourse(courseId), false);
|
||||
} else {
|
||||
return CoreGrades.isCourseGradesEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabledForUser(user: CoreUserProfile, courseId?: number): Promise<boolean> {
|
||||
return CoreUtils.promiseWorks(CoreGrades.getCourseGradesTable(courseId!, user.id));
|
||||
if (courseId) {
|
||||
return CoreUtils.promiseWorks(CoreGrades.getCourseGradesTable(courseId, user.id));
|
||||
}
|
||||
|
||||
// All course grades only available for the current user.
|
||||
return user.id == CoreSites.getCurrentSiteUserId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getDisplayData(): CoreUserProfileHandlerData {
|
||||
return {
|
||||
icon: 'fas-chart-bar',
|
||||
title: 'core.grades.grades',
|
||||
class: 'core-grades-user-handler',
|
||||
action: (event, user, courseId): void => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
CoreNavigator.navigateToSitePath(`/user-grades/${courseId}`, {
|
||||
params: { userId: user.id },
|
||||
});
|
||||
},
|
||||
};
|
||||
getDisplayData(user: CoreUserProfile, courseId?: number): CoreUserProfileHandlerData {
|
||||
if (courseId) {
|
||||
return {
|
||||
icon: 'fas-chart-bar',
|
||||
title: 'core.grades.grades',
|
||||
class: 'core-grades-user-handler',
|
||||
action: (event, user, courseId): void => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
CoreNavigator.navigateToSitePath(`/user-grades/${courseId}`, {
|
||||
params: { userId: user.id },
|
||||
});
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
icon: 'fas-chart-bar',
|
||||
title: 'core.grades.grades',
|
||||
class: 'core-grades-coursesgrades-handler',
|
||||
action: (event): void => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
CoreNavigator.navigateToSitePath(CoreGradesUserHandlerService.PAGE_NAME);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 { NgModule } from '@angular/core';
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { CoreLoginSiteOnboardingComponent } from './site-onboarding/site-onboarding';
|
||||
import { CoreLoginSiteHelpComponent } from './site-help/site-help';
|
||||
import { CoreLoginSitesComponent } from './sites/sites';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreLoginSiteOnboardingComponent,
|
||||
CoreLoginSiteHelpComponent,
|
||||
CoreLoginSitesComponent,
|
||||
],
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
],
|
||||
exports: [
|
||||
CoreLoginSiteOnboardingComponent,
|
||||
CoreLoginSiteHelpComponent,
|
||||
CoreLoginSitesComponent,
|
||||
],
|
||||
})
|
||||
export class CoreLoginComponentsModule {}
|
|
@ -0,0 +1,93 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-button fill="clear" (click)="close($event)" [attr.aria-label]="'core.back' | translate">
|
||||
<ion-icon name="arrow-back" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
|
||||
<h1>{{ 'core.mainmenu.switchaccount' | translate }}</h1>
|
||||
|
||||
<ion-buttons slot="end">
|
||||
<ion-button fill="clear" *ngIf="accountsList.count > 1" (click)="toggleDelete()"
|
||||
[attr.aria-label]="'core.login.toggleremove' | translate">
|
||||
<ion-icon slot="icon-only" name="fas-pen" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<ion-list>
|
||||
<ng-container *ngIf="accountsList.currentSite">
|
||||
<ion-item-divider sticky="true">
|
||||
<ion-label>
|
||||
<h2>
|
||||
<core-format-text [text]="accountsList.currentSite.siteName" clean="true"
|
||||
[siteId]="accountsList.currentSite.id"></core-format-text>
|
||||
</h2>
|
||||
<p><a [href]="accountsList.currentSite.siteUrl" core-link autoLogin="yes">{{
|
||||
accountsList.currentSite.siteUrlWithoutProtocol }}</a>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
|
||||
<ion-item detail="false" class="item-current">
|
||||
<ion-avatar slot="start">
|
||||
<img [src]="accountsList.currentSite.avatar" core-external-content [siteId]="accountsList.currentSite.id"
|
||||
alt="{{ 'core.pictureof' | translate:{$a: accountsList.currentSite.fullName} }}"
|
||||
onError="this.src='assets/img/user-avatar.png'">
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<p class="item-heading">{{accountsList.currentSite.fullName}}</p>
|
||||
</ion-label>
|
||||
<ion-icon color="success" name="fas-check"></ion-icon>
|
||||
</ion-item>
|
||||
|
||||
<ng-container *ngTemplateOutlet="siteList; context: {sites: accountsList.sameSite}"></ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngFor="let sites of accountsList.otherSites">
|
||||
<ion-item-divider sticky="true" *ngIf="sites[0]">
|
||||
<ion-label>
|
||||
<h2>
|
||||
<core-format-text [text]="sites[0].siteName" clean="true" [siteId]="sites[0].id"></core-format-text>
|
||||
</h2>
|
||||
<p><a [href]="sites[0].siteUrl" core-link autoLogin="no">{{ sites[0].siteUrlWithoutProtocol }}</a></p>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
|
||||
<ng-container *ngTemplateOutlet="siteList; context: {sites: sites}"></ng-container>
|
||||
</ng-container>
|
||||
|
||||
</ion-list>
|
||||
</core-loading>
|
||||
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end">
|
||||
<ion-fab-button (click)="add($event)" [attr.aria-label]="'core.login.add' | translate">
|
||||
<ion-icon name="fas-plus" aria-hidden="true"></ion-icon>
|
||||
<span class="sr-only">{{ 'core.login.add' | translate }}</span>
|
||||
</ion-fab-button>
|
||||
</ion-fab>
|
||||
</ion-content>
|
||||
|
||||
<!-- Template to render a list of sites. -->
|
||||
<ng-template #siteList let-sites="sites">
|
||||
<ion-item button *ngFor="let site of sites" (click)="login($event, site.id)" detail="true">
|
||||
<ion-avatar slot="start">
|
||||
<img [src]="site.avatar" core-external-content [siteId]="site.id" alt="{{ 'core.pictureof' | translate:{$a: site.fullName} }}"
|
||||
onError="this.src='assets/img/user-avatar.png'">
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<p class="item-heading">{{site.fullName}}</p>
|
||||
</ion-label>
|
||||
<ion-badge slot="end" *ngIf="!showDelete && site.badge" @coreShowHideAnimation>
|
||||
<span aria-hidden="true">{{site.badge}}</span>
|
||||
<span class="sr-only">{{ 'core.login.sitebadgedescription' | translate:{ count: site.badge }
|
||||
}}</span>
|
||||
</ion-badge>
|
||||
<ion-button *ngIf="showDelete" slot="end" fill="clear" color="danger" (click)="deleteSite($event, site)"
|
||||
[attr.aria-label]="'core.login.removeaccount' | translate" [@coreSlideInOut]="'fromRight'">
|
||||
<ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</ng-template>
|
|
@ -0,0 +1,131 @@
|
|||
// (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 { CoreDomUtils } from '@services/utils/dom';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
import { CoreSiteBasicInfo, CoreSites } from '@services/sites';
|
||||
import { CoreAccountsList, CoreLoginHelper } from '@features/login/services/login-helper';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreFilter } from '@features/filter/services/filter';
|
||||
import { CoreAnimations } from '@components/animations';
|
||||
import { ModalController } from '@singletons';
|
||||
|
||||
/**
|
||||
* Component that displays a "splash screen" while the app is being initialized.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-login-sites',
|
||||
templateUrl: 'sites.html',
|
||||
animations: [CoreAnimations.SLIDE_IN_OUT, CoreAnimations.SHOW_HIDE],
|
||||
})
|
||||
export class CoreLoginSitesComponent implements OnInit {
|
||||
|
||||
accountsList: CoreAccountsList = {
|
||||
sameSite: [],
|
||||
otherSites: [],
|
||||
count: 0,
|
||||
};
|
||||
|
||||
showDelete = false;
|
||||
currentSiteId: string;
|
||||
loaded = false;
|
||||
|
||||
constructor() {
|
||||
this.currentSiteId = CoreSites.getRequiredCurrentSite().getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.accountsList = await CoreLoginHelper.getAccountsList(this.currentSiteId);
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to the page to add a site.
|
||||
*
|
||||
* @param event Click event.
|
||||
*/
|
||||
async add(event: Event): Promise<void> {
|
||||
await this.close(event, true);
|
||||
|
||||
await CoreLoginHelper.goToAddSite(true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a site.
|
||||
*
|
||||
* @param event Click event.
|
||||
* @param site Site to delete.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async deleteSite(event: Event, site: CoreSiteBasicInfo): Promise<void> {
|
||||
event.stopPropagation();
|
||||
|
||||
let siteName = site.siteName || '';
|
||||
|
||||
siteName = await CoreFilter.formatText(siteName, { clean: true, singleLine: true, filter: false }, [], site.id);
|
||||
|
||||
try {
|
||||
await CoreDomUtils.showDeleteConfirm('core.login.confirmdeletesite', { sitename: siteName });
|
||||
} catch {
|
||||
// User cancelled, stop.
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await CoreLoginHelper.deleteAccountFromList(this.accountsList, site);
|
||||
|
||||
this.showDelete = false;
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'core.login.errordeletesite', true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Login in a site.
|
||||
*
|
||||
* @param event Click event.
|
||||
* @param siteId The site ID.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async login(event: Event, siteId: string): Promise<void> {
|
||||
await this.close(event, true);
|
||||
|
||||
// This navigation will logout and navigate to the site home.
|
||||
await CoreNavigator.navigateToSiteHome({ preferCurrentTab: false , siteId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle delete.
|
||||
*/
|
||||
toggleDelete(): void {
|
||||
this.showDelete = !this.showDelete;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close modal.
|
||||
*
|
||||
* @param event Click event.
|
||||
*/
|
||||
async close(event: Event, closeAll = false): Promise<void> {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
await ModalController.dismiss(closeAll);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
{
|
||||
"accounts": "Accounts",
|
||||
"add": "Add a new account",
|
||||
"auth_email": "Email-based self-registration",
|
||||
"authenticating": "Authenticating",
|
||||
"cancel": "Cancel",
|
||||
|
@ -8,7 +10,7 @@
|
|||
"changepasswordinstructions": "You cannot change your password in the app. Please click the following button to open the site in a web browser to change your password. Take into account you need to close the browser after changing the password as you will not be redirected to the app.",
|
||||
"changepasswordlogoutinstructions": "If you prefer to change site or log out, please click the following button:",
|
||||
"changepasswordreconnectinstructions": "Click the following button to reconnect to the site. (Take into account that if you didn't change your password successfully, you would return to the previous screen).",
|
||||
"confirmdeletesite": "Are you sure you want to delete the site {{sitename}}?",
|
||||
"confirmdeletesite": "Are you sure you want to remove the account on {{sitename}}?",
|
||||
"connect": "Connect!",
|
||||
"connecttomoodle": "Connect to Moodle",
|
||||
"contactyouradministrator": "Contact your site administrator for further help.",
|
||||
|
@ -23,7 +25,7 @@
|
|||
"emailconfirmsentsuccess": "Confirmation email sent successfully",
|
||||
"emailnotmatch": "Emails do not match",
|
||||
"erroraccesscontrolalloworigin": "The cross-origin call you're trying to perform has been rejected. Please check https://docs.moodle.org/dev/Moodle_Mobile_development_using_Chrome_or_Chromium",
|
||||
"errordeletesite": "An error occurred while deleting this site. Please try again.",
|
||||
"errordeletesite": "An error occurred while deleting this account. Please try again.",
|
||||
"errorexampleurl": "The URL https://campus.example.edu is only an example URL, it's not a real site. <strong>Please use the URL of your school or organization's site.</strong>",
|
||||
"errorqrnoscheme": "This URL isn't a valid login URL.",
|
||||
"errorupdatesite": "An error occurred while updating the site's token.",
|
||||
|
@ -95,11 +97,12 @@
|
|||
"reconnect": "Reconnect",
|
||||
"reconnectdescription": "Your authentication token is invalid or has expired. You have to reconnect to the site.",
|
||||
"reconnectssodescription": "Your authentication token is invalid or has expired. You have to reconnect to the site. You need to log in to the site in a browser window.",
|
||||
"removeaccount": "Remove account",
|
||||
"resendemail": "Resend email",
|
||||
"searchby": "Search by:",
|
||||
"security_question": "Security question",
|
||||
"selectacountry": "Select a country",
|
||||
"selectsite": "Please select your site:",
|
||||
"selectsite": "Please select your account:",
|
||||
"signupplugindisabled": "{{$a}} is not enabled.",
|
||||
"signuprequiredfieldnotsupported": "The signup form contains a required custom field that isn't supported in the app. Please create your account using a web browser.",
|
||||
"siteaddress": "Your site",
|
||||
|
@ -114,6 +117,7 @@
|
|||
"startsignup": "Create new account",
|
||||
"stillcantconnect": "Still can't connect?",
|
||||
"supplyinfo": "More details",
|
||||
"toggleremove": "Edit accounts list",
|
||||
"username": "Username",
|
||||
"usernameoremail": "Enter either username or email address",
|
||||
"usernamerequired": "Username required",
|
||||
|
|
|
@ -16,9 +16,8 @@ import { NgModule } from '@angular/core';
|
|||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { CoreLoginSiteHelpComponent } from './components/site-help/site-help';
|
||||
import { CoreLoginSiteOnboardingComponent } from './components/site-onboarding/site-onboarding';
|
||||
import { CoreLoginHasSitesGuard } from './guards/has-sites';
|
||||
import { CoreLoginComponentsModule } from './components/components.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
|
@ -67,11 +66,8 @@ const routes: Routes = [
|
|||
@NgModule({
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
CoreLoginComponentsModule,
|
||||
RouterModule.forChild(routes),
|
||||
],
|
||||
declarations: [
|
||||
CoreLoginSiteHelpComponent,
|
||||
CoreLoginSiteOnboardingComponent,
|
||||
],
|
||||
})
|
||||
export class CoreLoginLazyModule {}
|
||||
|
|
|
@ -35,7 +35,9 @@ const appRoutes: Routes = [
|
|||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [AppRoutingModule.forChild(appRoutes)],
|
||||
imports: [
|
||||
AppRoutingModule.forChild(appRoutes),
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
|
|
|
@ -35,17 +35,17 @@ export class CoreLoginSitePolicyPage implements OnInit {
|
|||
showInline?: boolean;
|
||||
policyLoaded?: boolean;
|
||||
protected siteId?: string;
|
||||
protected currentSite?: CoreSite;
|
||||
protected currentSite!: CoreSite;
|
||||
|
||||
/**
|
||||
* Component initialized.
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
|
||||
this.siteId = CoreNavigator.getRouteParam('siteId');
|
||||
this.currentSite = CoreSites.getCurrentSite();
|
||||
|
||||
if (!this.currentSite) {
|
||||
try {
|
||||
this.currentSite = CoreSites.getRequiredCurrentSite();
|
||||
} catch {
|
||||
// Not logged in, stop.
|
||||
this.cancel();
|
||||
|
||||
|
@ -86,7 +86,7 @@ export class CoreLoginSitePolicyPage implements OnInit {
|
|||
|
||||
const extension = CoreMimetypeUtils.getExtension(mimeType, this.sitePolicy);
|
||||
this.showInline = extension == 'html' || extension == 'htm';
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Unable to get mime type, assume it's not supported.
|
||||
this.showInline = false;
|
||||
} finally {
|
||||
|
@ -118,7 +118,7 @@ export class CoreLoginSitePolicyPage implements OnInit {
|
|||
|
||||
// Success accepting, go to site initial page.
|
||||
// Invalidate cache since some WS don't return error if site policy is not accepted.
|
||||
await CoreUtils.ignoreErrors(this.currentSite!.invalidateWsCache());
|
||||
await CoreUtils.ignoreErrors(this.currentSite.invalidateWsCache());
|
||||
|
||||
await CoreNavigator.navigateToSiteHome();
|
||||
} catch (error) {
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
|
||||
<h1>{{ 'core.settings.sites' | translate }}</h1>
|
||||
<h1>{{ 'core.login.accounts' | translate }}</h1>
|
||||
|
||||
<ion-buttons slot="end">
|
||||
<ion-button fill="clear" *ngIf="sites && sites.length > 0" (click)="toggleDelete()"
|
||||
[attr.aria-label]="'core.delete' | translate">
|
||||
<ion-button fill="clear" *ngIf="accountsList.count > 0" (click)="toggleDelete()"
|
||||
[attr.aria-label]="'core.login.toggleremove' | translate">
|
||||
<ion-icon slot="icon-only" name="fas-pen" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button (click)="openSettings()" [attr.aria-label]="'core.settings.appsettings' | translate">
|
||||
|
@ -18,31 +18,43 @@
|
|||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-item button (click)="login(site.id)" *ngFor="let site of sites" detail="true">
|
||||
<ion-avatar slot="start">
|
||||
<img [src]="site.avatar" core-external-content [siteId]="site.id"
|
||||
alt="{{ 'core.pictureof' | translate:{$a: site.fullName} }}" onError="this.src='assets/img/user-avatar.png'">
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<p class="item-heading">{{site.fullName}}</p>
|
||||
<p><core-format-text [text]="site.siteName" clean="true" [siteId]="site.id"></core-format-text></p>
|
||||
<p>{{site.siteUrl}}</p>
|
||||
</ion-label>
|
||||
<ion-badge slot="end" *ngIf="!showDelete && site.badge">
|
||||
<span aria-hidden="true">{{site.badge}}</span>
|
||||
<span class="sr-only">{{ 'core.login.sitebadgedescription' | translate:{ count: site.badge } }}</span>
|
||||
</ion-badge>
|
||||
<ion-button *ngIf="showDelete" slot="end" fill="clear" color="danger" (click)="deleteSite($event, site)"
|
||||
[attr.aria-label]="'core.delete' | translate" [@coreSlideInOut]="'fromRight'">
|
||||
<ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<ion-list>
|
||||
<ng-container *ngFor="let sites of accountsList.otherSites">
|
||||
<ion-item-divider sticky="true" *ngIf="sites[0]">
|
||||
<ion-label>
|
||||
<h2>
|
||||
<core-format-text [text]="sites[0].siteName" clean="true" [siteId]="sites[0].id"></core-format-text>
|
||||
</h2>
|
||||
<p><a [href]="sites[0].siteUrl" core-link autoLogin="no">{{ sites[0].siteUrlWithoutProtocol }}</a></p>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
|
||||
<ion-item button *ngFor="let site of sites" (click)="login($event, site.id)" detail="true">
|
||||
<ion-avatar slot="start">
|
||||
<img [src]="site.avatar" core-external-content [siteId]="site.id"
|
||||
alt="{{ 'core.pictureof' | translate:{$a: site.fullName} }}" onError="this.src='assets/img/user-avatar.png'">
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<p class="item-heading">{{site.fullName}}</p>
|
||||
</ion-label>
|
||||
<ion-badge slot="end" *ngIf="!showDelete && site.badge" @coreShowHideAnimation>
|
||||
<span aria-hidden="true">{{site.badge}}</span>
|
||||
<span class="sr-only">{{ 'core.login.sitebadgedescription' | translate:{ count: site.badge }
|
||||
}}</span>
|
||||
</ion-badge>
|
||||
<ion-button *ngIf="showDelete" slot="end" fill="clear" color="danger" (click)="deleteSite($event, site)"
|
||||
[attr.aria-label]="'core.login.removeaccount' | translate" [@coreSlideInOut]="'fromRight'">
|
||||
<ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
</core-loading>
|
||||
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end">
|
||||
<ion-fab-button (click)="add()" [attr.aria-label]="'core.add' | translate">
|
||||
<ion-fab-button (click)="add()" [attr.aria-label]="'core.login.add' | translate">
|
||||
<ion-icon name="fas-plus" aria-hidden="true"></ion-icon>
|
||||
<span class="sr-only">{{ 'core.add' | translate }}</span>
|
||||
<span class="sr-only">{{ 'core.login.add' | translate }}</span>
|
||||
</ion-fab-button>
|
||||
</ion-fab>
|
||||
</ion-content>
|
||||
|
|
|
@ -13,14 +13,11 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
import { CoreSiteBasicInfo, CoreSites } from '@services/sites';
|
||||
import { CoreLogger } from '@singletons/logger';
|
||||
import { CoreLoginHelper } from '@features/login/services/login-helper';
|
||||
import { CoreAccountsList, CoreLoginHelper } from '@features/login/services/login-helper';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications';
|
||||
import { CoreFilter } from '@features/filter/services/filter';
|
||||
import { CoreAnimations } from '@components/animations';
|
||||
|
||||
|
@ -30,40 +27,33 @@ import { CoreAnimations } from '@components/animations';
|
|||
@Component({
|
||||
selector: 'page-core-login-sites',
|
||||
templateUrl: 'sites.html',
|
||||
animations: [CoreAnimations.SLIDE_IN_OUT],
|
||||
animations: [CoreAnimations.SLIDE_IN_OUT, CoreAnimations.SHOW_HIDE],
|
||||
})
|
||||
export class CoreLoginSitesPage implements OnInit {
|
||||
|
||||
sites: CoreSiteBasicInfo[] = [];
|
||||
accountsList: CoreAccountsList = {
|
||||
sameSite: [],
|
||||
otherSites: [],
|
||||
count: 0,
|
||||
};
|
||||
|
||||
showDelete = false;
|
||||
|
||||
protected logger: CoreLogger;
|
||||
|
||||
constructor() {
|
||||
this.logger = CoreLogger.getInstance('CoreLoginSitesPage');
|
||||
}
|
||||
loaded = false;
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
if (CoreNavigator.getRouteBooleanParam('openAddSite')) {
|
||||
this.add();
|
||||
}
|
||||
|
||||
const sites = await CoreUtils.ignoreErrors(CoreSites.getSortedSites(), [] as CoreSiteBasicInfo[]);
|
||||
this.accountsList = await CoreLoginHelper.getAccountsList();
|
||||
this.loaded = true;
|
||||
|
||||
// Remove protocol from the url to show more url text.
|
||||
this.sites = await Promise.all(sites.map(async (site) => {
|
||||
site.siteUrl = site.siteUrl.replace(/^https?:\/\//, '');
|
||||
site.badge = await CoreUtils.ignoreErrors(CorePushNotifications.getSiteCounter(site.id)) || 0;
|
||||
|
||||
return site;
|
||||
}));
|
||||
|
||||
this.showDelete = false;
|
||||
if (this.accountsList.count == 0) {
|
||||
this.add();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -76,12 +66,12 @@ export class CoreLoginSitesPage implements OnInit {
|
|||
/**
|
||||
* Delete a site.
|
||||
*
|
||||
* @param e Click event.
|
||||
* @param event Click event.
|
||||
* @param site Site to delete.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async deleteSite(e: Event, site: CoreSiteBasicInfo): Promise<void> {
|
||||
e.stopPropagation();
|
||||
async deleteSite(event: Event, site: CoreSiteBasicInfo): Promise<void> {
|
||||
event.stopPropagation();
|
||||
|
||||
let siteName = site.siteName || '';
|
||||
|
||||
|
@ -95,20 +85,15 @@ export class CoreLoginSitesPage implements OnInit {
|
|||
}
|
||||
|
||||
try {
|
||||
await CoreSites.deleteSite(site.id);
|
||||
await CoreLoginHelper.deleteAccountFromList(this.accountsList, site);
|
||||
|
||||
const index = this.sites.findIndex((listedSite) => listedSite.id == site.id);
|
||||
index >= 0 && this.sites.splice(index, 1);
|
||||
this.showDelete = false;
|
||||
|
||||
// If there are no sites left, go to add site.
|
||||
const hasSites = await CoreSites.hasSites();
|
||||
|
||||
if (!hasSites) {
|
||||
if (this.accountsList.count == 0) {
|
||||
CoreLoginHelper.goToAddSite(true, true);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Error deleting site ' + site.id, error);
|
||||
CoreDomUtils.showErrorModalDefault(error, 'core.login.errordeletesite', true);
|
||||
}
|
||||
}
|
||||
|
@ -116,10 +101,14 @@ export class CoreLoginSitesPage implements OnInit {
|
|||
/**
|
||||
* Login in a site.
|
||||
*
|
||||
* @param event Click event.
|
||||
* @param siteId The site ID.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async login(siteId: string): Promise<void> {
|
||||
async login(event: Event, siteId: string): Promise<void> {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const modal = await CoreDomUtils.showModalLoading();
|
||||
|
||||
try {
|
||||
|
@ -131,7 +120,6 @@ export class CoreLoginSitesPage implements OnInit {
|
|||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Error loading site ' + siteId, error);
|
||||
CoreDomUtils.showErrorModalDefault(error, 'Error loading site.');
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
|
|
|
@ -19,7 +19,7 @@ import { Md5 } from 'ts-md5/dist/md5';
|
|||
import { CoreApp, CoreStoreConfig } from '@services/app';
|
||||
import { CoreConfig } from '@services/config';
|
||||
import { CoreEvents, CoreEventSessionExpiredData, CoreEventSiteData } from '@singletons/events';
|
||||
import { CoreSites, CoreLoginSiteInfo } from '@services/sites';
|
||||
import { CoreSites, CoreLoginSiteInfo, CoreSiteBasicInfo } from '@services/sites';
|
||||
import { CoreWS, CoreWSExternalWarning } from '@services/ws';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
|
@ -35,6 +35,8 @@ import { CoreUrl } from '@singletons/url';
|
|||
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
|
||||
import { CoreCanceledError } from '@classes/errors/cancelederror';
|
||||
import { CoreCustomURLSchemes } from '@services/urlschemes';
|
||||
import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins';
|
||||
import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications';
|
||||
|
||||
/**
|
||||
* Helper provider that provides some common features regarding authentication.
|
||||
|
@ -311,7 +313,7 @@ export class CoreLoginHelperProvider {
|
|||
site = site || CoreSites.getCurrentSite();
|
||||
const config = site?.getStoredConfig();
|
||||
|
||||
return 'core.mainmenu.' + (config && config.tool_mobile_forcelogout == '1' ? 'logout' : 'changesite');
|
||||
return 'core.mainmenu.' + (config && config.tool_mobile_forcelogout == '1' ? 'logout' : 'switchaccount');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -407,8 +409,25 @@ export class CoreLoginHelperProvider {
|
|||
* @param showKeyboard Whether to show keyboard in the new page. Only if no fixed URL set.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async goToAddSite(setRoot?: boolean, showKeyboard?: boolean): Promise<void> {
|
||||
const [path, params] = this.getAddSiteRouteInfo(showKeyboard);
|
||||
async goToAddSite(setRoot = false, showKeyboard = false): Promise<void> {
|
||||
let path = '/login/sites';
|
||||
let params: Params = { openAddSite: true , showKeyboard };
|
||||
|
||||
if (CoreSites.isLoggedIn()) {
|
||||
|
||||
if (CoreSitePlugins.hasSitePluginsLoaded) {
|
||||
// The site has site plugins so the app will be restarted. Store the data and logout.
|
||||
CoreApp.storeRedirect(CoreConstants.NO_SITE_ID, path, { params });
|
||||
|
||||
await CoreSites.logout();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await CoreSites.logout();
|
||||
} else {
|
||||
[path, params] = this.getAddSiteRouteInfo(showKeyboard);
|
||||
}
|
||||
|
||||
await CoreNavigator.navigate(path, { params, reset: setRoot });
|
||||
}
|
||||
|
@ -1317,43 +1336,120 @@ export class CoreLoginHelperProvider {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the accounts list classified per site.
|
||||
*
|
||||
* @param currentSiteId If loggedin, current Site Id.
|
||||
* @return Promise resolved with account list.
|
||||
*/
|
||||
async getAccountsList(currentSiteId?: string): Promise<CoreAccountsList> {
|
||||
const sites = await CoreUtils.ignoreErrors(CoreSites.getSortedSites(), [] as CoreSiteBasicInfo[]);
|
||||
|
||||
const accountsList: CoreAccountsList = {
|
||||
sameSite: [],
|
||||
otherSites: [],
|
||||
count: sites.length,
|
||||
};
|
||||
|
||||
let siteUrl = '';
|
||||
|
||||
if (currentSiteId) {
|
||||
const index = sites.findIndex((site) => site.id == currentSiteId);
|
||||
|
||||
accountsList.currentSite = sites.splice(index, 1)[0];
|
||||
siteUrl = accountsList.currentSite.siteUrlWithoutProtocol;
|
||||
}
|
||||
|
||||
const otherSites: Record<string, CoreSiteBasicInfo[]> = {};
|
||||
|
||||
// Add site counter and classify sites.
|
||||
await Promise.all(sites.map(async (site) => {
|
||||
site.badge = await CoreUtils.ignoreErrors(CorePushNotifications.getSiteCounter(site.id)) || 0;
|
||||
|
||||
if (site.siteUrlWithoutProtocol == siteUrl) {
|
||||
accountsList.sameSite.push(site);
|
||||
} else {
|
||||
if (!otherSites[site.siteUrlWithoutProtocol]) {
|
||||
otherSites[site.siteUrlWithoutProtocol] = [];
|
||||
}
|
||||
|
||||
otherSites[site.siteUrlWithoutProtocol].push(site);
|
||||
}
|
||||
|
||||
return;
|
||||
}));
|
||||
|
||||
accountsList.otherSites = CoreUtils.objectToArray(otherSites);
|
||||
|
||||
return accountsList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and delete a site from the list of sites.
|
||||
*
|
||||
* @param accountsList Account list.
|
||||
* @param site Site to be deleted.
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
async deleteAccountFromList(accountsList: CoreAccountsList, site: CoreSiteBasicInfo): Promise<void> {
|
||||
await CoreSites.deleteSite(site.id);
|
||||
|
||||
const siteUrl = site.siteUrlWithoutProtocol;
|
||||
let index = 0;
|
||||
|
||||
// Found on same site.
|
||||
if (accountsList.sameSite.length > 0 && accountsList.sameSite[0].siteUrlWithoutProtocol == siteUrl) {
|
||||
index = accountsList.sameSite.findIndex((listedSite) => listedSite.id == site.id);
|
||||
if (index >= 0) {
|
||||
accountsList.sameSite.splice(index, 1);
|
||||
accountsList.count--;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const otherSiteIndex = accountsList.otherSites.findIndex((sites) =>
|
||||
sites.length > 0 && sites[0].siteUrlWithoutProtocol == siteUrl);
|
||||
if (otherSiteIndex < 0) {
|
||||
// Site Url not found.
|
||||
return;
|
||||
}
|
||||
|
||||
index = accountsList.otherSites[otherSiteIndex].findIndex((listedSite) => listedSite.id == site.id);
|
||||
if (index >= 0) {
|
||||
accountsList.otherSites[otherSiteIndex].splice(index, 1);
|
||||
accountsList.count--;
|
||||
}
|
||||
|
||||
if (accountsList.otherSites[otherSiteIndex].length == 0) {
|
||||
accountsList.otherSites.splice(otherSiteIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const CoreLoginHelper = makeSingleton(CoreLoginHelperProvider);
|
||||
|
||||
/**
|
||||
* Accounts list for selecting sites interfaces.
|
||||
*/
|
||||
export type CoreAccountsList = {
|
||||
currentSite?: CoreSiteBasicInfo; // If logged in, current site info.
|
||||
sameSite: CoreSiteBasicInfo[]; // If logged in, accounts info on the same site.
|
||||
otherSites: CoreSiteBasicInfo[][]; // Other accounts in other sites.
|
||||
count: number; // Number of sites.
|
||||
};
|
||||
|
||||
/**
|
||||
* Data related to a SSO authentication.
|
||||
*/
|
||||
export interface CoreLoginSSOData {
|
||||
/**
|
||||
* The site's URL.
|
||||
*/
|
||||
siteUrl: string;
|
||||
|
||||
/**
|
||||
* User's token.
|
||||
*/
|
||||
token?: string;
|
||||
|
||||
/**
|
||||
* User's private token.
|
||||
*/
|
||||
privateToken?: string;
|
||||
|
||||
/**
|
||||
* Name of the page to go after authenticated.
|
||||
*/
|
||||
pageName?: string;
|
||||
|
||||
/**
|
||||
* Options of the navigation to the page.
|
||||
*/
|
||||
pageOptions?: CoreNavigationOptions;
|
||||
|
||||
/**
|
||||
* Other params added to the login url.
|
||||
*/
|
||||
ssoUrlParams?: CoreUrlParams;
|
||||
siteUrl: string; // The site's URL.
|
||||
token?: string; // User's token.
|
||||
privateToken?: string; // User's private token.
|
||||
pageName?: string; // Name of the page to go after authenticated.
|
||||
pageOptions?: CoreNavigationOptions; // Options of the navigation to the page.
|
||||
ssoUrlParams?: CoreUrlParams; // Other params added to the login url.
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
// (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 { CoreSharedModule } from '@/core/shared.module';
|
||||
import { CoreMainMenuUserButtonComponent } from './user-menu-button/user-menu-button';
|
||||
import { CoreMainMenuUserMenuComponent } from './user-menu/user-menu';
|
||||
import { CoreLoginComponentsModule } from '@features/login/components/components.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreMainMenuUserButtonComponent,
|
||||
CoreMainMenuUserMenuComponent,
|
||||
],
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
CoreLoginComponentsModule,
|
||||
],
|
||||
exports: [
|
||||
CoreMainMenuUserButtonComponent,
|
||||
CoreMainMenuUserMenuComponent,
|
||||
],
|
||||
})
|
||||
export class CoreMainMenuComponentsModule {}
|
|
@ -0,0 +1,3 @@
|
|||
<core-user-avatar *ngIf="isMainScreen && siteInfo" [user]="siteInfo" class="core-bar-button-image clickable" [linkProfile]="false"
|
||||
(ariaButtonClick)="openUserMenu($event)" role="button" tabindex="0" [attr.aria-label]="'core.user.account' | translate">
|
||||
</core-user-avatar>
|
|
@ -0,0 +1,3 @@
|
|||
:host-context(ion-tabs.placement-side div.tabs-inner) {
|
||||
display: none;
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
// (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';
|
||||
import { CoreSiteInfo } from '@classes/site';
|
||||
import { IonRouterOutlet } from '@ionic/angular';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreMainMenuUserMenuComponent } from '../user-menu/user-menu';
|
||||
|
||||
/**
|
||||
* Component to display an avatar on the header to open user menu.
|
||||
*
|
||||
* Example: <core-user-menu-button></core-user-menu-button>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-user-menu-button',
|
||||
templateUrl: 'user-menu-button.html',
|
||||
styleUrls: ['user-menu-button.scss'],
|
||||
})
|
||||
export class CoreMainMenuUserButtonComponent implements OnInit {
|
||||
|
||||
siteInfo?: CoreSiteInfo;
|
||||
isMainScreen = false;
|
||||
|
||||
constructor(protected routerOutlet: IonRouterOutlet) {
|
||||
const currentSite = CoreSites.getRequiredCurrentSite();
|
||||
|
||||
this.siteInfo = currentSite.getInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.isMainScreen = !this.routerOutlet.canGoBack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open User menu
|
||||
*
|
||||
* @param event Click event.
|
||||
*/
|
||||
openUserMenu(event: Event): void {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
CoreDomUtils.openSideModal<void>({
|
||||
component: CoreMainMenuUserMenuComponent,
|
||||
cssClass: 'core-modal-lateral-sm',
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-button fill="clear" (click)="close($event)" [attr.aria-label]="'core.close' | translate">
|
||||
<ion-icon name="fas-times" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<h1>
|
||||
{{'core.user.account' | translate}}
|
||||
</h1>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-item button class="core-user-profile-maininfo" *ngIf="siteInfo" lines="full" (click)="switchAccounts($event)" detail="true"
|
||||
[attr.aria-label]="'core.mainmenu.switchaccount' | translate">
|
||||
<core-user-avatar [user]="siteInfo" [userId]="siteInfo.userid" [linkProfile]="false" slot="start"></core-user-avatar>
|
||||
<ion-label>
|
||||
<h2>{{ siteInfo.fullname }}</h2>
|
||||
<p class="core-usermenu-siteinfo core-usermenu-sitename">
|
||||
<core-format-text [text]="siteName" contextLevel="system" [contextInstanceId]="0" [wsNotFiltered]="true">
|
||||
</core-format-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item button class="ion-text-wrap core-usermenu-handler" (click)="openUserProfile($event)"
|
||||
[attr.aria-label]="'core.user.details' | translate" detail="true" lines="none">
|
||||
<ion-icon name="fas-user" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.user.profile' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-text-center" *ngIf="(!handlers || !handlers.length) && !handlersLoaded" lines="none">
|
||||
<ion-label>
|
||||
<ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item button *ngFor="let handler of handlers" class="ion-text-wrap" (click)="handlerClicked($event, handler)"
|
||||
[ngClass]="['core-user-menu-handler', handler.class || '']" [hidden]="handler.hidden"
|
||||
[attr.aria-label]="handler.title | translate" detail="true" lines="none">
|
||||
<ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ handler.title | translate }}</p>
|
||||
</ion-label>
|
||||
<ion-badge slot="end" *ngIf="handler.showBadge" [hidden]="handler.loading || !handler.badge" aria-hidden="true">
|
||||
{{handler.badge}}
|
||||
</ion-badge>
|
||||
<span *ngIf="handler.showBadge && handler.badge && handler.badgeA11yText" class="sr-only">
|
||||
{{ handler.badgeA11yText | translate: {$a : handler.badge } }}
|
||||
</span>
|
||||
<ion-spinner slot="end" *ngIf="handler.showBadge && handler.loading" [attr.aria-label]="'core.loading' | translate">
|
||||
</ion-spinner>
|
||||
</ion-item>
|
||||
|
||||
<ion-item button (click)="openPreferences($event)" [attr.aria-label]="'core.settings.preferences' | translate" detail="true"
|
||||
class="core-user-menu-preferences">
|
||||
<ion-icon name="fas-wrench" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.settings.preferences' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
<ion-footer class="ion-padding">
|
||||
<ion-button (click)="logout($event)" expand="block" color="danger" [attr.aria-label]="'core.mainmenu.logout' | translate"
|
||||
class="ion-text-wrap">
|
||||
<ion-icon name="fas-sign-out-alt" slot="start" aria-hidden="true"></ion-icon>
|
||||
{{ 'core.mainmenu.logout' | translate }}
|
||||
</ion-button>
|
||||
</ion-footer>
|
|
@ -0,0 +1,26 @@
|
|||
@import "~theme/globals";
|
||||
|
||||
:host {
|
||||
.core-user-menu-preferences {
|
||||
--inner-border-width: 0;
|
||||
--border-width: 1px 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
@if ($core-user-hide-siteinfo) {
|
||||
.core-usermenu-siteinfo {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@if ($core-user-hide-sitename) {
|
||||
.core-usermenu-sitename {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@if ($core-user-hide-siteurl) {
|
||||
.core-usermenu-siteurl {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
// (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, OnDestroy, OnInit } from '@angular/core';
|
||||
import { CoreSiteInfo } from '@classes/site';
|
||||
import { CoreLoginSitesComponent } from '@features/login/components/sites/sites';
|
||||
import { CoreLoginHelper } from '@features/login/services/login-helper';
|
||||
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
|
||||
import { CoreUserProfileHandlerData, CoreUserDelegate, CoreUserDelegateService } from '@features/user/services/user-delegate';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { ModalController } from '@singletons';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Component to display a user menu.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-main-menu-user-menu',
|
||||
templateUrl: 'user-menu.html',
|
||||
styleUrls: ['user-menu.scss'],
|
||||
})
|
||||
export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy {
|
||||
|
||||
siteInfo?: CoreSiteInfo;
|
||||
siteName?: string;
|
||||
siteUrl?: string;
|
||||
handlers: CoreUserProfileHandlerData[] = [];
|
||||
handlersLoaded = false;
|
||||
loaded = false;
|
||||
user?: CoreUserProfile;
|
||||
moreSites = false;
|
||||
|
||||
protected subscription!: Subscription;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
// Check if there are more sites to switch.
|
||||
const sites = await CoreSites.getSites();
|
||||
this.moreSites = sites.length > 1;
|
||||
|
||||
const currentSite = CoreSites.getRequiredCurrentSite();
|
||||
this.siteInfo = currentSite.getInfo();
|
||||
this.siteName = currentSite.getSiteName();
|
||||
this.siteUrl = currentSite.getURL();
|
||||
|
||||
this.loaded = true;
|
||||
|
||||
// Load the handlers.
|
||||
if (this.siteInfo) {
|
||||
this.user = await CoreUser.getProfile(this.siteInfo.userid);
|
||||
|
||||
this.subscription = CoreUserDelegate.getProfileHandlersFor(this.user).subscribe((handlers) => {
|
||||
if (!handlers || !this.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.handlers = [];
|
||||
handlers.forEach((handler) => {
|
||||
if (handler.type == CoreUserDelegateService.TYPE_NEW_PAGE) {
|
||||
this.handlers.push(handler.data);
|
||||
}
|
||||
});
|
||||
|
||||
this.handlersLoaded = CoreUserDelegate.areHandlersLoaded(this.user.id);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens User profile page.
|
||||
*
|
||||
* @param event Click event.
|
||||
*/
|
||||
async openUserProfile(event: Event): Promise<void> {
|
||||
if (!this.siteInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.close(event);
|
||||
|
||||
CoreNavigator.navigateToSitePath('user/about', {
|
||||
params: {
|
||||
userId: this.siteInfo.userid,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens preferences.
|
||||
*
|
||||
* @param event Click event.
|
||||
*/
|
||||
async openPreferences(event: Event): Promise<void> {
|
||||
await this.close(event);
|
||||
|
||||
CoreNavigator.navigateToSitePath('preferences');
|
||||
}
|
||||
|
||||
/**
|
||||
* A handler was clicked.
|
||||
*
|
||||
* @param event Click event.
|
||||
* @param handler Handler that was clicked.
|
||||
*/
|
||||
async handlerClicked(event: Event, handler: CoreUserProfileHandlerData): Promise<void> {
|
||||
if (!this.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.close(event);
|
||||
|
||||
handler.action(event, this.user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout the user.
|
||||
*
|
||||
* @param event Click event
|
||||
*/
|
||||
async logout(event: Event): Promise<void> {
|
||||
await this.close(event);
|
||||
|
||||
CoreSites.logout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show account selector.
|
||||
*
|
||||
* @param event Click event
|
||||
*/
|
||||
async switchAccounts(event: Event): Promise<void> {
|
||||
const thisModal = await ModalController.getTop();
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const closeAll = await CoreDomUtils.openSideModal<boolean>({
|
||||
component: CoreLoginSitesComponent,
|
||||
cssClass: 'core-modal-lateral-sm',
|
||||
});
|
||||
|
||||
if (closeAll) {
|
||||
await ModalController.dismiss(undefined, undefined, thisModal.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add account.
|
||||
*
|
||||
* @param event Click event
|
||||
*/
|
||||
async addAccount(event: Event): Promise<void> {
|
||||
await this.close(event);
|
||||
|
||||
await CoreLoginHelper.goToAddSite(true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close modal.
|
||||
*/
|
||||
async close(event: Event): Promise<void> {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
await ModalController.dismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.subscription?.unsubscribe();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
{
|
||||
"changesite": "Change site",
|
||||
"help": "Help",
|
||||
"home": "Home",
|
||||
"logout": "Log out",
|
||||
"website": "Website"
|
||||
"switchaccount": "Switch account"
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import { MAIN_MENU_ROUTES } from './mainmenu-routing.module';
|
|||
import { CoreMainMenuPage } from './pages/menu/menu';
|
||||
import { CoreMainMenuHomeHandlerService } from './services/handlers/mainmenu';
|
||||
import { CoreMainMenuProvider } from './services/mainmenu';
|
||||
import { CoreMainMenuComponentsModule } from './components/components.module';
|
||||
|
||||
function buildRoutes(injector: Injector): Routes {
|
||||
const routes = resolveModuleRoutes(injector, MAIN_MENU_ROUTES);
|
||||
|
@ -54,6 +55,7 @@ function buildRoutes(injector: Injector): Routes {
|
|||
@NgModule({
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
CoreMainMenuComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
CoreMainMenuPage,
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
<img src="assets/img/top_logo.png" class="core-header-logo" [alt]="siteName">
|
||||
</h1>
|
||||
<ion-buttons slot="end">
|
||||
<core-user-menu-button></core-user-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
|
|
@ -22,6 +22,7 @@ import { CoreMainMenuHomePage } from './home';
|
|||
import { MAIN_MENU_HOME_ROUTES } from './home-routing.module';
|
||||
import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module';
|
||||
import { CoreMainMenuHomeHandlerService } from '@features/mainmenu/services/handlers/mainmenu';
|
||||
import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module';
|
||||
|
||||
function buildRoutes(injector: Injector): Routes {
|
||||
const routes = resolveModuleRoutes(injector, MAIN_MENU_HOME_ROUTES);
|
||||
|
@ -42,6 +43,7 @@ function buildRoutes(injector: Injector): Routes {
|
|||
@NgModule({
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
CoreMainMenuComponentsModule,
|
||||
],
|
||||
providers: [
|
||||
{ provide: ROUTES, multi: true, useFactory: buildRoutes, deps: [Injector] },
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
[@menuShowHideAnimation]="tabsPlacement == 'side' ? '' : (isMainScreen ? 'visible' : 'hidden')">
|
||||
<ion-spinner *ngIf="!loaded" [attr.aria-label]="'core.loading' | translate"></ion-spinner>
|
||||
|
||||
<core-user-menu-button *ngIf="loaded && tabsPlacement == 'side'"></core-user-menu-button>
|
||||
|
||||
<ion-tab-button *ngFor="let tab of tabs" (keydown)="tabAction.keyDown($event)" (keyup)="tabAction.keyUp(tab.page, $event)"
|
||||
[hidden]="!loaded && tab.hide" [tab]="tab.page" [disabled]="tab.hide" layout="label-hide" class="{{tab.class}}"
|
||||
[selected]="tab.page === selectedTab" [tabindex]="selectedTab == tab.page ? 0 : -1" [attr.aria-controls]="tab.id">
|
||||
|
@ -18,7 +20,7 @@
|
|||
|
||||
<ion-tab-button (keydown)="tabAction.keyDown($event)" (keyup)="tabAction.keyUp(morePageName, $event)" [hidden]="!loaded"
|
||||
[tab]="morePageName" layout="label-hide" [tabindex]="selectedTab == morePageName ? 0 : -1" [attr.aria-controls]="morePageName">
|
||||
<ion-icon name="fas-bars" aria-hidden="true"></ion-icon>
|
||||
<ion-icon name="ellipsis-horizontal" aria-hidden="true"></ion-icon>
|
||||
<ion-label aria-hidden="true">{{ 'core.more' | translate }}</ion-label>
|
||||
<span class="sr-only">{{ 'core.more' | translate }}</span>
|
||||
</ion-tab-button>
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
|
||||
@include padding(var(--ion-safe-area-top), 0px, var(--ion-safe-area-bottom), var(--ion-safe-area-left));
|
||||
|
||||
ion-tab-button {
|
||||
ion-tab-button, core-user-menu-button {
|
||||
width: 100%;
|
||||
min-height: var(--menutabbar-size);
|
||||
flex: 0;
|
||||
|
@ -83,6 +83,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
core-user-menu-button {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.core-network-message {
|
||||
--network-message-height: 16px;
|
||||
position: absolute;
|
||||
|
|
|
@ -4,98 +4,63 @@
|
|||
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
|
||||
<h1><core-format-text [text]="siteName" contextLevel="system" [contextInstanceId]="0"></core-format-text></h1>
|
||||
<h1>{{ 'core.more' | translate }}</h1>
|
||||
|
||||
<ion-buttons slot="end">
|
||||
<core-user-menu-button></core-user-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<core-loading [hideUntil]="!loggedOut">
|
||||
<ion-list>
|
||||
<ion-item button *ngIf="siteInfo" class="ion-text-wrap" core-user-link [userId]="siteInfo.userid" detail="true">
|
||||
<core-user-avatar [user]="siteInfo" slot="start"></core-user-avatar>
|
||||
<ion-list>
|
||||
<ion-item class="ion-text-center" *ngIf="(!handlers || !handlers.length) && !handlersLoaded">
|
||||
<ion-label><ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner></ion-label>
|
||||
</ion-item>
|
||||
<ion-item button *ngFor="let handler of handlers" [ngClass]="['core-moremenu-handler', handler.class || '']"
|
||||
(click)="openHandler(handler)" [attr.aria-label]="handler.title | translate" detail="true">
|
||||
<ion-icon [name]="handler.icon" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ handler.title | translate}}</p>
|
||||
</ion-label>
|
||||
<ion-badge slot="end" *ngIf="handler.showBadge" [hidden]="handler.loading || !handler.badge" aria-hidden="true">
|
||||
{{handler.badge}}
|
||||
</ion-badge>
|
||||
<span *ngIf="handler.showBadge && handler.badge && handler.badgeA11yText" class="sr-only">
|
||||
{{ handler.badgeA11yText | translate: {$a : handler.badge } }}
|
||||
</span>
|
||||
<ion-spinner slot="end" *ngIf="handler.showBadge && handler.loading" [attr.aria-label]="'core.loading' | translate">
|
||||
</ion-spinner>
|
||||
</ion-item>
|
||||
<ng-container *ngFor="let item of customItems">
|
||||
<ion-item button *ngIf="item.type != 'embedded'" [href]="item.url" [attr.aria-label]="item.label" core-link
|
||||
[capture]="item.type == 'app'" [inApp]="item.type == 'inappbrowser'" class="core-moremenu-customitem" detail="true"
|
||||
[detailIcon]="item.type == 'browser' ? 'open-outline' : 'chevron-forward'">
|
||||
<ion-icon [name]="item.icon" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<p class="item-heading">{{siteInfo.fullname}}</p>
|
||||
<p class="core-moremenu-siteinfo core-moremenu-sitename">
|
||||
<core-format-text [text]="siteName" contextLevel="system" [contextInstanceId]="0" [wsNotFiltered]="true">
|
||||
</core-format-text>
|
||||
</p>
|
||||
<p class="core-moremenu-siteinfo core-moremenu-siteurl">{{ siteUrl }}</p>
|
||||
<p class="item-heading">{{item.label}}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<core-spacer></core-spacer>
|
||||
<ion-item class="ion-text-center" *ngIf="(!handlers || !handlers.length) && !handlersLoaded">
|
||||
<ion-label><ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner></ion-label>
|
||||
</ion-item>
|
||||
<ion-item button *ngFor="let handler of handlers" [ngClass]="['core-moremenu-handler', handler.class || '']"
|
||||
(click)="openHandler(handler)" [attr.aria-label]="handler.title | translate" detail="true">
|
||||
<ion-icon [name]="handler.icon" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-item button *ngIf="item.type == 'embedded'" (click)="openItem(item)" [attr.aria-label]="item.label"
|
||||
class="core-moremenu-customitem" detail="true">
|
||||
<ion-icon [name]="item.icon" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ handler.title | translate}}</p>
|
||||
</ion-label>
|
||||
<ion-badge slot="end" *ngIf="handler.showBadge" [hidden]="handler.loading || !handler.badge" aria-hidden="true">
|
||||
{{handler.badge}}
|
||||
</ion-badge>
|
||||
<span *ngIf="handler.showBadge && handler.badge && handler.badgeA11yText" class="sr-only">
|
||||
{{ handler.badgeA11yText | translate: {$a : handler.badge } }}
|
||||
</span>
|
||||
<ion-spinner slot="end" *ngIf="handler.showBadge && handler.loading" [attr.aria-label]="'core.loading' | translate">
|
||||
</ion-spinner>
|
||||
</ion-item>
|
||||
<ng-container *ngFor="let item of customItems">
|
||||
<ion-item button *ngIf="item.type != 'embedded'" [href]="item.url" [attr.aria-label]="item.label" core-link
|
||||
[capture]="item.type == 'app'" [inApp]="item.type == 'inappbrowser'" class="core-moremenu-customitem" detail="true"
|
||||
[detailIcon]="item.type == 'browser' ? 'open-outline' : 'chevron-forward'">
|
||||
<ion-icon [name]="item.icon" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<p class="item-heading">{{item.label}}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button *ngIf="item.type == 'embedded'" (click)="openItem(item)" [attr.aria-label]="item.label"
|
||||
class="core-moremenu-customitem" detail="true">
|
||||
<ion-icon [name]="item.icon" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<p class="item-heading">{{item.label}}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
<ion-item button *ngIf="showScanQR" (click)="scanQR()" detail="true">
|
||||
<ion-icon name="fas-qrcode" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.scanqr' | translate }}</p>
|
||||
<p class="item-heading">{{item.label}}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button *ngIf="showWeb && siteInfo" [href]="siteInfo.siteurl" core-link autoLogin="yes"
|
||||
[attr.aria-label]="'core.mainmenu.website' | translate" detail="true" detailIcon="open-outline">
|
||||
<ion-icon name="fas-globe" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.mainmenu.website' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button *ngIf="showHelp" [href]="docsUrl" core-link autoLogin="no"
|
||||
[attr.aria-label]="'core.mainmenu.help' | translate" detail="true" detailIcon="open-outline">
|
||||
<ion-icon name="far-life-ring" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.mainmenu.help' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button (click)="openPreferences()" [attr.aria-label]="'core.settings.preferences' | translate" detail="true">
|
||||
<ion-icon name="fas-wrench" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.settings.preferences' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button (click)="logout()" [attr.aria-label]="logoutLabel | translate" detail="true">
|
||||
<ion-icon name="fas-sign-out-alt" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ logoutLabel | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<core-spacer></core-spacer>
|
||||
<ion-item button (click)="openSettings()" [attr.aria-label]="'core.settings.appsettings' | translate" detail="true">
|
||||
<ion-icon name="fas-cogs" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.settings.appsettings' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</core-loading>
|
||||
</ng-container>
|
||||
<ion-item button *ngIf="showScanQR" (click)="scanQR()" detail="true">
|
||||
<ion-icon name="fas-qrcode" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.scanqr' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
<ion-footer>
|
||||
<ion-item button (click)="openSettings()" [attr.aria-label]="'core.settings.appsettings' | translate" detail="true">
|
||||
<ion-icon name="fas-cogs" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.settings.appsettings' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-footer>
|
||||
|
|
|
@ -19,10 +19,12 @@ import { CoreSharedModule } from '@/core/shared.module';
|
|||
import { CoreMainMenuMorePage } from './more';
|
||||
import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module';
|
||||
import { CoreMainMenuProvider } from '@features/mainmenu/services/mainmenu';
|
||||
import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
CoreMainMenuComponentsModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
|
|
@ -17,21 +17,3 @@ ion-item {
|
|||
color: var(--core-more-icon, inherit);
|
||||
}
|
||||
}
|
||||
|
||||
@if ($core-more-hide-siteinfo) {
|
||||
.core-moremenu-siteinfo {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@if ($core-more-hide-sitename) {
|
||||
.core-moremenu-sitename {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@if ($core-more-hide-siteurl) {
|
||||
.core-moremenu-siteurl {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,8 +17,6 @@ import { Subscription } from 'rxjs';
|
|||
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreSiteInfo } from '@classes/site';
|
||||
import { CoreLoginHelper } from '@features/login/services/login-helper';
|
||||
import { CoreMainMenuDelegate, CoreMainMenuHandlerData } from '../../services/mainmenu-delegate';
|
||||
import { CoreMainMenu, CoreMainMenuCustomItem } from '../../services/mainmenu';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
|
@ -29,7 +27,7 @@ import { CoreTextUtils } from '@services/utils/text';
|
|||
import { Translate } from '@singletons';
|
||||
|
||||
/**
|
||||
* Page that displays the main menu of the app.
|
||||
* Page that displays the more page of the app.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-core-mainmenu-more',
|
||||
|
@ -39,38 +37,30 @@ import { Translate } from '@singletons';
|
|||
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;
|
||||
loggedOut = false;
|
||||
|
||||
protected allHandlers?: CoreMainMenuHandlerData[];
|
||||
protected subscription!: Subscription;
|
||||
protected langObserver: CoreEventObserver;
|
||||
protected updateSiteObserver: CoreEventObserver;
|
||||
|
||||
constructor() {
|
||||
this.langObserver = CoreEvents.on(CoreEvents.LANGUAGE_CHANGED, this.loadCustomMenuItems.bind(this));
|
||||
|
||||
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, async () => {
|
||||
this.customItems = await CoreMainMenu.getCustomMenuItems();
|
||||
}, CoreSites.getCurrentSiteId());
|
||||
|
||||
this.loadCustomMenuItems();
|
||||
|
||||
this.langObserver = CoreEvents.on(CoreEvents.LANGUAGE_CHANGED, this.loadSiteInfo.bind(this));
|
||||
this.updateSiteObserver = CoreEvents.on(
|
||||
CoreEvents.SITE_UPDATED,
|
||||
this.loadSiteInfo.bind(this),
|
||||
CoreSites.getCurrentSiteId(),
|
||||
);
|
||||
this.loadSiteInfo();
|
||||
this.showScanQR = CoreUtils.canScanQR() &&
|
||||
!CoreSites.getCurrentSite()?.isFeatureDisabled('CoreMainMenuDelegate_QrReader');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize component.
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Load the handlers.
|
||||
|
@ -84,7 +74,7 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
/**
|
||||
* Page destroyed.
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
window.removeEventListener('resize', this.initHandlers.bind(this));
|
||||
|
@ -113,24 +103,9 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
/**
|
||||
* Load the site info required by the view.
|
||||
* Load custom menu items.
|
||||
*/
|
||||
protected async loadSiteInfo(): Promise<void> {
|
||||
const currentSite = CoreSites.getCurrentSite();
|
||||
|
||||
if (!currentSite) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.siteInfo = currentSite.getInfo();
|
||||
this.siteName = currentSite.getSiteName();
|
||||
this.siteUrl = currentSite.getURL();
|
||||
this.logoutLabel = CoreLoginHelper.getLogoutLabel(currentSite);
|
||||
this.showWeb = !currentSite.isFeatureDisabled('CoreMainMenuDelegate_website');
|
||||
this.showHelp = !currentSite.isFeatureDisabled('CoreMainMenuDelegate_help');
|
||||
|
||||
this.docsUrl = await currentSite.getDocsUrl();
|
||||
|
||||
protected async loadCustomMenuItems(): Promise<void> {
|
||||
this.customItems = await CoreMainMenu.getCustomMenuItems();
|
||||
}
|
||||
|
||||
|
@ -154,13 +129,6 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy {
|
|||
CoreNavigator.navigateToSitePath('viewer/iframe', { params: { title: item.label, url: item.url } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Open preferences.
|
||||
*/
|
||||
openPreferences(): void {
|
||||
CoreNavigator.navigateToSitePath('preferences');
|
||||
}
|
||||
|
||||
/**
|
||||
* Open settings.
|
||||
*/
|
||||
|
@ -200,12 +168,4 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout the user.
|
||||
*/
|
||||
logout(): void {
|
||||
this.loggedOut = true;
|
||||
CoreSites.logout();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,18 +15,6 @@
|
|||
</ion-refresher>
|
||||
<core-loading [hideUntil]="handlers.loaded">
|
||||
<ion-list>
|
||||
<ion-item *ngIf="siteInfo" class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<p class="item-heading">{{siteInfo!.fullname}}</p>
|
||||
<p>
|
||||
<core-format-text [text]="siteName" contextLevel="system" [contextInstanceId]="0"
|
||||
[wsNotFiltered]="true"></core-format-text>
|
||||
</p>
|
||||
<p>{{ siteUrl }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<core-spacer></core-spacer>
|
||||
|
||||
<ion-item *ngFor="let handler of handlers.items" [ngClass]="['core-settings-handler', handler.class]"
|
||||
[attr.aria-label]="handler.title | translate" detail="true" (click)="handlers.select(handler)" button
|
||||
[attr.aria-current]="handlers.getItemAriaCurrent(handler)">
|
||||
|
@ -36,38 +24,37 @@
|
|||
<p class="item-heading">{{ handler.title | translate}}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-card>
|
||||
<ion-item class="ion-text-wrap" *ngIf="spaceUsage">
|
||||
<ion-label>
|
||||
<p class="item-heading ion-text-wrap">{{ 'core.settings.spaceusage' | translate }}</p>
|
||||
<p *ngIf="spaceUsage.spaceUsage">{{ spaceUsage.spaceUsage | coreBytesToSize }}</p>
|
||||
</ion-label>
|
||||
<ion-button fill="clear" [attr.aria-label]="'core.info' | translate" (click)="showSpaceInfo()" slot="end">
|
||||
<ion-icon name="fas-info-circle" color="info" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button fill="clear" color="danger" slot="end" (click)="deleteSiteStorage()"
|
||||
[hidden]="spaceUsage.spaceUsage! + spaceUsage.cacheEntries! <= 0"
|
||||
[attr.aria-label]="'core.settings.deletesitefilestitle' | translate">
|
||||
<ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.settings.synchronizenow' | translate }}</p>
|
||||
</ion-label>
|
||||
<ion-button fill="clear" [attr.aria-label]="'core.info' | translate" (click)="showSyncInfo()" slot="end">
|
||||
<ion-icon name="fas-info-circle" color="info" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
<core-button-with-spinner [loading]="isSynchronizing()" slot="end">
|
||||
<ion-button fill="clear" (click)="synchronize()"
|
||||
[attr.aria-label]="'core.settings.synchronizenow' | translate">
|
||||
<ion-icon name="fas-sync-alt" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</core-button-with-spinner>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
</ion-list>
|
||||
<ion-card>
|
||||
<ion-item class="ion-text-wrap" *ngIf="spaceUsage">
|
||||
<ion-label>
|
||||
<p class="item-heading ion-text-wrap">{{ 'core.settings.spaceusage' | translate }}</p>
|
||||
<p *ngIf="spaceUsage.spaceUsage">{{ spaceUsage.spaceUsage | coreBytesToSize }}</p>
|
||||
</ion-label>
|
||||
<ion-button fill="clear" [attr.aria-label]="'core.info' | translate" (click)="showSpaceInfo()" slot="end">
|
||||
<ion-icon name="fas-info-circle" color="info" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button fill="clear" color="danger" slot="end" (click)="deleteSiteStorage()"
|
||||
[hidden]="spaceUsage.spaceUsage! + spaceUsage.cacheEntries! <= 0"
|
||||
[attr.aria-label]="'core.settings.deletesitefilestitle' | translate">
|
||||
<ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.settings.synchronizenow' | translate }}</p>
|
||||
</ion-label>
|
||||
<ion-button fill="clear" [attr.aria-label]="'core.info' | translate" (click)="showSyncInfo()" slot="end">
|
||||
<ion-icon name="fas-info-circle" color="info" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
<core-button-with-spinner [loading]="isSynchronizing()" slot="end">
|
||||
<ion-button fill="clear" (click)="synchronize()"
|
||||
[attr.aria-label]="'core.settings.synchronizenow' | translate">
|
||||
<ion-icon name="fas-sync-alt" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</core-button-with-spinner>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
</core-loading>
|
||||
</core-split-view>
|
||||
</ion-content>
|
||||
|
|
|
@ -22,7 +22,6 @@ import { CoreSites } from '@services/sites';
|
|||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreSettingsHelper, CoreSiteSpaceUsage } from '../../services/settings-helper';
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreSiteInfo } from '@classes/site';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
|
||||
|
@ -43,9 +42,6 @@ export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy {
|
|||
|
||||
isIOS: boolean;
|
||||
siteId: string;
|
||||
siteInfo?: CoreSiteInfo;
|
||||
siteName?: string;
|
||||
siteUrl?: string;
|
||||
spaceUsage: CoreSiteSpaceUsage = {
|
||||
cacheEntries: 0,
|
||||
spaceUsage: 0,
|
||||
|
@ -60,11 +56,9 @@ export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy {
|
|||
this.siteId = CoreSites.getCurrentSiteId();
|
||||
this.handlers = new CoreSettingsSitePreferencesManager(CoreSitePreferencesPage);
|
||||
|
||||
this.sitesObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, (data) => {
|
||||
if (data.siteId == this.siteId) {
|
||||
this.refreshData();
|
||||
}
|
||||
});
|
||||
this.sitesObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
||||
this.refreshData();
|
||||
}, this.siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -94,11 +88,6 @@ export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy {
|
|||
protected async fetchData(): Promise<void> {
|
||||
this.handlers.setItems(CoreSettingsDelegate.getHandlers());
|
||||
|
||||
const currentSite = CoreSites.getCurrentSite();
|
||||
this.siteInfo = currentSite!.getInfo();
|
||||
this.siteName = currentSite!.getSiteName();
|
||||
this.siteUrl = currentSite!.getURL();
|
||||
|
||||
this.spaceUsage = await CoreSettingsHelper.getSiteSpaceUsage(this.siteId);
|
||||
}
|
||||
|
||||
|
@ -145,7 +134,9 @@ export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy {
|
|||
*/
|
||||
async deleteSiteStorage(): Promise<void> {
|
||||
try {
|
||||
this.spaceUsage = await CoreSettingsHelper.deleteSiteStorage(this.siteName || '', this.siteId);
|
||||
const siteName = CoreSites.getRequiredCurrentSite().getSiteName();
|
||||
|
||||
this.spaceUsage = await CoreSettingsHelper.deleteSiteStorage(siteName, this.siteId);
|
||||
} catch {
|
||||
// Ignore cancelled confirmation modal.
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<core-format-text [text]="site.siteName" clean="true" [siteId]="site.id"></core-format-text>
|
||||
</p>
|
||||
<p class="ion-text-wrap">{{ site.fullName }}</p>
|
||||
<p>{{ site.siteUrl }}</p>
|
||||
<p>{{ site.siteUrlWithoutProtocol }}</p>
|
||||
</ion-label>
|
||||
<p *ngIf="site.spaceUsage !== undefined" slot="end">
|
||||
{{ site.spaceUsage | coreBytesToSize }}
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
<core-format-text [text]="site.siteName" clean="true" [siteId]="site.id"></core-format-text>
|
||||
</p>
|
||||
<p>{{ site.fullName }}</p>
|
||||
<p>{{ site.siteUrl }}</p>
|
||||
<p>{{ site.siteUrlWithoutProtocol }}</p>
|
||||
</ion-label>
|
||||
<core-button-with-spinner [loading]="isSynchronizing(site.id)" slot="end">
|
||||
<ion-button fill="clear" (click)="synchronize(site.id)" [title]="site.siteName"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<core-navbar-buttons slot="end">
|
||||
<core-navbar-buttons slot="end" prepend>
|
||||
<ion-button *ngIf="searchEnabled" (click)="openSearch()" [attr.aria-label]="'core.courses.searchcourses' | translate">
|
||||
<ion-icon name="fas-search" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<h1>{{ 'core.tag.searchtags' | translate }}</h1>
|
||||
<ion-buttons slot="end">
|
||||
<core-user-menu-button></core-user-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
|
|
|
@ -19,6 +19,7 @@ import { CoreSharedModule } from '@/core/shared.module';
|
|||
import { CoreSearchComponentsModule } from '@features/search/components/components.module';
|
||||
|
||||
import { CoreTagSearchPage } from './search.page';
|
||||
import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
|
@ -34,6 +35,7 @@ const routes: Routes = [
|
|||
RouterModule.forChild(routes),
|
||||
CoreSharedModule,
|
||||
CoreSearchComponentsModule,
|
||||
CoreMainMenuComponentsModule,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
|
|
|
@ -35,7 +35,7 @@ function buildRoutes(injector: Injector): Routes {
|
|||
data: {
|
||||
mainMenuTabRoot: CoreTagMainMenuHandlerService.PAGE_NAME,
|
||||
},
|
||||
loadChildren: () => import('@features/tag//pages/search/search.page.module').then(m => m.CoreTagSearchPageModule),
|
||||
loadChildren: () => import('@features/tag/pages/search/search.page.module').then(m => m.CoreTagSearchPageModule),
|
||||
},
|
||||
CoreTagIndexAreaRoute,
|
||||
...buildTabMainRoutes(injector, {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"address": "Address",
|
||||
"account": "Account",
|
||||
"city": "City/town",
|
||||
"contact": "Contact",
|
||||
"country": "Country",
|
||||
|
@ -19,6 +20,7 @@
|
|||
"participants": "Participants",
|
||||
"phone1": "Phone",
|
||||
"phone2": "Mobile phone",
|
||||
"profile": "Profile",
|
||||
"roles": "Roles",
|
||||
"sendemail": "Email",
|
||||
"student": "Student",
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<ion-buttons slot="start">
|
||||
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<h1 *ngIf="title">{{ title }}</h1>
|
||||
<h1>{{ 'core.user.profile' | translate }}</h1>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
|
@ -12,39 +12,57 @@
|
|||
</ion-refresher>
|
||||
<core-loading [hideUntil]="userLoaded">
|
||||
<ion-list *ngIf="user">
|
||||
<ion-item class="ion-text-center core-user-profile-maininfo" lines="full">
|
||||
<core-user-avatar [user]="user" [userId]="user.id" [linkProfile]="false" [checkOnline]="true">
|
||||
<ion-button class="edit-avatar" *ngIf="canChangeProfilePicture" (click)="changeProfilePicture()"
|
||||
[attr.aria-label]="'core.user.newpicture' | translate" fill="clear" color="dark">
|
||||
<ion-icon slot="icon-only" name="fas-pen" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</core-user-avatar>
|
||||
<ion-label>
|
||||
<h2>{{ user.fullname }}</h2>
|
||||
<p *ngIf="user.address">
|
||||
<ion-icon name="fas-map-marker-alt" [attr.aria-hidden]="true"></ion-icon> {{ user.address }}
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-group *ngIf="hasContact">
|
||||
<ion-item-divider><ion-label><h2>{{ 'core.user.contact' | translate}}</h2></ion-label></ion-item-divider>
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<h2>{{ 'core.user.contact' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap" *ngIf="user.email">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.user.email' | translate }}</h2>
|
||||
<p><a class="core-anchor" href="mailto:{{user.email}}" core-link auto-login="no"
|
||||
[showBrowserWarning]="false">
|
||||
{{ user.email }}
|
||||
</a></p>
|
||||
<p><a class="core-anchor" href="mailto:{{user.email}}" core-link auto-login="no" [showBrowserWarning]="false">
|
||||
{{ user.email }}
|
||||
</a></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="user.phone1">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.user.phone1' | translate}}</h2>
|
||||
<p><a class="core-anchor" href="tel:{{user.phone1}}" core-link auto-login="no" [showBrowserWarning]="false">
|
||||
{{ user.phone1 }}
|
||||
</a></p>
|
||||
{{ user.phone1 }}
|
||||
</a></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="user.phone2">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.user.phone2' | translate}}</h2>
|
||||
<p><a class="core-anchor" href="tel:{{user.phone2}}" core-link auto-login="no" [showBrowserWarning]="false">
|
||||
{{ user.phone2 }}
|
||||
</a></p>
|
||||
{{ user.phone2 }}
|
||||
</a></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="formattedAddress">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.user.address' | translate}}</h2>
|
||||
<p><a class="core-anchor" [href]="encodedAddress" core-link auto-login="no" [showBrowserWarning]="false">
|
||||
{{ formattedAddress }}
|
||||
</a></p>
|
||||
{{ formattedAddress }}
|
||||
</a></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="user.city && !formattedAddress">
|
||||
|
@ -61,13 +79,17 @@
|
|||
</ion-item>
|
||||
</ion-item-group>
|
||||
<ion-item-group *ngIf="hasDetails">
|
||||
<ion-item-divider><ion-label><h2>{{ 'core.userdetails' | translate}}</h2></ion-label></ion-item-divider>
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<h2>{{ 'core.userdetails' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap" *ngIf="user.url">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.user.webpage' | translate}}</h2>
|
||||
<p><a class="core-anchor" href="{{user.url}}" core-link>
|
||||
{{ user.url }}
|
||||
</a></p>
|
||||
{{ user.url }}
|
||||
</a></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="user.interests">
|
||||
|
@ -81,11 +103,17 @@
|
|||
</core-user-profile-field>
|
||||
</ion-item-group>
|
||||
<ion-item-group *ngIf="user.description">
|
||||
<ion-item-divider><ion-label><h2>{{ 'core.user.description' | translate}}</h2></ion-label></ion-item-divider>
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<h2>{{ 'core.user.description' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<p><core-format-text [text]="user.description" contextLevel="user" [contextInstanceId]="user.id">
|
||||
</core-format-text></p>
|
||||
<p>
|
||||
<core-format-text [text]="user.description" contextLevel="user" [contextInstanceId]="user.id">
|
||||
</core-format-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { SafeUrl } from '@angular/platform-browser';
|
||||
import { IonRefresher } from '@ionic/angular';
|
||||
|
||||
|
@ -20,10 +20,15 @@ import { CoreSites } from '@services/sites';
|
|||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreUser, CoreUserProfile, CoreUserProvider } from '@features/user/services/user';
|
||||
import { CoreUserHelper } from '@features/user/services/user-helper';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreIonLoadingElement } from '@classes/ion-loading';
|
||||
import { CoreSite } from '@classes/site';
|
||||
import { CoreFileUploaderHelper } from '@features/fileuploader/services/fileuploader-helper';
|
||||
import { CoreMimetypeUtils } from '@services/utils/mimetype';
|
||||
import { Translate } from '@singletons';
|
||||
|
||||
/**
|
||||
* Page that displays info about a user.
|
||||
|
@ -31,11 +36,9 @@ import { CoreNavigator } from '@services/navigator';
|
|||
@Component({
|
||||
selector: 'page-core-user-about',
|
||||
templateUrl: 'about.html',
|
||||
styleUrls: ['about.scss'],
|
||||
})
|
||||
export class CoreUserAboutPage implements OnInit {
|
||||
|
||||
protected userId!: number;
|
||||
protected siteId: string;
|
||||
export class CoreUserAboutPage implements OnInit, OnDestroy {
|
||||
|
||||
courseId!: number;
|
||||
userLoaded = false;
|
||||
|
@ -45,20 +48,46 @@ export class CoreUserAboutPage implements OnInit {
|
|||
title?: string;
|
||||
formattedAddress?: string;
|
||||
encodedAddress?: SafeUrl;
|
||||
canChangeProfilePicture = false;
|
||||
|
||||
protected userId!: number;
|
||||
protected site!: CoreSite;
|
||||
protected obsProfileRefreshed?: CoreEventObserver;
|
||||
|
||||
constructor() {
|
||||
this.siteId = CoreSites.getCurrentSiteId();
|
||||
try {
|
||||
this.site = CoreSites.getRequiredCurrentSite();
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModal(error);
|
||||
CoreNavigator.back();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.obsProfileRefreshed = CoreEvents.on(CoreUserProvider.PROFILE_REFRESHED, (data) => {
|
||||
if (!this.user || !data.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.user.email = data.user.email;
|
||||
this.user.address = CoreUserHelper.formatAddress('', data.user.city, data.user.country);
|
||||
}, CoreSites.getCurrentSiteId());
|
||||
}
|
||||
|
||||
/**
|
||||
* On init.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.userId = CoreNavigator.getRouteNumberParam('userId') || 0;
|
||||
this.courseId = CoreNavigator.getRouteNumberParam('courseId') || 0;
|
||||
|
||||
// Allow to change the profile image only in the app profile page.
|
||||
this.canChangeProfilePicture =
|
||||
!this.courseId &&
|
||||
this.userId == this.site.getUserId() &&
|
||||
this.site.canUploadFiles() &&
|
||||
!CoreUser.isUpdatePictureDisabledInSite(this.site);
|
||||
|
||||
this.fetchUser().finally(() => {
|
||||
this.userLoaded = true;
|
||||
});
|
||||
|
@ -83,11 +112,85 @@ export class CoreUserAboutPage implements OnInit {
|
|||
|
||||
this.user = user;
|
||||
this.title = user.fullname;
|
||||
|
||||
this.user.address = CoreUserHelper.formatAddress('', user.city, user.country);
|
||||
|
||||
await this.checkUserImageUpdated();
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'core.user.errorloaduser', true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current user image has changed.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async checkUserImageUpdated(): Promise<void> {
|
||||
if (!this.site || !this.site.getInfo() || !this.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.userId != this.site.getUserId() || !this.isUserAvatarDirty()) {
|
||||
// Not current user or hasn't changed.
|
||||
return;
|
||||
}
|
||||
|
||||
// The current user image received is different than the one stored in site info. Assume the image was updated.
|
||||
// Update the site info to get the right avatar in there.
|
||||
try {
|
||||
await CoreSites.updateSiteInfo(this.site.getId());
|
||||
} catch {
|
||||
// Cannot update site info. Assume the profile image is the right one.
|
||||
CoreEvents.trigger(CoreUserProvider.PROFILE_PICTURE_UPDATED, {
|
||||
userId: this.userId,
|
||||
picture: this.user.profileimageurl,
|
||||
}, this.site.getId());
|
||||
}
|
||||
|
||||
if (this.isUserAvatarDirty()) {
|
||||
// The image is still different, this means that the good one is the one in site info.
|
||||
await this.refreshUser();
|
||||
} else {
|
||||
// Now they're the same, send event to use the right avatar in the rest of the app.
|
||||
CoreEvents.trigger(CoreUserProvider.PROFILE_PICTURE_UPDATED, {
|
||||
userId: this.userId,
|
||||
picture: this.user.profileimageurl,
|
||||
}, this.site.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens dialog to change profile picture.
|
||||
*/
|
||||
async changeProfilePicture(): Promise<void> {
|
||||
const maxSize = -1;
|
||||
const title = Translate.instant('core.user.newpicture');
|
||||
const mimetypes = CoreMimetypeUtils.getGroupMimeInfo('image', 'mimetypes');
|
||||
let modal: CoreIonLoadingElement | undefined;
|
||||
|
||||
try {
|
||||
const result = await CoreFileUploaderHelper.selectAndUploadFile(maxSize, title, mimetypes);
|
||||
|
||||
modal = await CoreDomUtils.showModalLoading('core.sending', true);
|
||||
|
||||
const profileImageURL = await CoreUser.changeProfilePicture(result.itemid, this.userId, this.site.getId());
|
||||
|
||||
CoreEvents.trigger(CoreUserProvider.PROFILE_PICTURE_UPDATED, {
|
||||
userId: this.userId,
|
||||
picture: profileImageURL,
|
||||
}, this.site.getId());
|
||||
|
||||
CoreSites.updateSiteInfo(this.site.getId());
|
||||
|
||||
this.refreshUser();
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModal(error);
|
||||
} finally {
|
||||
modal?.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the user data.
|
||||
*
|
||||
|
@ -106,8 +209,52 @@ export class CoreUserAboutPage implements OnInit {
|
|||
courseId: this.courseId,
|
||||
userId: this.userId,
|
||||
user: this.user,
|
||||
}, this.siteId);
|
||||
}, this.site.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the user avatar is not up to date with site info.
|
||||
*
|
||||
* @return Whether the user avatar differs from site info cache.
|
||||
*/
|
||||
protected isUserAvatarDirty(): boolean {
|
||||
if (!this.user || !this.site) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const courseAvatarUrl = this.normalizeAvatarUrl(this.user.profileimageurl);
|
||||
const siteAvatarUrl = this.normalizeAvatarUrl(this.site.getInfo()?.userpictureurl);
|
||||
|
||||
return courseAvatarUrl !== siteAvatarUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize an avatar url regardless of theme.
|
||||
*
|
||||
* Given that the default image is the only one that can be changed per theme, any other url will stay the same. Note that
|
||||
* the values returned by this function may not be valid urls, given that they are intended for string comparison.
|
||||
*
|
||||
* @param avatarUrl Avatar url.
|
||||
* @return Normalized avatar string (may not be a valid url).
|
||||
*/
|
||||
protected normalizeAvatarUrl(avatarUrl?: string): string {
|
||||
if (!avatarUrl) {
|
||||
return 'undefined';
|
||||
}
|
||||
|
||||
if (avatarUrl.startsWith(`${this.site?.siteUrl}/theme/image.php`)) {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.obsProfileRefreshed?.off();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
:host {
|
||||
|
||||
.core-user-profile-maininfo::part(native) {
|
||||
flex-direction: column;
|
||||
}
|
||||
::ng-deep {
|
||||
core-user-avatar {
|
||||
display: block;
|
||||
--core-avatar-size: var(--core-large-avatar-size);
|
||||
height: calc(var(--core-avatar-size) + 16px);
|
||||
|
||||
img {
|
||||
margin: 8px auto;
|
||||
}
|
||||
|
||||
.contact-status {
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
right: calc(50% - 12px - var(--core-avatar-size) / 2) !important;
|
||||
}
|
||||
|
||||
.edit-avatar {
|
||||
position: absolute;
|
||||
right: calc(50% - 15px - var(--core-avatar-size) / 2);
|
||||
bottom: -12px;
|
||||
|
||||
:host-context([dir="rtl"]) & {
|
||||
left: 0;
|
||||
right: unset;
|
||||
}
|
||||
&::part(native) {
|
||||
border-radius: 50%;
|
||||
background: var(--ion-item-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
:host-context([dir="rtl"]) ::ng-deep core-user-avatar .edit-avatar {
|
||||
left: -24px;
|
||||
right: unset;
|
||||
}
|
|
@ -14,20 +14,12 @@
|
|||
<ion-list *ngIf="user && !isDeleted && isEnrolled">
|
||||
<ion-item class="ion-text-center core-user-profile-maininfo">
|
||||
<core-user-avatar [user]="user" [userId]="user.id" [linkProfile]="false" [checkOnline]="true">
|
||||
<ion-button
|
||||
class="edit-avatar"
|
||||
*ngIf="canChangeProfilePicture"
|
||||
(click)="changeProfilePicture()"
|
||||
[attr.aria-label]="'core.user.newpicture' | translate"
|
||||
fill="clear"
|
||||
color="dark"
|
||||
>
|
||||
<ion-icon slot="icon-only" name="fas-pen" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</core-user-avatar>
|
||||
<ion-label>
|
||||
<h2>{{ user.fullname }}</h2>
|
||||
<p *ngIf="user.address">{{ user.address }}</p>
|
||||
<p *ngIf="user.address">
|
||||
<ion-icon name="fas-map-marker-alt" [attr.aria-hidden]="true"></ion-icon> {{ user.address }}
|
||||
</p>
|
||||
<p *ngIf="rolesFormatted" class="ion-text-wrap">
|
||||
<strong>{{ 'core.user.roles' | translate}}</strong>{{'core.labelsep' | translate}}
|
||||
{{ rolesFormatted }}
|
||||
|
@ -61,7 +53,9 @@
|
|||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-center core-loading-handlers" *ngIf="isLoadingHandlers">
|
||||
<ion-label><ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner></ion-label>
|
||||
<ion-label>
|
||||
<ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item button *ngFor="let handler of newPageHandlers" class="ion-text-wrap" (click)="handlerClicked($event, handler)"
|
||||
|
@ -71,6 +65,14 @@
|
|||
<ion-label>
|
||||
<p class="item-heading">{{ handler.title | translate }}</p>
|
||||
</ion-label>
|
||||
<ion-badge slot="end" *ngIf="handler.showBadge" [hidden]="handler.loading || !handler.badge" aria-hidden="true">
|
||||
{{handler.badge}}
|
||||
</ion-badge>
|
||||
<span *ngIf="handler.showBadge && handler.badge && handler.badgeA11yText" class="sr-only">
|
||||
{{ handler.badgeA11yText | translate: {$a : handler.badge } }}
|
||||
</span>
|
||||
<ion-spinner slot="end" *ngIf="handler.showBadge && handler.loading" [attr.aria-label]="'core.loading' | translate">
|
||||
</ion-spinner>
|
||||
</ion-item>
|
||||
|
||||
<ion-item *ngIf="actionHandlers && actionHandlers.length">
|
||||
|
@ -86,8 +88,7 @@
|
|||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<core-empty-box *ngIf="!user && !isDeleted && isEnrolled" icon="far-user"
|
||||
[message]=" 'core.user.detailsnotavailable' | translate">
|
||||
<core-empty-box *ngIf="!user && !isDeleted && isEnrolled" icon="far-user" [message]=" 'core.user.detailsnotavailable' | translate">
|
||||
</core-empty-box>
|
||||
<core-empty-box *ngIf="isDeleted" icon="far-user" [message]="'core.userdeleted' | translate"></core-empty-box>
|
||||
<core-empty-box *ngIf="!isEnrolled" icon="far-user" [message]="'core.notenrolledprofile' | translate"></core-empty-box>
|
||||
|
|
|
@ -19,8 +19,6 @@ import { Subscription } from 'rxjs';
|
|||
import { CoreSite } from '@classes/site';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreMimetypeUtils } from '@services/utils/mimetype';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import {
|
||||
CoreUser,
|
||||
|
@ -29,8 +27,6 @@ import {
|
|||
} from '@features/user/services/user';
|
||||
import { CoreUserHelper } from '@features/user/services/user-helper';
|
||||
import { CoreUserDelegate, CoreUserDelegateService, CoreUserProfileHandlerData } from '@features/user/services/user-delegate';
|
||||
import { CoreFileUploaderHelper } from '@features/fileuploader/services/fileuploader-helper';
|
||||
import { CoreIonLoadingElement } from '@classes/ion-loading';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreCourses } from '@features/courses/services/courses';
|
||||
|
@ -54,7 +50,6 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
|
|||
title?: string;
|
||||
isDeleted = false;
|
||||
isEnrolled = true;
|
||||
canChangeProfilePicture = false;
|
||||
rolesFormatted?: string;
|
||||
actionHandlers: CoreUserProfileHandlerData[] = [];
|
||||
newPageHandlers: CoreUserProfileHandlerData[] = [];
|
||||
|
@ -72,7 +67,7 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
/**
|
||||
* On init.
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
try {
|
||||
|
@ -91,13 +86,6 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
|
|||
this.courseId = undefined;
|
||||
}
|
||||
|
||||
// Allow to change the profile image only in the app profile page.
|
||||
this.canChangeProfilePicture =
|
||||
!this.courseId &&
|
||||
this.userId == this.site.getUserId() &&
|
||||
this.site.canUploadFiles() &&
|
||||
!CoreUser.isUpdatePictureDisabledInSite(this.site);
|
||||
|
||||
try {
|
||||
await this.fetchUser();
|
||||
|
||||
|
@ -154,84 +142,12 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
|
|||
this.isLoadingHandlers = !CoreUserDelegate.areHandlersLoaded(user.id);
|
||||
});
|
||||
|
||||
await this.checkUserImageUpdated();
|
||||
|
||||
} catch (error) {
|
||||
// Error is null for deleted users, do not show the modal.
|
||||
CoreDomUtils.showErrorModal(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current user image has changed.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async checkUserImageUpdated(): Promise<void> {
|
||||
if (!this.site || !this.site.getInfo() || !this.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.userId != this.site.getUserId() || !this.isUserAvatarDirty()) {
|
||||
// Not current user or hasn't changed.
|
||||
return;
|
||||
}
|
||||
|
||||
// The current user image received is different than the one stored in site info. Assume the image was updated.
|
||||
// Update the site info to get the right avatar in there.
|
||||
try {
|
||||
await CoreSites.updateSiteInfo(this.site.getId());
|
||||
} catch {
|
||||
// Cannot update site info. Assume the profile image is the right one.
|
||||
CoreEvents.trigger(CoreUserProvider.PROFILE_PICTURE_UPDATED, {
|
||||
userId: this.userId,
|
||||
picture: this.user.profileimageurl,
|
||||
}, this.site.getId());
|
||||
}
|
||||
|
||||
if (this.isUserAvatarDirty()) {
|
||||
// The image is still different, this means that the good one is the one in site info.
|
||||
await this.refreshUser();
|
||||
} else {
|
||||
// Now they're the same, send event to use the right avatar in the rest of the app.
|
||||
CoreEvents.trigger(CoreUserProvider.PROFILE_PICTURE_UPDATED, {
|
||||
userId: this.userId,
|
||||
picture: this.user.profileimageurl,
|
||||
}, this.site.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens dialog to change profile picture.
|
||||
*/
|
||||
async changeProfilePicture(): Promise<void> {
|
||||
const maxSize = -1;
|
||||
const title = Translate.instant('core.user.newpicture');
|
||||
const mimetypes = CoreMimetypeUtils.getGroupMimeInfo('image', 'mimetypes');
|
||||
let modal: CoreIonLoadingElement | undefined;
|
||||
|
||||
try {
|
||||
const result = await CoreFileUploaderHelper.selectAndUploadFile(maxSize, title, mimetypes);
|
||||
|
||||
modal = await CoreDomUtils.showModalLoading('core.sending', true);
|
||||
|
||||
const profileImageURL = await CoreUser.changeProfilePicture(result.itemid, this.userId, this.site.getId());
|
||||
|
||||
CoreEvents.trigger(CoreUserProvider.PROFILE_PICTURE_UPDATED, {
|
||||
userId: this.userId,
|
||||
picture: profileImageURL,
|
||||
}, this.site.getId());
|
||||
|
||||
CoreSites.updateSiteInfo(this.site.getId());
|
||||
|
||||
this.refreshUser();
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModal(error);
|
||||
} finally {
|
||||
modal?.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the user.
|
||||
*
|
||||
|
@ -285,48 +201,11 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
/**
|
||||
* Page destroyed.
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.subscription?.unsubscribe();
|
||||
this.obsProfileRefreshed.off();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the user avatar is not up to date with site info.
|
||||
*
|
||||
* @return Whether the user avatar differs from site info cache.
|
||||
*/
|
||||
private isUserAvatarDirty(): boolean {
|
||||
if (!this.user || !this.site) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const courseAvatarUrl = this.normalizeAvatarUrl(this.user.profileimageurl);
|
||||
const siteAvatarUrl = this.normalizeAvatarUrl(this.site.getInfo()?.userpictureurl);
|
||||
|
||||
return courseAvatarUrl !== siteAvatarUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize an avatar url regardless of theme.
|
||||
*
|
||||
* Given that the default image is the only one that can be changed per theme, any other url will stay the same. Note that
|
||||
* the values returned by this function may not be valid urls, given that they are intended for string comparison.
|
||||
*
|
||||
* @param avatarUrl Avatar url.
|
||||
* @return Normalized avatar string (may not be a valid url).
|
||||
*/
|
||||
private normalizeAvatarUrl(avatarUrl?: string): string {
|
||||
if (!avatarUrl) {
|
||||
return 'undefined';
|
||||
}
|
||||
|
||||
if (avatarUrl.startsWith(`${this.site?.siteUrl}/theme/image.php`)) {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,21 +18,6 @@
|
|||
height: 24px !important;
|
||||
right: calc(50% - 12px - var(--core-avatar-size) / 2) !important;
|
||||
}
|
||||
|
||||
.edit-avatar {
|
||||
position: absolute;
|
||||
right: calc(50% - 15px - var(--core-avatar-size) / 2);
|
||||
bottom: -12px;
|
||||
|
||||
:host-context([dir="rtl"]) & {
|
||||
left: 0;
|
||||
right: unset;
|
||||
}
|
||||
&::part(native) {
|
||||
border-radius: 50%;
|
||||
background: var(--ion-item-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -111,6 +111,26 @@ export interface CoreUserProfileHandlerData {
|
|||
*/
|
||||
spinner?: boolean;
|
||||
|
||||
/**
|
||||
* If the handler has badge to show or not. Only for TYPE_NEW_PAGE.
|
||||
*/
|
||||
showBadge?: boolean;
|
||||
|
||||
/**
|
||||
* Text to display on the badge. Only used if showBadge is true and only for TYPE_NEW_PAGE.
|
||||
*/
|
||||
badge?: string;
|
||||
|
||||
/**
|
||||
* Accessibility text to add on the badge. Only used if showBadge is true and only for TYPE_NEW_PAGE.
|
||||
*/
|
||||
badgeA11yText?: string;
|
||||
|
||||
/**
|
||||
* If true, the badge number is being loaded. Only used if showBadge is true and only for TYPE_NEW_PAGE.
|
||||
*/
|
||||
loading?: boolean;
|
||||
|
||||
/**
|
||||
* Action to do when clicked.
|
||||
*
|
||||
|
|
|
@ -185,7 +185,7 @@
|
|||
"mod_wiki": "Wiki",
|
||||
"mod_workshop": "Workshop",
|
||||
"moduleintro": "Description",
|
||||
"more": "more",
|
||||
"more": "More",
|
||||
"mygroups": "My groups",
|
||||
"name": "Name",
|
||||
"needhelp": "Need help?",
|
||||
|
|
|
@ -606,7 +606,7 @@ export class CoreAppProvider {
|
|||
};
|
||||
|
||||
localStorage.setItem('CoreRedirect', JSON.stringify(redirect));
|
||||
} catch (ex) {
|
||||
} catch {
|
||||
// Ignore errors.
|
||||
}
|
||||
}
|
||||
|
|
|
@ -428,7 +428,7 @@ export class CoreSitesProvider {
|
|||
|
||||
const result = this.isValidMoodleVersion(info);
|
||||
if (result != CoreSitesProvider.VALID_VERSION) {
|
||||
return this.treatInvalidAppVersion(result, siteUrl);
|
||||
return this.treatInvalidAppVersion(result);
|
||||
}
|
||||
|
||||
const siteId = this.createSiteID(info.siteurl, info.username);
|
||||
|
@ -492,7 +492,7 @@ export class CoreSitesProvider {
|
|||
} catch (error) {
|
||||
// Error invaliddevice is returned by Workplace server meaning the same as connecttoworkplaceapp.
|
||||
if (error && error.errorcode == 'invaliddevice') {
|
||||
return this.treatInvalidAppVersion(CoreSitesProvider.WORKPLACE_APP, siteUrl);
|
||||
return this.treatInvalidAppVersion(CoreSitesProvider.WORKPLACE_APP);
|
||||
}
|
||||
|
||||
throw error;
|
||||
|
@ -503,14 +503,13 @@ export class CoreSitesProvider {
|
|||
* Having the result of isValidMoodleVersion, it treats the error message to be shown.
|
||||
*
|
||||
* @param result Result returned by isValidMoodleVersion function.
|
||||
* @param siteUrl The site url.
|
||||
* @param siteId If site is already added, it will invalidate the token.
|
||||
* @return A promise rejected with the error info.
|
||||
*/
|
||||
protected async treatInvalidAppVersion(result: number, siteUrl: string, siteId?: string): Promise<never> {
|
||||
protected async treatInvalidAppVersion(result: number, siteId?: string): Promise<never> {
|
||||
let errorCode: string | undefined;
|
||||
let errorKey: string | undefined;
|
||||
let translateParams;
|
||||
let translateParams = {};
|
||||
|
||||
switch (result) {
|
||||
case CoreSitesProvider.MOODLE_APP:
|
||||
|
@ -528,7 +527,7 @@ export class CoreSitesProvider {
|
|||
}
|
||||
|
||||
if (siteId) {
|
||||
await this.setSiteLoggedOut(siteId, true);
|
||||
await this.setSiteLoggedOut(siteId);
|
||||
}
|
||||
|
||||
throw new CoreSiteError({
|
||||
|
@ -746,7 +745,7 @@ export class CoreSitesProvider {
|
|||
if (siteId) {
|
||||
// Logout the currentSite and expire the token.
|
||||
this.logout();
|
||||
this.setSiteLoggedOut(siteId, true);
|
||||
this.setSiteLoggedOut(siteId);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1053,8 +1052,10 @@ export class CoreSitesProvider {
|
|||
* @param siteId The site ID. If not defined, current site (if available).
|
||||
* @return Promise resolved with site home ID.
|
||||
*/
|
||||
getSiteHomeId(siteId?: string): Promise<number> {
|
||||
return this.getSite(siteId).then((site) => site.getSiteHomeId());
|
||||
async getSiteHomeId(siteId?: string): Promise<number> {
|
||||
const site = await this.getSite(siteId);
|
||||
|
||||
return site.getSiteHomeId();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1075,6 +1076,7 @@ export class CoreSitesProvider {
|
|||
const basicInfo: CoreSiteBasicInfo = {
|
||||
id: site.id,
|
||||
siteUrl: site.siteUrl,
|
||||
siteUrlWithoutProtocol: site.siteUrl.replace(/^https?:\/\//, '').toLowerCase(),
|
||||
fullName: siteInfo?.fullname,
|
||||
siteName: CoreConstants.CONFIG.sitename == '' ? siteInfo?.sitename: CoreConstants.CONFIG.sitename,
|
||||
avatar: siteInfo?.userpictureurl,
|
||||
|
@ -1096,12 +1098,10 @@ export class CoreSitesProvider {
|
|||
async getSortedSites(ids?: string[]): Promise<CoreSiteBasicInfo[]> {
|
||||
const sites = await this.getSites(ids);
|
||||
|
||||
// Sort sites by url and ful lname.
|
||||
// Sort sites by url and fullname.
|
||||
sites.sort((a, b) => {
|
||||
// First compare by site url without the protocol.
|
||||
const urlA = a.siteUrl.replace(/^https?:\/\//, '').toLowerCase();
|
||||
const urlB = b.siteUrl.replace(/^https?:\/\//, '').toLowerCase();
|
||||
const compare = urlA.localeCompare(urlB);
|
||||
const compare = a.siteUrlWithoutProtocol.localeCompare(b.siteUrlWithoutProtocol);
|
||||
|
||||
if (compare !== 0) {
|
||||
return compare;
|
||||
|
@ -1191,7 +1191,7 @@ export class CoreSitesProvider {
|
|||
this.currentSite = undefined;
|
||||
|
||||
if (siteConfig && siteConfig.tool_mobile_forcelogout == '1') {
|
||||
promises.push(this.setSiteLoggedOut(siteId, true));
|
||||
promises.push(this.setSiteLoggedOut(siteId));
|
||||
}
|
||||
|
||||
promises.push(this.removeStoredCurrentSite());
|
||||
|
@ -1221,34 +1221,24 @@ export class CoreSitesProvider {
|
|||
this.logger.debug(`Restore session in site ${siteId}`);
|
||||
|
||||
await this.loadSite(siteId);
|
||||
} catch (err) {
|
||||
} catch {
|
||||
// No current session.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark or unmark a site as logged out so the user needs to authenticate again.
|
||||
* Mark a site as logged out so the user needs to authenticate again.
|
||||
*
|
||||
* @param siteId ID of the site.
|
||||
* @param loggedOut True to set the site as logged out, false otherwise.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async setSiteLoggedOut(siteId: string, loggedOut: boolean): Promise<void> {
|
||||
protected async setSiteLoggedOut(siteId: string): Promise<void> {
|
||||
const db = await this.appDB;
|
||||
const site = await this.getSite(siteId);
|
||||
const newValues: Partial<SiteDBEntry> = {
|
||||
loggedOut: loggedOut ? 1 : 0,
|
||||
};
|
||||
|
||||
if (loggedOut) {
|
||||
// Erase the token for security.
|
||||
newValues.token = '';
|
||||
site.token = '';
|
||||
}
|
||||
site.setLoggedOut(true);
|
||||
|
||||
site.setLoggedOut(loggedOut);
|
||||
|
||||
await db.updateRecords(SITES_TABLE_NAME, newValues, { id: siteId });
|
||||
await db.updateRecords(SITES_TABLE_NAME, { loggedOut: 1 }, { id: siteId });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1315,7 +1305,7 @@ export class CoreSitesProvider {
|
|||
const versionCheck = this.isValidMoodleVersion(info);
|
||||
if (versionCheck != CoreSitesProvider.VALID_VERSION) {
|
||||
// The Moodle version is not supported, reject.
|
||||
return this.treatInvalidAppVersion(versionCheck, site.getURL(), site.getId());
|
||||
return this.treatInvalidAppVersion(versionCheck, site.getId());
|
||||
}
|
||||
|
||||
// Try to get the site config.
|
||||
|
@ -1344,7 +1334,7 @@ export class CoreSitesProvider {
|
|||
} finally {
|
||||
CoreEvents.trigger(CoreEvents.SITE_UPDATED, info, siteId);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Ignore that we cannot fetch site info. Probably the auth token is invalid.
|
||||
}
|
||||
}
|
||||
|
@ -1417,7 +1407,7 @@ export class CoreSitesProvider {
|
|||
await Promise.all(promises);
|
||||
|
||||
return ids;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Shouldn't happen.
|
||||
return [];
|
||||
}
|
||||
|
@ -1475,8 +1465,10 @@ export class CoreSitesProvider {
|
|||
* @param siteId The site ID. If not defined, current site (if available).
|
||||
* @return Promise resolved with true if disabled.
|
||||
*/
|
||||
isFeatureDisabled(name: string, siteId?: string): Promise<boolean> {
|
||||
return this.getSite(siteId).then((site) => site.isFeatureDisabled(name));
|
||||
async isFeatureDisabled(name: string, siteId?: string): Promise<boolean> {
|
||||
const site = await this.getSite(siteId);
|
||||
|
||||
return site.isFeatureDisabled(name);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1763,40 +1755,14 @@ export type CoreSiteUserTokenResponse = {
|
|||
* Site's basic info.
|
||||
*/
|
||||
export type CoreSiteBasicInfo = {
|
||||
/**
|
||||
* Site ID.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Site URL.
|
||||
*/
|
||||
siteUrl: string;
|
||||
|
||||
/**
|
||||
* User's full name.
|
||||
*/
|
||||
fullName?: string;
|
||||
|
||||
/**
|
||||
* Site's name.
|
||||
*/
|
||||
siteName?: string;
|
||||
|
||||
/**
|
||||
* User's avatar.
|
||||
*/
|
||||
avatar?: string;
|
||||
|
||||
/**
|
||||
* Badge to display in the site.
|
||||
*/
|
||||
badge?: number;
|
||||
|
||||
/**
|
||||
* Site home ID.
|
||||
*/
|
||||
siteHomeId?: number;
|
||||
id: string; // Site ID.
|
||||
siteUrl: string; // Site URL.
|
||||
siteUrlWithoutProtocol: string; // Site URL without protocol.
|
||||
fullName?: string; // User's full name.
|
||||
siteName?: string; // Site's name.
|
||||
avatar?: string; // User's avatar.
|
||||
badge?: number; // Badge to display in the site.
|
||||
siteHomeId?: number; // Site home ID.
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -1677,13 +1677,13 @@ export class CoreDomUtilsProvider {
|
|||
modalOptions: ModalOptions,
|
||||
): Promise<T | undefined> {
|
||||
|
||||
modalOptions = Object.assign(modalOptions, {
|
||||
modalOptions = Object.assign({
|
||||
cssClass: 'core-modal-lateral',
|
||||
showBackdrop: true,
|
||||
backdropDismiss: true,
|
||||
enterAnimation: CoreModalLateralTransitionEnter,
|
||||
leaveAnimation: CoreModalLateralTransitionLeave,
|
||||
});
|
||||
}, modalOptions);
|
||||
|
||||
return await this.openModal<T>(modalOptions);
|
||||
}
|
||||
|
|
|
@ -102,6 +102,8 @@ $screen-breakpoints: (
|
|||
xl: 1200px
|
||||
) !default;
|
||||
|
||||
$modal-lateral-width: 360px;
|
||||
|
||||
$core-course-image-background: #81ecec, #74b9ff, #a29bfe, #dfe6e9, #00b894, #0984e3, #b2bec3, #fdcb6e, #fd79a8, #6c5ce7 !default;
|
||||
$core-dd-question-colors: #FFFFFF, #B0C4DE, #DCDCDC, #D8BFD8, #87CEFA, #DAA520, #FFD700, #F0E68C !default;
|
||||
$core-text-hightlight-background-color: lighten($blue, 40%) !default;
|
||||
|
@ -127,7 +129,15 @@ $core-login-loading-color-dark: $text-color-dark !default;
|
|||
$core-login-hide-forgot-password: false !default;
|
||||
$core-login-hide-need-help: false !default;
|
||||
|
||||
// Configuration options for more page.
|
||||
// Configuration options for more page. (deprecated on 4.0)
|
||||
$core-more-hide-siteinfo: false !default;
|
||||
$core-more-hide-sitename: false !default;
|
||||
$core-more-hide-siteurl: false !default;
|
||||
|
||||
// Configuration options for user page.
|
||||
$core-user-hide-siteinfo: $core-more-hide-siteinfo !default;
|
||||
$core-user-hide-sitename: $core-more-hide-sitename !default;
|
||||
$core-user-hide-siteurl: $core-more-hide-siteurl !default;
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -508,7 +508,7 @@ body.core-iframe-fullscreen ion-router-outlet {
|
|||
z-index: 100000 !important;
|
||||
}
|
||||
|
||||
@media only screen and (min-height: 400px) and (min-width: 300px) {
|
||||
@media only screen and (min-height: 400px) and (min-width: #{$modal-lateral-width}) {
|
||||
.core-modal-lateral {
|
||||
--ion-safe-area-left: 0px;
|
||||
--ion-safe-area-right: 0px;
|
||||
|
@ -519,7 +519,7 @@ body.core-iframe-fullscreen ion-router-outlet {
|
|||
display: block;
|
||||
height: 100% !important;
|
||||
width: auto;
|
||||
min-width: 300px;
|
||||
min-width: #{$modal-lateral-width};
|
||||
box-shadow: 0 28px 48px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
ion-backdrop {
|
||||
|
@ -528,6 +528,29 @@ body.core-iframe-fullscreen ion-router-outlet {
|
|||
}
|
||||
}
|
||||
|
||||
@each $breakpoint, $width in $screen-breakpoints {
|
||||
@media only screen and (min-height: 400px) and (min-width: #{$width}) {
|
||||
.core-modal-lateral-#{$breakpoint} {
|
||||
--ion-safe-area-left: 0px;
|
||||
--ion-safe-area-right: 0px;
|
||||
|
||||
.modal-wrapper {
|
||||
position: absolute;
|
||||
@include position(0 !important, 0 !important, 0 !important, unset !important);
|
||||
display: block;
|
||||
height: 100% !important;
|
||||
width: auto;
|
||||
min-width: #{$width};
|
||||
box-shadow: 0 28px 48px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
ion-backdrop {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Hidden submit button.
|
||||
.core-submit-hidden-enter {
|
||||
position: absolute;
|
||||
|
@ -655,12 +678,13 @@ ion-card ion-item:only-child {
|
|||
|
||||
ion-toolbar h1 img.core-bar-button-image,
|
||||
ion-toolbar h1 .core-bar-button-image img {
|
||||
padding: 0;
|
||||
padding: 4px;
|
||||
width: var(--core-header-toolbar-button-image-size);
|
||||
height: var(--core-header-toolbar-button-image-size);
|
||||
max-width: var(--core-header-toolbar-button-image-size);
|
||||
max-height: var(--core-header-toolbar-button-image-size);
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
// Action sheet.
|
||||
|
@ -1038,6 +1062,10 @@ ion-item.item-multiple-inputs.only-links {
|
|||
}
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// Case with ion-input + ion-select inside.
|
||||
ion-item.item-input.item-multiple-inputs {
|
||||
.flex-row {
|
||||
|
|
Loading…
Reference in New Issue