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);
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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);
|
|
@ -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…
Reference in New Issue