commit
						620857621c
					
				| @ -12,13 +12,9 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; | ||||
| import { AddonRemoteThemes, AddonRemoteThemesProvider } from './services/remotethemes'; | ||||
| 
 | ||||
| // List of providers (without handlers).
 | ||||
| export const ADDON_REMOTETHEMES_SERVICES: Type<unknown>[] = [ | ||||
|     AddonRemoteThemesProvider, | ||||
| ]; | ||||
| import { APP_INITIALIZER, NgModule } from '@angular/core'; | ||||
| import { CoreStyles } from '@features/styles/services/styles'; | ||||
| import { AddonRemoteThemesHandler } from './services/remotethemes-handler'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     providers: [ | ||||
| @ -27,7 +23,7 @@ export const ADDON_REMOTETHEMES_SERVICES: Type<unknown>[] = [ | ||||
|             multi: true, | ||||
|             deps: [], | ||||
|             useFactory: () => async () => { | ||||
|                 await AddonRemoteThemes.initialize(); | ||||
|                 CoreStyles.registerStyleHandler(AddonRemoteThemesHandler.instance); | ||||
|             }, | ||||
|         }, | ||||
|     ], | ||||
|  | ||||
							
								
								
									
										148
									
								
								src/addons/remotethemes/services/remotethemes-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								src/addons/remotethemes/services/remotethemes-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,148 @@ | ||||
| // (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 { CoreConstants } from '@/core/constants'; | ||||
| import { CoreSitePublicConfigResponse } from '@classes/site'; | ||||
| import { CoreFile } from '@services/file'; | ||||
| import { CoreFilepool } from '@services/filepool'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreWS } from '@services/ws'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { CoreStyleHandler, CoreStylesService } from '@features/styles/services/styles'; | ||||
| import { CoreLogger } from '@singletons/logger'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| 
 | ||||
| const SEPARATOR_35 = /\/\*\*? *3\.5(\.0)? *styles? *\*\//i; // A comment like "/* 3.5 styles */".
 | ||||
| const COMPONENT = 'mmaRemoteStyles'; | ||||
| 
 | ||||
