From 09a2cd2ceff509b590563b14af895e249f01b049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 10 Dec 2024 09:27:32 +0100 Subject: [PATCH 1/2] MOBILE-4653 core: Add site logo component --- moodle.config.json | 1 + .../classes/sites/unauthenticated-site.ts | 30 +++- src/core/components/site-logo/site-logo.html | 14 ++ src/core/components/site-logo/site-logo.scss | 22 +++ src/core/components/site-logo/site-logo.ts | 131 ++++++++++++++++++ src/core/directives/format-text.ts | 6 +- src/core/directives/tests/format-text.test.ts | 13 +- .../courses/courses-my-lazy.module.ts | 2 + src/core/features/courses/pages/my/my.html | 3 +- src/core/features/courses/pages/my/my.ts | 13 -- .../login/login-credentials-lazy.module.ts | 2 + .../login/login-reconnect-lazy.module.ts | 2 + src/core/features/login/login.scss | 25 ++-- .../login/pages/credentials/credentials.html | 11 +- .../login/pages/credentials/credentials.ts | 6 - .../login/pages/reconnect/reconnect.html | 10 +- .../login/pages/reconnect/reconnect.ts | 2 - src/core/features/login/pages/site/site.scss | 2 +- ...-displayed-when-adding-a-new-account_9.png | Bin 28165 -> 28136 bytes ...wloginform-setting-in-the-app-login_10.png | Bin 20542 -> 20520 bytes ...inform-setting-in-the-app-reconnect_10.png | Bin 18200 -> 18174 bytes .../components/user-menu/user-menu.html | 14 +- .../components/user-menu/user-menu.scss | 20 +-- .../components/user-menu/user-menu.ts | 27 +--- .../mainmenu/mainmenu-home-lazy.module.ts | 2 + .../features/mainmenu/pages/home/home.html | 3 +- src/theme/components/ion-header.scss | 11 +- src/theme/globals.variables.scss | 1 - src/theme/theme.design-system.scss | 8 -- src/types/config.d.ts | 1 + 30 files changed, 249 insertions(+), 133 deletions(-) create mode 100644 src/core/components/site-logo/site-logo.html create mode 100644 src/core/components/site-logo/site-logo.scss create mode 100644 src/core/components/site-logo/site-logo.ts diff --git a/moodle.config.json b/moodle.config.json index c93ad42ce..9a0100bdf 100644 --- a/moodle.config.json +++ b/moodle.config.json @@ -98,6 +98,7 @@ "enableonboarding": true, "forceColorScheme": "", "forceLoginLogo": false, + "showTopLogo": "hidden", "ioswebviewscheme": "moodleappfs", "appstores": { "android": "com.moodle.moodlemobile", diff --git a/src/core/classes/sites/unauthenticated-site.ts b/src/core/classes/sites/unauthenticated-site.ts index fc958b0e3..643c25af1 100644 --- a/src/core/classes/sites/unauthenticated-site.ts +++ b/src/core/classes/sites/unauthenticated-site.ts @@ -163,7 +163,7 @@ export class CoreUnauthenticatedSite { } /** - * Check whether the app should use the local logo instead of the remote one. + * Check whether the app should use the local logo instead or the remote one. * * @returns Whether local logo is forced. */ @@ -180,10 +180,34 @@ export class CoreUnauthenticatedSite { getLogoUrl(config?: CoreSitePublicConfigResponse): string | undefined { config = config ?? this.publicConfig; if (!config || this.forcesLocalLogo()) { - return 'assets/img/login_logo.png'; + return; } - return config.logourl || config.compactlogourl || 'assets/img/login_logo.png'; + return config.logourl || config.compactlogourl || undefined; + } + + /** + * Check show top logo mode. + * + * @returns The top logo mode. + */ + getShowTopLogo(): 'online' | 'offline' | 'hidden' { + return this.isDemoModeSite() ? 'hidden' : CoreConstants.CONFIG.showTopLogo; + } + + /** + * Get logo URL from a site public config. + * + * @param config Site public config. + * @returns Logo URL. + */ + getTopLogoUrl(config?: CoreSitePublicConfigResponse): string | undefined { + config = config ?? this.publicConfig; + if (!config || this.getShowTopLogo() !== 'online') { + return; + } + + return config.logourl || config.compactlogourl || undefined; } /** diff --git a/src/core/components/site-logo/site-logo.html b/src/core/components/site-logo/site-logo.html new file mode 100644 index 000000000..c4fff46be --- /dev/null +++ b/src/core/components/site-logo/site-logo.html @@ -0,0 +1,14 @@ +
+ + +
+ +

+ +

+ +

+ +

diff --git a/src/core/components/site-logo/site-logo.scss b/src/core/components/site-logo/site-logo.scss new file mode 100644 index 000000000..ad385ed4b --- /dev/null +++ b/src/core/components/site-logo/site-logo.scss @@ -0,0 +1,22 @@ +:host { + display: block; +} + +.core-logo-container { + margin-bottom: var(--core-site-logo-margin-bottom, 0px); +} + +img.core-logo { + max-height: var(--core-site-logo-max-height); + max-width: var(--core-site-logo-max-width, 100%); + + + width: var(--core-site-logo-width, auto); + margin: var(--core-site-logo-margin, 0px); +} + +.core-logo-sitename { + display: var(--core-site-logo-sitename-display, block); + font: var(--core-site-logo-sitename-font); + margin-bottom: var(--core-site-logo-sitename-margin-bottom, 0px); +} diff --git a/src/core/components/site-logo/site-logo.ts b/src/core/components/site-logo/site-logo.ts new file mode 100644 index 000000000..ae88652c8 --- /dev/null +++ b/src/core/components/site-logo/site-logo.ts @@ -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 { Component, OnInit, OnDestroy, Input } from '@angular/core'; +import { CoreSharedModule } from '@/core/shared.module'; +import { CoreSites } from '@services/sites'; +import { CoreEventObserver, CoreEvents } from '@singletons/events'; +import { CoreSite } from '@classes/sites/site'; +import { toBoolean } from '@/core/transforms/boolean'; +import { CorePromiseUtils } from '@singletons/promise-utils'; +import { CoreUnauthenticatedSite } from '@classes/sites/unauthenticated-site'; + +/** + * Component to render the current site logo. + */ +@Component({ + selector: 'core-site-logo', + templateUrl: 'site-logo.html', + styleUrl: 'site-logo.scss', + standalone: true, + imports: [CoreSharedModule], + +}) +export class CoreSiteLogoComponent implements OnInit, OnDestroy { + + @Input({ transform: toBoolean }) hideOnError = false; + @Input() siteNameMode: CoreSiteLogoSiteNameMode = CoreSiteLogoSiteNameMode.NOTAG; + @Input({ transform: toBoolean }) showLogo = true; + @Input() site?: CoreSite | CoreUnauthenticatedSite; + @Input() logoType: 'top' | 'login' = 'login'; + + siteName?: string; + siteId?: string; + siteLogo?: string; + logoLoaded = false; + fallbackLogo = ''; + showSiteName = true; + + protected updateSiteObserver?: CoreEventObserver; + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + this.loadSite(); + + this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, async () => { + await this.loadInfo(); + }, this.siteId); + + this.fallbackLogo = this.logoType === 'top' ? 'assets/img/top_logo.png' : 'assets/img/login_logo.png'; + this.showSiteName = this.logoType !== 'top'; + + await this.loadInfo(); + } + + /** + * Function to handle the image error. + */ + imageError(): void { + if (this.hideOnError) { + this.showLogo = false; + } + this.siteLogo = undefined; + } + + /** + * Load the site and siteId. + * + * @returns Site. + */ + protected loadSite(): CoreSite | CoreUnauthenticatedSite { + this.site = this.site ?? CoreSites.getRequiredCurrentSite(); + + // During login, the siteId could be not defined yet. + if (!this.siteId && this.site instanceof CoreSite) { + this.siteId = this.site.getId(); + } + + return this.site; + } + + /** + * Load the site name and logo. + */ + protected async loadInfo(): Promise { + const site = this.loadSite(); + + this.siteName = await site.getSiteName() || ''; + + this.showSiteName = this.logoType !== 'top' || site.getShowTopLogo() === 'hidden'; + + if (this.logoType === 'top' && site.getShowTopLogo() === 'hidden') { + this.showLogo = false; + } else { + // Get the public config to avoid race conditions when retrieving the logo. + const siteConfig = await CorePromiseUtils.ignoreErrors(site.getPublicConfig()); + + this.siteLogo = this.logoType === 'top' + ? site.getTopLogoUrl(siteConfig) + : site.getLogoUrl(siteConfig); + } + + this.logoLoaded = true; + } + + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.updateSiteObserver?.off(); + } + +} + +export const enum CoreSiteLogoSiteNameMode { + HEADING2 = 'h2', + PARAGRAPH = 'p', + NOTAG = '', +} diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts index 7257beb71..1e8035b5d 100644 --- a/src/core/directives/format-text.ts +++ b/src/core/directives/format-text.ts @@ -120,14 +120,14 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec this.element.innerHTML = this.emptyText; this.element.addEventListener('click', (event) => this.elementClicked(event)); - - this.siteId = this.siteId || CoreSites.getCurrentSiteId(); } /** * @inheritdoc */ ngOnChanges(changes: { [name: string]: SimpleChange }): void { + this.siteId = this.siteId || CoreSites.getCurrentSiteId(); + if (changes.text || changes.filter || changes.contextLevel || changes.contextInstanceId) { this.formatAndRenderContents(); @@ -450,7 +450,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec let formatted: string; let filters: CoreFilterFilter[] = []; - if (filter) { + if (filter && siteId) { const filterResult = await CoreFilterHelper.getFiltersAndFormatText( this.text || '', this.contextLevel || ContextLevel.SYSTEM, diff --git a/src/core/directives/tests/format-text.test.ts b/src/core/directives/tests/format-text.test.ts index 1c1f34550..a9196dc9e 100644 --- a/src/core/directives/tests/format-text.test.ts +++ b/src/core/directives/tests/format-text.test.ts @@ -96,6 +96,17 @@ describe('CoreFormatTextDirective', () => { }); it('should get filters from server and format text', async () => { + // Arrange + const site = mock(new CoreSite('25', 'https://mysite.com', 'token'), { + getId: () => site.id, + }); + + mockSingleton(CoreSites, { + getSite: () => Promise.resolve(site), + getCurrentSite: () => site, + getCurrentSiteId: () => site.id, + }); + // Arrange mockSingleton(CoreFilterHelper, { getFiltersAndFormatText: () => Promise.resolve({ @@ -124,7 +135,7 @@ describe('CoreFormatTextDirective', () => { ContextLevel.COURSE, 42, expect.anything(), - undefined, + '25', ); }); diff --git a/src/core/features/courses/courses-my-lazy.module.ts b/src/core/features/courses/courses-my-lazy.module.ts index 375c7ae8e..ba5789efb 100644 --- a/src/core/features/courses/courses-my-lazy.module.ts +++ b/src/core/features/courses/courses-my-lazy.module.ts @@ -20,6 +20,7 @@ import { CoreBlockComponentsModule } from '@features/block/components/components import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module'; import { CoreCoursesMyPage } from '@features/courses/pages/my/my'; +import { CoreSiteLogoComponent } from '@/core/components/site-logo/site-logo'; const routes: Routes = [ { @@ -34,6 +35,7 @@ const routes: Routes = [ CoreSharedModule, CoreBlockComponentsModule, CoreMainMenuComponentsModule, + CoreSiteLogoComponent, ], declarations: [ CoreCoursesMyPage, diff --git a/src/core/features/courses/pages/my/my.html b/src/core/features/courses/pages/my/my.html index 933afcc04..23e27b2e3 100644 --- a/src/core/features/courses/pages/my/my.html +++ b/src/core/features/courses/pages/my/my.html @@ -5,8 +5,7 @@

- - +

diff --git a/src/core/features/courses/pages/my/my.ts b/src/core/features/courses/pages/my/my.ts index f7a9379ae..f0e1d978d 100644 --- a/src/core/features/courses/pages/my/my.ts +++ b/src/core/features/courses/pages/my/my.ts @@ -48,7 +48,6 @@ export class CoreCoursesMyPage implements OnInit, OnDestroy, AsyncDirective { @ViewChild(CoreBlockComponent) block!: CoreBlockComponent; - siteName = ''; downloadCoursesEnabled = false; userId: number; loadedBlock?: Partial; @@ -66,8 +65,6 @@ export class CoreCoursesMyPage implements OnInit, OnDestroy, AsyncDirective { // Refresh the enabled flags if site is updated. this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, async () => { this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite(); - await this.loadSiteName(); - }, CoreSites.getCurrentSiteId()); this.userId = CoreSites.getCurrentSiteUserId(); @@ -98,8 +95,6 @@ export class CoreCoursesMyPage implements OnInit, OnDestroy, AsyncDirective { CoreSites.loginNavigationFinished(); - await this.loadSiteName(); - this.loadContent(true); } @@ -156,14 +151,6 @@ export class CoreCoursesMyPage implements OnInit, OnDestroy, AsyncDirective { this.logView(); } - /** - * Load the site name. - */ - protected async loadSiteName(): Promise { - const site = CoreSites.getRequiredCurrentSite(); - this.siteName = await site.getSiteName() || ''; - } - /** * Load fallback blocks. */ diff --git a/src/core/features/login/login-credentials-lazy.module.ts b/src/core/features/login/login-credentials-lazy.module.ts index 0827e8b98..0ecf235e4 100644 --- a/src/core/features/login/login-credentials-lazy.module.ts +++ b/src/core/features/login/login-credentials-lazy.module.ts @@ -18,6 +18,7 @@ import { RouterModule, Routes } from '@angular/router'; import { CoreSharedModule } from '@/core/shared.module'; import { CoreLoginComponentsModule } from '@features/login/components/components.module'; import { CoreLoginCredentialsPage } from '@features/login/pages/credentials/credentials'; +import { CoreSiteLogoComponent } from '@/core/components/site-logo/site-logo'; const routes: Routes = [ { @@ -31,6 +32,7 @@ const routes: Routes = [ RouterModule.forChild(routes), CoreSharedModule, CoreLoginComponentsModule, + CoreSiteLogoComponent, ], declarations: [ CoreLoginCredentialsPage, diff --git a/src/core/features/login/login-reconnect-lazy.module.ts b/src/core/features/login/login-reconnect-lazy.module.ts index 22084638f..e0696584c 100644 --- a/src/core/features/login/login-reconnect-lazy.module.ts +++ b/src/core/features/login/login-reconnect-lazy.module.ts @@ -18,6 +18,7 @@ import { RouterModule, Routes } from '@angular/router'; import { CoreSharedModule } from '@/core/shared.module'; import { CoreLoginComponentsModule } from '@features/login/components/components.module'; import { CoreLoginReconnectPage } from '@features/login/pages/reconnect/reconnect'; +import { CoreSiteLogoComponent } from '@/core/components/site-logo/site-logo'; const routes: Routes = [ { @@ -31,6 +32,7 @@ const routes: Routes = [ RouterModule.forChild(routes), CoreSharedModule, CoreLoginComponentsModule, + CoreSiteLogoComponent, ], declarations: [ CoreLoginReconnectPage, diff --git a/src/core/features/login/login.scss b/src/core/features/login/login.scss index e02eaea05..27a95309d 100644 --- a/src/core/features/login/login.scss +++ b/src/core/features/login/login.scss @@ -29,20 +29,14 @@ margin-bottom: 32px; .core-login-site { - .core-login-site-logo { - width: 90%; - max-width: 300px; - margin: 0px auto; + core-site-logo { + --core-site-logo-max-height: 104px; + --core-site-logo-sitename-margin-bottom: 8px; + --core-site-logo-sitename-font: var(--mdl-typography-subtitle-font-lg); - img { - max-width: 100%; - max-height: 104px; - } - } - - .core-sitename { - font-size: 1.2rem; - margin-bottom: 8px; + --core-site-logo-max-width: 300px; + --core-site-logo-width: 90%; + --core-site-logo-margin: 0 auto; } .core-siteurl { @@ -89,9 +83,12 @@ } @if ($core-fixed-url) { - .core-sitename, .core-siteurl { + .core-siteurl { display: none; } + core-site-logo { + --core-site-logo-sitename-display: none; + } } @if ($core-login-button-outline) { diff --git a/src/core/features/login/pages/credentials/credentials.html b/src/core/features/login/pages/credentials/credentials.html index c6d71047b..a54176236 100644 --- a/src/core/features/login/pages/credentials/credentials.html +++ b/src/core/features/login/pages/credentials/credentials.html @@ -23,16 +23,7 @@