| /** | ||||
|  * Service to handle remote themes. | ||||
|  * A remote theme is a CSS sheet stored in the site that allows customising the Mobile app. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonRemoteThemesHandlerService implements CoreStyleHandler { | ||||
| 
 | ||||
|     protected logger: CoreLogger; | ||||
| 
 | ||||
|     name = 'mobilecssurl'; | ||||
|     priority = 1000; | ||||
| 
 | ||||
|     constructor() { | ||||
|         this.logger = CoreLogger.getInstance('AddonRemoteThemes'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritDoc | ||||
|      */ | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||
|     async isEnabled(siteId: string, config?: CoreSitePublicConfigResponse): Promise<boolean> { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritDoc | ||||
|      */ | ||||
|     async getStyle(siteId: string, config?: CoreSitePublicConfigResponse): Promise<string> { | ||||
|         if (siteId == CoreStylesService.TMP_SITE_ID) { | ||||
|             if (!config) { | ||||
|                 return ''; | ||||
|             } | ||||
| 
 | ||||
|             // Config received, it's a temp site.
 | ||||
|             return await this.get35Styles(config.mobilecssurl); | ||||
|         } | ||||
| 
 | ||||
|         const site = await CoreSites.getSite(siteId); | ||||
|         const infos = site.getInfo(); | ||||
| 
 | ||||
|         if (!infos?.mobilecssurl) { | ||||
|             if (infos?.mobilecssurl === '') { | ||||
|                 // CSS URL is empty. Delete downloaded files (if any).
 | ||||
|                 CoreFilepool.removeFilesByComponent(siteId, COMPONENT, 1); | ||||
|             } | ||||
| 
 | ||||
|             return ''; | ||||
|         } | ||||
| 
 | ||||
|         let fileUrl = infos.mobilecssurl; | ||||
| 
 | ||||
|         if (CoreFile.isAvailable()) { | ||||
|             // The file system is available. Download the file and remove old CSS files if needed.
 | ||||
|             fileUrl = await this.downloadFileAndRemoveOld(siteId, fileUrl); | ||||
|         } | ||||
| 
 | ||||
|         this.logger.debug('Loading styles from: ', fileUrl); | ||||
| 
 | ||||
|         // Get the CSS content using HTTP because we will treat the styles before saving them in the file.
 | ||||
|         const style = await this.get35Styles(fileUrl); | ||||
| 
 | ||||
|         if (style != '') { | ||||
|             // Treat the CSS.
 | ||||
|             CoreUtils.ignoreErrors( | ||||
|                 CoreFilepool.treatCSSCode(siteId, fileUrl, style, COMPONENT, 2), | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         return style; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the CSS code has a separator for 3.5 styles. If it does, get only the styles after the separator. | ||||
|      * | ||||
|      * @param url Url to get the code from. | ||||
|      * @return The filtered styles. | ||||
|      */ | ||||
|     protected async get35Styles(url?: string): Promise<string> { | ||||
|         if (!url) { | ||||
|             return ''; | ||||
|         } | ||||
| 
 | ||||
|         const cssCode = await CoreWS.getText(url); | ||||
| 
 | ||||
|         const separatorPos = cssCode.search(SEPARATOR_35); | ||||
|         if (separatorPos > -1) { | ||||
|             return cssCode.substr(separatorPos).replace(SEPARATOR_35, ''); | ||||
|         } | ||||
| 
 | ||||
|         return cssCode; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Downloads a CSS file and remove old files if needed. | ||||
|      * | ||||
|      * @param siteId Site ID. | ||||
|      * @param url File URL. | ||||
|      * @return Promise resolved when the file is downloaded. | ||||
|      */ | ||||
|     protected async downloadFileAndRemoveOld(siteId: string, url: string): Promise<string> { | ||||
| 
 | ||||
|         try { | ||||
|             // Check if the file is downloaded.
 | ||||
|             const state = await CoreFilepool.getFileStateByUrl(siteId, url); | ||||
| 
 | ||||
|             if (state == CoreConstants.NOT_DOWNLOADED) { | ||||
|                 // File not downloaded, URL has changed or first time. Delete downloaded CSS files.
 | ||||
|                 await CoreFilepool.removeFilesByComponent(siteId, COMPONENT, 1); | ||||
|             } | ||||
|         } catch { | ||||
|             // An error occurred while getting state (shouldn't happen). Don't delete downloaded file.
 | ||||
|         } | ||||
| 
 | ||||
|         return CoreFilepool.downloadUrl(siteId, url, false, COMPONENT, 1); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export const AddonRemoteThemesHandler = makeSingleton(AddonRemoteThemesHandlerService); | ||||
| @ -105,7 +105,7 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit { | ||||
|      */ | ||||
|     protected setData(): void { | ||||
|         this.label = this.shown ? 'core.hide' : 'core.show'; | ||||
|         this.iconName = this.shown ? 'eye-off' : 'eye'; | ||||
|         this.iconName = this.shown ? 'fas-eye-slash' : 'fas-eye'; | ||||
|         if (this.input) { | ||||
|             this.input.type = this.shown ? 'text' : 'password'; | ||||
|         } | ||||
|  | ||||
| @ -63,6 +63,7 @@ import { CORE_SEARCH_SERVICES } from '@features/search/search.module'; | ||||
| import { CORE_SETTINGS_SERVICES } from '@features/settings/settings.module'; | ||||
| import { CORE_SITEHOME_SERVICES } from '@features/sitehome/sitehome.module'; | ||||
| import { CORE_TAG_SERVICES } from '@features/tag/tag.module'; | ||||
| import { CORE_STYLE_SERVICES } from '@features/styles/styles.module'; | ||||
| import { CORE_USER_SERVICES } from '@features/user/user.module'; | ||||
| import { CORE_XAPI_SERVICES } from '@features/xapi/xapi.module'; | ||||
| import { CoreSitePluginsProvider } from '@features/siteplugins/services/siteplugins'; | ||||
| @ -146,7 +147,6 @@ import { ADDON_MOD_WORKSHOP_SERVICES } from '@addons/mod/workshop/workshop.modul | ||||
| import { ADDON_NOTES_SERVICES } from '@addons/notes/notes.module'; | ||||
| import { ADDON_NOTIFICATIONS_SERVICES } from '@addons/notifications/notifications.module'; | ||||
| import { ADDON_PRIVATEFILES_SERVICES } from '@addons/privatefiles/privatefiles.module'; | ||||
| import { ADDON_REMOTETHEMES_SERVICES } from '@addons/remotethemes/remotethemes.module'; | ||||
| 
 | ||||
| // Import some addon modules that define components, directives and pipes. Only import the important ones.
 | ||||
| import { AddonModAssignComponentsModule } from '@addons/mod/assign/components/components.module'; | ||||
| @ -277,6 +277,7 @@ export class CoreCompileProvider { | ||||
|             ...CORE_SITEHOME_SERVICES, | ||||
|             CoreSitePluginsProvider, | ||||
|             ...CORE_TAG_SERVICES, | ||||
|             ...CORE_STYLE_SERVICES, | ||||
|             ...CORE_USER_SERVICES, | ||||
|             ...CORE_XAPI_SERVICES, | ||||
|             ...IONIC_NATIVE_SERVICES, | ||||
| @ -311,7 +312,6 @@ export class CoreCompileProvider { | ||||
|             ...ADDON_NOTES_SERVICES, | ||||
|             ...ADDON_NOTIFICATIONS_SERVICES, | ||||
|             ...ADDON_PRIVATEFILES_SERVICES, | ||||
|             ...ADDON_REMOTETHEMES_SERVICES, | ||||
|         ]; | ||||
| 
 | ||||
|         // We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance.
 | ||||
|  | ||||
| @ -93,14 +93,14 @@ | ||||
| 
 | ||||
|             <ion-buttons class="ion-padding core-course-section-nav-buttons safe-padding-horizontal" | ||||
|                 *ngIf="displaySectionSelector && sections?.length"> | ||||
|                 <ion-button *ngIf="previousSection" (click)="sectionChanged(previousSection)" fill="outline" | ||||
|                 <ion-button *ngIf="previousSection" (click)="sectionChanged(previousSection)" fill="outline" color="primary" | ||||
|                     [attr.aria-label]="('core.previous' | translate) + ': ' + previousSection.name"> | ||||
|                     <ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|                     <core-format-text class="sr-only" [text]="previousSection.name" contextLevel="course" | ||||
|                         [contextInstanceId]="course?.id"> | ||||
|                     </core-format-text> | ||||
|                 </ion-button> | ||||
|                 <ion-button *ngIf="nextSection" (click)="sectionChanged(nextSection)" fill="solid" | ||||
|                 <ion-button *ngIf="nextSection" (click)="sectionChanged(nextSection)" fill="solid" color="primary" | ||||
|                     [attr.aria-label]="('core.next' | translate) + ': ' + nextSection.name"> | ||||
|                     <core-format-text class="sr-only" [text]="nextSection.name" contextLevel="course" | ||||
|                         [contextInstanceId]="course?.id"> | ||||
|  | ||||
| @ -35,6 +35,7 @@ import { CoreSettingsModule } from './settings/settings.module'; | ||||
| import { CoreSharedFilesModule } from './sharedfiles/sharedfiles.module'; | ||||
| import { CoreSiteHomeModule } from './sitehome/sitehome.module'; | ||||
| import { CoreSitePluginsModule } from './siteplugins/siteplugins.module'; | ||||
| import { CoreStylesModule } from './styles/styles.module'; | ||||
| import { CoreTagModule } from './tag/tag.module'; | ||||
| import { CoreUserModule } from './user/user.module'; | ||||
| import { CoreViewerModule } from './viewer/viewer.module'; | ||||
| @ -64,6 +65,7 @@ import { CoreXAPIModule } from './xapi/xapi.module'; | ||||
|         CoreSiteHomeModule, | ||||
|         CoreSitePluginsModule, | ||||
|         CoreTagModule, | ||||
|         CoreStylesModule, | ||||
|         CoreUserModule, | ||||
|         CoreViewerModule, | ||||
|         CoreXAPIModule, | ||||
|  | ||||
| @ -8,7 +8,7 @@ | ||||
|         </h1> | ||||
|         <ion-buttons slot="end"> | ||||
|             <ion-button fill="clear" (click)="copyInfo()" [attr.aria-label]="'core.settings.copyinfo' | translate"> | ||||
|                 <ion-icon slot="icon-only" name="fas-clipboard" color="light" aria-hidden="true"></ion-icon> | ||||
|                 <ion-icon slot="icon-only" name="fas-clipboard" aria-hidden="true"></ion-icon> | ||||
|             </ion-button> | ||||
|         </ion-buttons> | ||||
|     </ion-toolbar> | ||||
|  | ||||
| @ -434,12 +434,14 @@ export class CoreSettingsHelperProvider { | ||||
|     setColorScheme(colorScheme: CoreColorScheme): void { | ||||
|         if (colorScheme == CoreColorScheme.SYSTEM && this.prefersDark) { | ||||
|             // Listen for changes to the prefers-color-scheme media query.
 | ||||
|             this.prefersDark.addEventListener('change', this.toggleDarkModeListener); | ||||
|             this.prefersDark.addEventListener && | ||||
|                 this.prefersDark.addEventListener('change', this.toggleDarkModeListener); | ||||
| 
 | ||||
|             this.toggleDarkMode(this.prefersDark.matches); | ||||
|         } else { | ||||
|             // Stop listening to changes.
 | ||||
|             this.prefersDark?.removeEventListener('change', this.toggleDarkModeListener); | ||||
|             this.prefersDark?.removeEventListener && | ||||
|                 this.prefersDark?.removeEventListener('change', this.toggleDarkModeListener); | ||||
| 
 | ||||
|             this.toggleDarkMode(colorScheme == CoreColorScheme.DARK); | ||||
|         } | ||||
|  | ||||
| @ -13,41 +13,74 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { Md5 } from 'ts-md5/dist/md5'; | ||||
| 
 | ||||
| import { CoreConstants } from '@/core/constants'; | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| import { CoreSitePublicConfigResponse } from '@classes/site'; | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreFile } from '@services/file'; | ||||
| import { CoreFilepool } from '@services/filepool'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreWS } from '@services/ws'; | ||||
| import { CoreLogger } from '@singletons/logger'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { CoreEvents } from '@singletons/events'; | ||||
| 
 | ||||
| const SEPARATOR_35 = /\/\*\*? *3\.5(\.0)? *styles? *\*\//i; // A comment like "/* 3.5 styles */".
 | ||||
| export const TMP_SITE_ID = 'tmpsite'; | ||||
| import { Md5 } from 'ts-md5'; | ||||
| import { CoreLogger } from '../../../singletons/logger'; | ||||
| 
 | ||||
| /** | ||||
|  * Service to handle remote themes. A remote theme is a CSS sheet stored in the site that allows customising the Mobile app. | ||||
|  * Interface that all style handlers must implement. | ||||
|  */ | ||||
| export interface CoreStyleHandler { | ||||
| 
 | ||||
|     /** | ||||
|      * Source name. | ||||
|      */ | ||||
|     name: string; | ||||
| 
 | ||||
|     /** | ||||
|      * Priority of application. | ||||
|      */ | ||||
|     priority: number; | ||||
| 
 | ||||
|     /** | ||||
|      * Wether the handler should be enabled for the site. | ||||
|      * | ||||
|      * @param siteId Site Id. | ||||
|      * @param config Site public config for temp sites. | ||||
|      * @return Wether the handler should be enabled for the site. | ||||
|      */ | ||||
|     isEnabled(siteId: string, config?: CoreSitePublicConfigResponse): boolean | Promise<boolean>; | ||||
| 
 | ||||
|     /** | ||||
|      * Get the style for the site. | ||||
|      * | ||||
|      * @param siteId Site Id. | ||||
|      * @param config Site public config for temp sites. | ||||
|      * @return CSS to apply. | ||||
|      */ | ||||
|     getStyle(siteId?: string, config?: CoreSitePublicConfigResponse): string | Promise<string>; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Singleton with helper functions to style the app. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonRemoteThemesProvider { | ||||
| 
 | ||||
|     static readonly COMPONENT = 'mmaRemoteStyles'; | ||||
| export class CoreStylesService { | ||||
| 
 | ||||
|     protected logger: CoreLogger; | ||||
|     protected stylesEls: {[siteId: string]: { element: HTMLStyleElement; hash: string }} = {}; | ||||
| 
 | ||||
|     protected stylesEls: { | ||||
|         [siteId: string]: { | ||||
|             [sourceName: string]: string; // Hashes
 | ||||
|         }; | ||||
|     } = {}; | ||||
| 
 | ||||
|     protected styleHandlers: CoreStyleHandler[] = []; | ||||
| 
 | ||||
|     static readonly TMP_SITE_ID = 'tmpsite'; | ||||
| 
 | ||||
|     constructor() { | ||||
|         this.logger = CoreLogger.getInstance('AddonRemoteThemes'); | ||||
|         this.logger = CoreLogger.getInstance('CoreStyles'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initialize remote themes. | ||||
|      * Initialize styles. | ||||
|      */ | ||||
|     async initialize(): Promise<void> { | ||||
|         this.listenEvents(); | ||||
| @ -59,6 +92,18 @@ export class AddonRemoteThemesProvider { | ||||
|         await this.preloadSites(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Register a new style handler. | ||||
|      * | ||||
|      * @param styleHandler Style handler to be registered. | ||||
|      */ | ||||
|     registerStyleHandler(styleHandler: CoreStyleHandler): void { | ||||
|         this.styleHandlers.push(styleHandler); | ||||
| 
 | ||||
|         // Sort them by priority, greatest go last because style loaded last it's more important.
 | ||||
|         this.styleHandlers = this.styleHandlers.sort((a, b) => a.priority! >= b.priority! ? 1 : -1); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Listen events. | ||||
|      */ | ||||
| @ -79,10 +124,10 @@ export class AddonRemoteThemesProvider { | ||||
|                 // User has logged in, remove tmp styles and enable loaded styles.
 | ||||
|                 if (data.siteId == CoreSites.getCurrentSiteId()) { | ||||
|                     this.unloadTmpStyles(); | ||||
|                     this.enable(data.siteId); | ||||
|                     this.enableSiteStyles(data.siteId); | ||||
|                 } | ||||
|             } catch (error) { | ||||
|                 this.logger.error('Error adding remote styles for new site', error); | ||||
|                 this.logger.error('Error adding styles for new site', error); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
| @ -98,7 +143,7 @@ export class AddonRemoteThemesProvider { | ||||
|         // Enable styles of current site on login.
 | ||||
|         CoreEvents.on(CoreEvents.LOGIN, (data) => { | ||||
|             this.unloadTmpStyles(); | ||||
|             this.enable(data.siteId); | ||||
|             this.enableSiteStyles(data.siteId); | ||||
|         }); | ||||
| 
 | ||||
|         // Disable added styles on logout.
 | ||||
| @ -113,7 +158,7 @@ export class AddonRemoteThemesProvider { | ||||
| 
 | ||||
|         // Load temporary styles when site config is checked in login.
 | ||||
|         CoreEvents.on(CoreEvents.LOGIN_SITE_CHECKED, (data) => { | ||||
|             this.loadTmpStylesForSiteConfig(data.config).catch((error) => { | ||||
|             this.loadTmpStyles(data.config).catch((error) => { | ||||
|                 this.logger.error('Error loading tmp styles', error); | ||||
|             }); | ||||
|         }); | ||||
| @ -131,20 +176,94 @@ export class AddonRemoteThemesProvider { | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Create a style element for a site. | ||||
|      * | ||||
|      * @param siteId Site Id. | ||||
|      * @param disabled Whether the element should be disabled. | ||||
|      */ | ||||
|     protected createStyleElements(siteId: string, disabled: boolean): void { | ||||
|         this.stylesEls[siteId] = {}; | ||||
| 
 | ||||
|         this.styleHandlers.forEach((handler) => { | ||||
| 
 | ||||
|             const styleElementId = this.getStyleId(siteId, handler.name); | ||||
| 
 | ||||
|             let styleEl: HTMLStyleElement | null = document.head.querySelector(`style#${styleElementId}`); | ||||
| 
 | ||||
|             if (!styleEl) { | ||||
|                 // Create the style and add it to the header.
 | ||||
|                 styleEl = document.createElement('style'); | ||||
| 
 | ||||
|                 styleEl.setAttribute('id', styleElementId); | ||||
|                 this.disableStyleElement(styleEl, disabled); | ||||
| 
 | ||||
|                 this.stylesEls[siteId][handler.name] = ''; | ||||
|                 document.head.appendChild(styleEl); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the content of an style element. | ||||
|      * | ||||
|      * @param siteId Site Id. | ||||
|      * @param handler Style handler. | ||||
|      * @param disabled Whether the element should be disabled. | ||||
|      * @param config Site public config. | ||||
|      * @return New element. | ||||
|      */ | ||||
|     protected async setStyle( | ||||
|         siteId: string, | ||||
|         handler: CoreStyleHandler, | ||||
|         disabled: boolean, | ||||
|         config?: CoreSitePublicConfigResponse, | ||||
|     ): Promise<void> { | ||||
|         let contents = ''; | ||||
| 
 | ||||
|         const enabled = await handler.isEnabled(siteId, config); | ||||
|         if (enabled) { | ||||
|             contents = (await handler.getStyle(siteId, config)).trim(); | ||||
|         } | ||||
| 
 | ||||
|         const hash = <string>Md5.hashAsciiStr(contents); | ||||
| 
 | ||||
|         // Update the styles only if they have changed.
 | ||||
|         if (this.stylesEls[siteId!][handler.name] === hash) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const styleElementId = this.getStyleId(siteId, handler.name); | ||||
| 
 | ||||
|         const styleEl: HTMLStyleElement | null = document.head.querySelector(`style#${styleElementId}`); | ||||
| 
 | ||||
|         if (!styleEl) { | ||||
|             this.stylesEls[siteId][handler.name] = ''; | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         styleEl.innerHTML = contents; | ||||
|         this.stylesEls[siteId][handler.name] = hash; | ||||
| 
 | ||||
|         // Adding styles to a style element automatically enables it. Disable it again if needed.
 | ||||
|         this.disableStyleElement(styleEl, disabled); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Add a style element for a site and load the styles for that element. The style will be disabled. | ||||
|      * | ||||
|      * @param siteId Site ID. | ||||
|      * @return Promise resolved when added and loaded. | ||||
|      */ | ||||
|     async addSite(siteId?: string): Promise<void> { | ||||
|     protected async addSite(siteId?: string): Promise<void> { | ||||
|         if (!siteId || this.stylesEls[siteId]) { | ||||
|             // Invalid site ID or style already added.
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Create the style and add it to the header.
 | ||||
|         this.initSiteStyleElement(siteId, true); | ||||
|         this.createStyleElements(siteId, true); | ||||
| 
 | ||||
|         try { | ||||
|             await this.load(siteId, true); | ||||
| @ -156,28 +275,47 @@ export class AddonRemoteThemesProvider { | ||||
|     /** | ||||
|      * Clear styles added to the DOM, disabling them all. | ||||
|      */ | ||||
|     clear(): void { | ||||
|     protected clear(): void { | ||||
|         let styles: HTMLStyleElement[] = []; | ||||
|         // Disable all the styles.
 | ||||
|         this.disableElementsBySelector('style[id*=mobilecssurl]'); | ||||
|         this.styleHandlers.forEach((handler) => { | ||||
|             styles = styles.concat(Array.from(document.querySelectorAll(`style[id*=${handler.name}]`))); | ||||
|         }); | ||||
| 
 | ||||
|         styles.forEach((style) => { | ||||
|             this.disableStyleElement(style, true); | ||||
|         }); | ||||
| 
 | ||||
|         // Set StatusBar properties.
 | ||||
|         CoreApp.setStatusBarColor(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Create a style element. | ||||
|      * Returns style element Id based on site and source. | ||||
|      * | ||||
|      * @param id ID to set to the element. | ||||
|      * @param disabled Whether the element should be disabled. | ||||
|      * @return New element. | ||||
|      * @param siteId Site Id. | ||||
|      * @param sourceName Source or handler name. | ||||
|      * @return Element Id. | ||||
|      */ | ||||
|     protected createStyleElement(id: string, disabled: boolean): HTMLStyleElement { | ||||
|         const styleEl = document.createElement('style'); | ||||
|     protected getStyleId(siteId: string, sourceName: string): string { | ||||
|         return `${sourceName}-${siteId}`; | ||||
|     } | ||||
| 
 | ||||
|         styleEl.setAttribute('id', id); | ||||
|         this.disableElement(styleEl, disabled); | ||||
|     /** | ||||
|      * Disabled an element based on site and source name. | ||||
|      * | ||||
|      * @param siteId Site Id. | ||||
|      * @param sourceName Source or handler name. | ||||
|      * @param disable Whether to disable or enable the element. | ||||
|      */ | ||||
|     protected disableStyleElementByName(siteId: string, sourceName: string, disable: boolean): void { | ||||
|         const styleElementId = this.getStyleId(siteId, sourceName); | ||||
| 
 | ||||
|         return styleEl; | ||||
|         const styleEl: HTMLStyleElement | null = document.head.querySelector(`style#${styleElementId}`); | ||||
| 
 | ||||
|         if (styleEl) { | ||||
|             this.disableStyleElement(styleEl, disable); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -186,7 +324,7 @@ export class AddonRemoteThemesProvider { | ||||
|      * @param element The element to enable or disable. | ||||
|      * @param disable Whether to disable or enable the element. | ||||
|      */ | ||||
|     disableElement(element: HTMLStyleElement, disable: boolean): void { | ||||
|     protected disableStyleElement(element: HTMLStyleElement, disable: boolean): void { | ||||
|         // Setting disabled should be enough, but we also set the attribute so it can be seen in the DOM which ones are disabled.
 | ||||
|         // Cast to any because the HTMLStyleElement type doesn't define the disabled attribute.
 | ||||
|         (<any> element).disabled = !!disable; // eslint-disable-line @typescript-eslint/no-explicit-any
 | ||||
| @ -195,135 +333,24 @@ export class AddonRemoteThemesProvider { | ||||
|             element.setAttribute('disabled', 'true'); | ||||
|         } else { | ||||
|             element.removeAttribute('disabled'); | ||||
| 
 | ||||
|             if (element.innerHTML != '') { | ||||
|                 CoreApp.setStatusBarColor(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Disable all the style elements based on a query selector. | ||||
|      * | ||||
|      * @param selector The selector to get the style elements. | ||||
|      */ | ||||
|     protected disableElementsBySelector(selector: string): void { | ||||
|         const styles = <HTMLStyleElement[]> Array.from(document.querySelectorAll(selector)); | ||||
| 
 | ||||
|         styles.forEach((style) => { | ||||
|             this.disableElement(style, true); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Downloads a CSS file and remove old files if needed. | ||||
|      * | ||||
|      * @param siteId Site ID. | ||||
|      * @param url File URL. | ||||
|      * @return Promise resolved when the file is downloaded. | ||||
|      */ | ||||
|     protected async downloadFileAndRemoveOld(siteId: string, url: string): Promise<string> { | ||||
| 
 | ||||
|         try { | ||||
|             // Check if the file is downloaded.
 | ||||
|             const state = await CoreFilepool.getFileStateByUrl(siteId, url); | ||||
| 
 | ||||
|             if (state == CoreConstants.NOT_DOWNLOADED) { | ||||
|                 // File not downloaded, URL has changed or first time. Delete downloaded CSS files.
 | ||||
|                 await CoreFilepool.removeFilesByComponent(siteId, AddonRemoteThemesProvider.COMPONENT, 1); | ||||
|             } | ||||
|         } catch { | ||||
|             // An error occurred while getting state (shouldn't happen). Don't delete downloaded file.
 | ||||
|         } | ||||
| 
 | ||||
|         return CoreFilepool.downloadUrl(siteId, url, false, AddonRemoteThemesProvider.COMPONENT, 1); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Enable the styles of a certain site. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      */ | ||||
|     enable(siteId?: string): void { | ||||
|     protected enableSiteStyles(siteId?: string): void { | ||||
|         siteId = siteId || CoreSites.getCurrentSiteId(); | ||||
| 
 | ||||
|         if (this.stylesEls[siteId]) { | ||||
|             this.disableElement(this.stylesEls[siteId].element, false); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get remote styles of a certain site. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the styles and the URL of the CSS file, | ||||
|      *         resolved with undefined if no styles to load. | ||||
|      */ | ||||
|     async get(siteId?: string): Promise<{fileUrl: string; styles: string} | undefined> { | ||||
|         siteId = siteId || CoreSites.getCurrentSiteId(); | ||||
| 
 | ||||
|         const site = await CoreSites.getSite(siteId); | ||||
|         const infos = site.getInfo(); | ||||
| 
 | ||||
|         if (!infos?.mobilecssurl) { | ||||
|             if (infos?.mobilecssurl === '') { | ||||
|                 // CSS URL is empty. Delete downloaded files (if any).
 | ||||
|                 CoreFilepool.removeFilesByComponent(siteId, AddonRemoteThemesProvider.COMPONENT, 1); | ||||
|             for (const sourceName in this.stylesEls[siteId]) { | ||||
|                 this.disableStyleElementByName(siteId, sourceName, false); | ||||
|             } | ||||
| 
 | ||||
|             return; | ||||
|             CoreApp.setStatusBarColor(); | ||||
|         } | ||||
| 
 | ||||
|         let fileUrl = infos.mobilecssurl; | ||||
| 
 | ||||
|         if (CoreFile.isAvailable()) { | ||||
|             // The file system is available. Download the file and remove old CSS files if needed.
 | ||||
|             fileUrl = await this.downloadFileAndRemoveOld(siteId, fileUrl); | ||||
|         } | ||||
| 
 | ||||
|         this.logger.debug('Loading styles from: ', fileUrl); | ||||
| 
 | ||||
|         // Get the CSS content using HTTP because we will treat the styles before saving them in the file.
 | ||||
|         const text = await CoreWS.getText(fileUrl); | ||||
| 
 | ||||
|         return { fileUrl, styles: this.get35Styles(text) }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the CSS code has a separator for 3.5 styles. If it does, get only the styles after the separator. | ||||
|      * | ||||
|      * @param cssCode The CSS code to check. | ||||
|      * @return The filtered styles. | ||||
|      */ | ||||
|     protected get35Styles(cssCode: string): string { | ||||
|         const separatorPos = cssCode.search(SEPARATOR_35); | ||||
|         if (separatorPos > -1) { | ||||
|             return cssCode.substr(separatorPos).replace(SEPARATOR_35, ''); | ||||
|         } | ||||
| 
 | ||||
|         return cssCode; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Init the style element for a site. | ||||
|      * | ||||
|      * @param siteId Site ID. | ||||
|      * @param disabled Whether the element should be disabled. | ||||
|      */ | ||||
|     protected initSiteStyleElement(siteId: string, disabled: boolean): void { | ||||
|         if (this.stylesEls[siteId]) { | ||||
|             // Already initialized, ignore.
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Create the style and add it to the header.
 | ||||
|         const styleEl = this.createStyleElement('mobilecssurl-' + siteId, disabled); | ||||
| 
 | ||||
|         document.head.appendChild(styleEl); | ||||
|         this.stylesEls[siteId] = { | ||||
|             element: styleEl, | ||||
|             hash: '', | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -333,67 +360,28 @@ export class AddonRemoteThemesProvider { | ||||
|      * @param disabled Whether loaded styles should be disabled. | ||||
|      * @return Promise resolved when styles are loaded. | ||||
|      */ | ||||
|     async load(siteId?: string, disabled?: boolean): Promise<void> { | ||||
|     protected async load(siteId?: string, disabled = false): Promise<void> { | ||||
|         siteId = siteId || CoreSites.getCurrentSiteId(); | ||||
|         disabled = !!disabled; | ||||
| 
 | ||||
|         if (!siteId || !this.stylesEls[siteId]) { | ||||
|             throw new CoreError('Cannot load remote styles, site not found: ${siteId}'); | ||||
|             throw new CoreError('Cannot load styles, site not found: ${siteId}'); | ||||
|         } | ||||
| 
 | ||||
|         this.logger.debug('Load site', siteId, disabled); | ||||
| 
 | ||||
|         // Enable or disable the styles.
 | ||||
|         this.disableElement(this.stylesEls[siteId].element, disabled); | ||||
| 
 | ||||
|         const data = await this.get(siteId); | ||||
| 
 | ||||
|         if (typeof data == 'undefined') { | ||||
|             // Nothing to load.
 | ||||
|             return; | ||||
|         for (const sourceName in this.stylesEls[siteId]) { | ||||
|             this.disableStyleElementByName(siteId, sourceName, disabled); | ||||
|         } | ||||
| 
 | ||||
|         const hash = <string> Md5.hashAsciiStr(data.styles); | ||||
|         await CoreUtils.allPromises(this.styleHandlers.map(async (handler) => { | ||||
|             await this.setStyle(siteId!, handler, !!disabled); | ||||
|         })); | ||||
| 
 | ||||
|         // Update the styles only if they have changed.
 | ||||
|         if (this.stylesEls[siteId].hash !== hash) { | ||||
|             this.stylesEls[siteId].element.innerHTML = data.styles; | ||||
|             this.stylesEls[siteId].hash = hash; | ||||
| 
 | ||||
|             // Adding styles to a style element automatically enables it. Disable it again.
 | ||||
|             if (disabled) { | ||||
|                 this.disableElement(this.stylesEls[siteId].element, true); | ||||
|             } else { | ||||
|                 // Set StatusBar properties.
 | ||||
|                 CoreApp.setStatusBarColor(); | ||||
|             } | ||||
|         if (!disabled) { | ||||
|             // Set StatusBar properties.
 | ||||
|             CoreApp.setStatusBarColor(); | ||||
|         } | ||||
| 
 | ||||
|         // Styles have been loaded, now treat the CSS.
 | ||||
|         CoreUtils.ignoreErrors( | ||||
|             CoreFilepool.treatCSSCode(siteId, data.fileUrl, data.styles, AddonRemoteThemesProvider.COMPONENT, 2), | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load styles for a temporary site. These styles aren't prefetched. | ||||
|      * | ||||
|      * @param url URL to get the styles from. | ||||
|      * @return Promise resolved when loaded. | ||||
|      */ | ||||
|     async loadTmpStyles(url?: string): Promise<void> { | ||||
|         if (!url) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         let text = await CoreWS.getText(url); | ||||
| 
 | ||||
|         text = this.get35Styles(text); | ||||
| 
 | ||||
|         this.initSiteStyleElement(TMP_SITE_ID, false); | ||||
|         this.stylesEls[TMP_SITE_ID].element.innerHTML = text; | ||||
| 
 | ||||
|         CoreApp.setStatusBarColor(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -402,8 +390,15 @@ export class AddonRemoteThemesProvider { | ||||
|      * @param config Site public config. | ||||
|      * @return Promise resolved when loaded. | ||||
|      */ | ||||
|     loadTmpStylesForSiteConfig(config: CoreSitePublicConfigResponse): Promise<void> { | ||||
|         return this.loadTmpStyles(config.mobilecssurl); | ||||
|     protected async loadTmpStyles(config: CoreSitePublicConfigResponse): Promise<void> { | ||||
|         // Create the style and add it to the header.
 | ||||
|         this.createStyleElements(CoreStylesService.TMP_SITE_ID, true); | ||||
| 
 | ||||
|         await CoreUtils.allPromises(this.styleHandlers.map(async (handler) => { | ||||
|             await this.setStyle(CoreStylesService.TMP_SITE_ID, handler, false, config); | ||||
|         })); | ||||
| 
 | ||||
|         CoreApp.setStatusBarColor(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -411,7 +406,7 @@ export class AddonRemoteThemesProvider { | ||||
|      * | ||||
|      * @return Promise resolved when loaded. | ||||
|      */ | ||||
|     async preloadCurrentSite(): Promise<void> { | ||||
|     protected async preloadCurrentSite(): Promise<void> { | ||||
|         const siteId = await CoreUtils.ignoreErrors(CoreSites.getStoredCurrentSiteId()); | ||||
| 
 | ||||
|         if (!siteId) { | ||||
| @ -427,7 +422,7 @@ export class AddonRemoteThemesProvider { | ||||
|      * | ||||
|      * @return Promise resolved when loaded. | ||||
|      */ | ||||
|     async preloadSites(): Promise<void> { | ||||
|     protected async preloadSites(): Promise<void> { | ||||
|         const ids = await CoreSites.getSitesIds(); | ||||
| 
 | ||||
|         await CoreUtils.allPromises(ids.map((siteId) => this.addSite(siteId))); | ||||
| @ -438,9 +433,17 @@ export class AddonRemoteThemesProvider { | ||||
|      * | ||||
|      * @param siteId Site ID. | ||||
|      */ | ||||
|     removeSite(siteId: string): void { | ||||
|     protected removeSite(siteId: string): void { | ||||
|         if (siteId && this.stylesEls[siteId]) { | ||||
|             document.head.removeChild(this.stylesEls[siteId].element); | ||||
|             for (const sourceName in this.stylesEls[siteId]) { | ||||
|                 const styleElementId = this.getStyleId(siteId, sourceName); | ||||
| 
 | ||||
|                 const styleEl: HTMLStyleElement | null = document.head.querySelector(`style#${styleElementId}`); | ||||
| 
 | ||||
|                 if (styleEl) { | ||||
|                     document.head.removeChild(styleEl); | ||||
|                 } | ||||
|             } | ||||
|             delete this.stylesEls[siteId]; | ||||
| 
 | ||||
|             CoreApp.setStatusBarColor(); | ||||
| @ -450,10 +453,10 @@ export class AddonRemoteThemesProvider { | ||||
|     /** | ||||
|      * Unload styles for a temporary site. | ||||
|      */ | ||||
|     unloadTmpStyles(): void { | ||||
|         return this.removeSite(TMP_SITE_ID); | ||||
|     protected unloadTmpStyles(): void { | ||||
|         return this.removeSite(CoreStylesService.TMP_SITE_ID); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export const AddonRemoteThemes = makeSingleton(AddonRemoteThemesProvider); | ||||
| export const CoreStyles = makeSingleton(CoreStylesService); | ||||
							
								
								
									
										34
									
								
								src/core/features/styles/styles.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/core/features/styles/styles.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,34 @@ | ||||
| // (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 { APP_INITIALIZER, NgModule, Type } from '@angular/core'; | ||||
| import { CoreStyles, CoreStylesService } from './services/styles'; | ||||
| 
 | ||||
| // List of providers (without handlers).
 | ||||
| export const CORE_STYLE_SERVICES: Type<unknown>[] = [ | ||||
|     CoreStylesService, | ||||
| ]; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     providers: [ | ||||
|         { | ||||
|             provide: APP_INITIALIZER, | ||||
|             multi: true, | ||||
|             useValue: async () => { | ||||
|                 await CoreStyles.initialize(); | ||||
|             }, | ||||
|         }, | ||||
|     ], | ||||
| }) | ||||
| export class CoreStylesModule {} | ||||
| @ -642,9 +642,12 @@ export class CoreAppProvider { | ||||
|             color = CoreColors.getColorHex(color); | ||||
|         } | ||||
| 
 | ||||
|         // Make darker on Android.
 | ||||
|         // Make darker on Android, except white.
 | ||||
|         if (this.isAndroid()) { | ||||
|             color = CoreColors.darker(color); | ||||
|             const rgb = CoreColors.hexToRGB(color); | ||||
|             if (rgb.red != 255 || rgb.green != 255 || rgb.blue != 255) { | ||||
|                 color = CoreColors.darker(color); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         this.logger.debug(`Set status bar color ${color}`); | ||||
|  | ||||
| @ -93,7 +93,7 @@ export class CoreColors { | ||||
|      * @param color Hexadec RGB Color. | ||||
|      * @return RGB color components. | ||||
|      */ | ||||
|     protected static hexToRGB(color: string): ColorComponents { | ||||
|     static hexToRGB(color: string): ColorComponents { | ||||
|         if (color.charAt(0) == '#') { | ||||
|             color = color.substr(1); | ||||
|         } | ||||
|  | ||||
| @ -111,15 +111,14 @@ | ||||
|     --core-header-toolbar-border-width: #{$toolbar-border-width}; | ||||
|     --core-header-toolbar-border-color: #{$toolbar-border-color}; | ||||
|     --core-header-toolbar-color: #{$toolbar-color}; | ||||
|     ion-header ion-toolbar, | ||||
|     ion-header.header-ios ion-toolbar:last-of-type { | ||||
|     ion-header ion-toolbar { | ||||
|         --color: var(--core-header-toolbar-color); | ||||
|         --background: var(--core-header-toolbar-background); | ||||
|         --border-width: 0 0 var(--core-header-toolbar-border-width) 0; | ||||
|         --border-color: var(--core-header-toolbar-border-color); | ||||
| 
 | ||||
|         ion-button { | ||||
|             --ion-toolbar-color: transparent; | ||||
|             --ion-toolbar-color: var(--core-header-toolbar-color); | ||||
|             --color: var(--core-header-toolbar-color); | ||||
|         } | ||||
| 
 | ||||
| @ -129,6 +128,10 @@ | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     ion-header.header-ios ion-toolbar:last-of-type { | ||||
|         --border-width: 0 0 var(--core-header-toolbar-border-width) 0; | ||||
|     } | ||||
| 
 | ||||
|     ion-searchbar { | ||||
|         --background: var(--ion-item-background); | ||||
|         .searchbar-input { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user