2020-11-04 17:37:02 +01:00
|
|
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
|
|
|
import { Injectable } from '@angular/core';
|
|
|
|
import { CoreApp } from '@services/app';
|
2020-12-09 16:42:21 +01:00
|
|
|
import { CoreCronDelegate } from '@services/cron';
|
2020-11-04 17:37:02 +01:00
|
|
|
import { CoreEvents } from '@singletons/events';
|
|
|
|
import { CoreFilepool } from '@services/filepool';
|
|
|
|
import { CoreSite } from '@classes/site';
|
|
|
|
import { CoreSites } from '@services/sites';
|
|
|
|
import { CoreUtils } from '@services/utils/utils';
|
2020-11-19 12:40:18 +01:00
|
|
|
import { CoreConstants } from '@/core/constants';
|
2020-11-04 17:37:02 +01:00
|
|
|
import { CoreConfig } from '@services/config';
|
2020-12-10 16:16:55 +01:00
|
|
|
import { CoreFilter } from '@features/filter/services/filter';
|
2020-11-04 17:37:02 +01:00
|
|
|
import { CoreDomUtils } from '@services/utils/dom';
|
2020-11-20 12:08:30 +01:00
|
|
|
import { CoreCourse } from '@features/course/services/course';
|
2020-11-24 09:31:11 +01:00
|
|
|
import { makeSingleton, Translate } from '@singletons';
|
2020-11-12 10:09:32 +01:00
|
|
|
import { CoreError } from '@classes/errors/error';
|
2020-11-04 17:37:02 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Object with space usage and cache entries that can be erased.
|
|
|
|
*/
|
|
|
|
export interface CoreSiteSpaceUsage {
|
2020-11-12 09:53:56 +01:00
|
|
|
cacheEntries: number; // Number of cached entries that can be cleared.
|
|
|
|
spaceUsage: number; // Space used in this site (total files + estimate of cache).
|
2020-11-04 17:37:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Constants to define color schemes.
|
|
|
|
*/
|
|
|
|
export const enum CoreColorScheme {
|
|
|
|
AUTO = 'auto',
|
|
|
|
LIGHT = 'light',
|
|
|
|
DARK = 'dark',
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Settings helper service.
|
|
|
|
*/
|
2020-12-09 14:38:53 +01:00
|
|
|
@Injectable({ providedIn: 'root' })
|
2020-11-04 17:37:02 +01:00
|
|
|
export class CoreSettingsHelperProvider {
|
|
|
|
|
|
|
|
protected syncPromises: { [s: string]: Promise<void> } = {};
|
|
|
|
protected prefersDark?: MediaQueryList;
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
if (!CoreConstants.CONFIG.forceColorScheme) {
|
|
|
|
// Update color scheme when a user enters or leaves a site, or when the site info is updated.
|
|
|
|
const applySiteScheme = (): void => {
|
|
|
|
if (this.isColorSchemeDisabledInSite()) {
|
|
|
|
// Dark mode is disabled, force light mode.
|
|
|
|
this.setColorScheme(CoreColorScheme.LIGHT);
|
|
|
|
} else {
|
|
|
|
this.prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
|
|
|
|
// Reset color scheme settings.
|
|
|
|
this.initColorScheme();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
CoreEvents.on(CoreEvents.LOGIN, applySiteScheme.bind(this));
|
|
|
|
|
|
|
|
CoreEvents.on(CoreEvents.SITE_UPDATED, applySiteScheme.bind(this));
|
|
|
|
|
|
|
|
CoreEvents.on(CoreEvents.LOGOUT, () => {
|
|
|
|
// Reset color scheme settings.
|
|
|
|
this.initColorScheme();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Deletes files of a site and the tables that can be cleared.
|
|
|
|
*
|
|
|
|
* @param siteName Site Name.
|
|
|
|
* @param siteId: Site ID.
|
|
|
|
* @return Resolved with detailed new info when done.
|
|
|
|
*/
|
|
|
|
async deleteSiteStorage(siteName: string, siteId: string): Promise<CoreSiteSpaceUsage> {
|
|
|
|
const siteInfo: CoreSiteSpaceUsage = {
|
|
|
|
cacheEntries: 0,
|
|
|
|
spaceUsage: 0,
|
|
|
|
};
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
siteName = await CoreFilter.formatText(siteName, { clean: true, singleLine: true, filter: false }, [], siteId);
|
2020-11-04 17:37:02 +01:00
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
const title = Translate.instant('core.settings.deletesitefilestitle');
|
|
|
|
const message = Translate.instant('core.settings.deletesitefiles', { sitename: siteName });
|
2020-11-04 17:37:02 +01:00
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
await CoreDomUtils.showConfirm(message, title);
|
2020-11-04 17:37:02 +01:00
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
const site = await CoreSites.getSite(siteId);
|
2020-11-04 17:37:02 +01:00
|
|
|
|
|
|
|
// Clear cache tables.
|
2021-03-02 11:41:04 +01:00
|
|
|
const cleanSchemas = CoreSites.getSiteTableSchemasToClear(site);
|
2020-11-04 17:37:02 +01:00
|
|
|
const promises: Promise<number | void>[] = cleanSchemas.map((name) => site.getDb().deleteRecords(name));
|
|
|
|
const filepoolService = CoreFilepool.instance;
|
|
|
|
|
|
|
|
|
|
|
|
promises.push(site.deleteFolder().then(() => {
|
|
|
|
filepoolService.clearAllPackagesStatus(siteId);
|
|
|
|
filepoolService.clearFilepool(siteId);
|
2021-03-02 11:41:04 +01:00
|
|
|
CoreCourse.clearAllCoursesStatus(siteId);
|
2020-11-04 17:37:02 +01:00
|
|
|
|
|
|
|
siteInfo.spaceUsage = 0;
|
|
|
|
|
|
|
|
return;
|
|
|
|
}).catch(async (error) => {
|
|
|
|
if (error && error.code === FileError.NOT_FOUND_ERR) {
|
|
|
|
// Not found, set size 0.
|
|
|
|
filepoolService.clearAllPackagesStatus(siteId);
|
|
|
|
siteInfo.spaceUsage = 0;
|
|
|
|
} else {
|
|
|
|
// Error, recalculate the site usage.
|
2021-03-02 11:41:04 +01:00
|
|
|
CoreDomUtils.showErrorModal('core.settings.errordeletesitefiles', true);
|
2020-11-04 17:37:02 +01:00
|
|
|
|
|
|
|
siteInfo.spaceUsage = await site.getSpaceUsage();
|
|
|
|
}
|
|
|
|
}).then(async () => {
|
|
|
|
CoreEvents.trigger(CoreEvents.SITE_STORAGE_DELETED, {}, siteId);
|
|
|
|
|
|
|
|
siteInfo.cacheEntries = await this.calcSiteClearRows(site);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}));
|
|
|
|
|
|
|
|
await Promise.all(promises);
|
|
|
|
|
|
|
|
return siteInfo;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Calculates each site's usage, and the total usage.
|
|
|
|
*
|
|
|
|
* @param siteId ID of the site. Current site if undefined.
|
|
|
|
* @return Resolved with detailed info when done.
|
|
|
|
*/
|
|
|
|
async getSiteSpaceUsage(siteId?: string): Promise<CoreSiteSpaceUsage> {
|
2021-03-02 11:41:04 +01:00
|
|
|
const site = await CoreSites.getSite(siteId);
|
2020-11-04 17:37:02 +01:00
|
|
|
|
|
|
|
// Get space usage.
|
|
|
|
const siteInfo: CoreSiteSpaceUsage = {
|
|
|
|
cacheEntries: 0,
|
|
|
|
spaceUsage: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
siteInfo.cacheEntries = await this.calcSiteClearRows(site);
|
|
|
|
siteInfo.spaceUsage = await site.getTotalUsage();
|
|
|
|
|
|
|
|
return siteInfo;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Calculate the number of rows to be deleted on a site.
|
|
|
|
*
|
|
|
|
* @param site Site object.
|
|
|
|
* @return If there are rows to delete or not.
|
|
|
|
*/
|
|
|
|
protected async calcSiteClearRows(site: CoreSite): Promise<number> {
|
2021-03-02 11:41:04 +01:00
|
|
|
const clearTables = CoreSites.getSiteTableSchemasToClear(site);
|
2020-11-04 17:37:02 +01:00
|
|
|
|
|
|
|
let totalEntries = 0;
|
|
|
|
|
|
|
|
await Promise.all(clearTables.map(async (name) =>
|
|
|
|
totalEntries = await site.getDb().countRecords(name) + totalEntries));
|
|
|
|
|
|
|
|
return totalEntries;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a certain processor from a list of processors.
|
|
|
|
*
|
|
|
|
* @param processors List of processors.
|
|
|
|
* @param name Name of the processor to get.
|
|
|
|
* @param fallback True to return first processor if not found, false to not return any. Defaults to true.
|
|
|
|
* @return Processor.
|
2021-01-12 10:38:34 +01:00
|
|
|
* @deprecated since 3.9.5. This function has been moved to AddonNotificationsHelperProvider.
|
2020-11-04 17:37:02 +01:00
|
|
|
*/
|
2021-01-12 10:38:34 +01:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
|
|
getProcessor(processors: unknown[], name: string, fallback: boolean = true): undefined {
|
|
|
|
return;
|
2020-11-04 17:37:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the components and notifications that have a certain processor.
|
|
|
|
*
|
2021-01-12 10:38:34 +01:00
|
|
|
* @param processorName Name of the processor to filter.
|
2020-11-04 17:37:02 +01:00
|
|
|
* @param components Array of components.
|
|
|
|
* @return Filtered components.
|
2021-01-12 10:38:34 +01:00
|
|
|
* @deprecated since 3.9.5. This function has been moved to AddonNotificationsHelperProvider.
|
2020-11-04 17:37:02 +01:00
|
|
|
*/
|
2021-01-12 10:38:34 +01:00
|
|
|
getProcessorComponents(processorName: string, components: unknown[]): unknown[] {
|
|
|
|
return components;
|
2020-11-04 17:37:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the synchronization promise of a site.
|
|
|
|
*
|
|
|
|
* @param siteId ID of the site.
|
|
|
|
* @return Sync promise or null if site is not being syncrhonized.
|
|
|
|
*/
|
2020-11-12 09:44:44 +01:00
|
|
|
getSiteSyncPromise(siteId: string): Promise<void> | void {
|
2020-11-04 17:37:02 +01:00
|
|
|
if (this.syncPromises[siteId]) {
|
|
|
|
return this.syncPromises[siteId];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Synchronize a site.
|
|
|
|
*
|
|
|
|
* @param syncOnlyOnWifi True to sync only on wifi, false otherwise.
|
|
|
|
* @param siteId ID of the site to synchronize.
|
|
|
|
* @return Promise resolved when synchronized, rejected if failure.
|
|
|
|
*/
|
|
|
|
async synchronizeSite(syncOnlyOnWifi: boolean, siteId: string): Promise<void> {
|
|
|
|
if (this.syncPromises[siteId]) {
|
|
|
|
// There's already a sync ongoing for this site, return the promise.
|
|
|
|
return this.syncPromises[siteId];
|
|
|
|
}
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
const hasSyncHandlers = CoreCronDelegate.hasManualSyncHandlers();
|
2020-11-04 17:37:02 +01:00
|
|
|
|
|
|
|
if (site.isLoggedOut()) {
|
|
|
|
// Cannot sync logged out sites.
|
2021-03-02 11:41:04 +01:00
|
|
|
throw new CoreError(Translate.instant('core.settings.cannotsyncloggedout'));
|
|
|
|
} else if (hasSyncHandlers && !CoreApp.isOnline()) {
|
2020-11-04 17:37:02 +01:00
|
|
|
// We need connection to execute sync.
|
2021-03-02 11:41:04 +01:00
|
|
|
throw new CoreError(Translate.instant('core.settings.cannotsyncoffline'));
|
|
|
|
} else if (hasSyncHandlers && syncOnlyOnWifi && CoreApp.isNetworkAccessLimited()) {
|
|
|
|
throw new CoreError(Translate.instant('core.settings.cannotsyncwithoutwifi'));
|
2020-11-04 17:37:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const syncPromise = Promise.all([
|
|
|
|
// Invalidate all the site files so they are re-downloaded.
|
2021-03-02 11:41:04 +01:00
|
|
|
CoreUtils.ignoreErrors(CoreFilepool.invalidateAllFiles(siteId)),
|
2020-11-04 17:37:02 +01:00
|
|
|
// Invalidate and synchronize site data.
|
|
|
|
site.invalidateWsCache(),
|
|
|
|
this.checkSiteLocalMobile(site),
|
2021-03-02 11:41:04 +01:00
|
|
|
CoreSites.updateSiteInfo(site.getId()),
|
|
|
|
CoreCronDelegate.forceSyncExecution(site.getId()),
|
2020-11-04 17:37:02 +01:00
|
|
|
// eslint-disable-next-line arrow-body-style
|
|
|
|
]).then(() => {
|
|
|
|
return;
|
|
|
|
});
|
|
|
|
|
|
|
|
this.syncPromises[siteId] = syncPromise;
|
|
|
|
|
|
|
|
try {
|
|
|
|
await syncPromise;
|
|
|
|
} finally {
|
|
|
|
delete this.syncPromises[siteId];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if local_mobile was added to the site.
|
|
|
|
*
|
|
|
|
* @param site Site to check.
|
|
|
|
* @return Promise resolved if no action needed.
|
|
|
|
*/
|
|
|
|
protected async checkSiteLocalMobile(site: CoreSite): Promise<void> {
|
|
|
|
try {
|
|
|
|
// Check if local_mobile was installed in Moodle.
|
|
|
|
await site.checkIfLocalMobileInstalledAndNotUsed();
|
|
|
|
} catch {
|
|
|
|
// Not added, nothing to do.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Local mobile was added. Throw invalid session to force reconnect and create a new token.
|
|
|
|
CoreEvents.trigger(CoreEvents.SESSION_EXPIRED, {}, site.getId());
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
throw new CoreError(Translate.instant('core.lostconnection'));
|
2020-11-04 17:37:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Init Settings related to DOM.
|
|
|
|
*/
|
|
|
|
async initDomSettings(): Promise<void> {
|
|
|
|
// Set the font size based on user preference.
|
2021-03-02 11:41:04 +01:00
|
|
|
const fontSize = await CoreConfig.get(CoreConstants.SETTINGS_FONT_SIZE, CoreConstants.CONFIG.font_sizes[0]);
|
2020-11-04 17:37:02 +01:00
|
|
|
this.setFontSize(fontSize);
|
|
|
|
|
|
|
|
this.initColorScheme();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Init the color scheme.
|
|
|
|
*/
|
|
|
|
async initColorScheme(): Promise<void> {
|
|
|
|
if (CoreConstants.CONFIG.forceColorScheme) {
|
|
|
|
this.setColorScheme(CoreConstants.CONFIG.forceColorScheme);
|
|
|
|
} else {
|
2021-03-02 11:41:04 +01:00
|
|
|
const scheme = await CoreConfig.get(CoreConstants.SETTINGS_COLOR_SCHEME, CoreColorScheme.LIGHT);
|
2020-11-04 17:37:02 +01:00
|
|
|
this.setColorScheme(scheme);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if color scheme is disabled in a site.
|
|
|
|
*
|
|
|
|
* @param siteId Site ID. If not defined, current site.
|
|
|
|
* @return Promise resolved with whether color scheme is disabled.
|
|
|
|
*/
|
|
|
|
async isColorSchemeDisabled(siteId?: string): Promise<boolean> {
|
2021-03-02 11:41:04 +01:00
|
|
|
const site = await CoreSites.getSite(siteId);
|
2020-11-04 17:37:02 +01:00
|
|
|
|
|
|
|
return this.isColorSchemeDisabledInSite(site);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if color scheme is disabled in a site.
|
|
|
|
*
|
|
|
|
* @param site Site instance. If not defined, current site.
|
|
|
|
* @return Whether color scheme is disabled.
|
|
|
|
*/
|
|
|
|
isColorSchemeDisabledInSite(site?: CoreSite): boolean {
|
2021-03-02 11:41:04 +01:00
|
|
|
site = site || CoreSites.getCurrentSite();
|
2020-11-04 17:37:02 +01:00
|
|
|
|
|
|
|
return site ? site.isFeatureDisabled('NoDelegate_DarkMode') : false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set document default font size.
|
|
|
|
*
|
|
|
|
* @param fontSize Font size in percentage.
|
|
|
|
*/
|
|
|
|
setFontSize(fontSize: number): void {
|
|
|
|
// @todo Since zoom is deprecated and fontSize is not working, we should do some research here.
|
|
|
|
document.documentElement.style.zoom = fontSize + '%';
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set body color scheme.
|
|
|
|
*
|
|
|
|
* @param colorScheme Name of the color scheme.
|
|
|
|
*/
|
|
|
|
setColorScheme(colorScheme: CoreColorScheme): void {
|
|
|
|
if (colorScheme == CoreColorScheme.AUTO && this.prefersDark) {
|
|
|
|
// Listen for changes to the prefers-color-scheme media query.
|
|
|
|
this.prefersDark.addEventListener('change', this.toggleDarkModeListener);
|
|
|
|
|
|
|
|
this.toggleDarkMode(this.prefersDark.matches);
|
|
|
|
} else {
|
|
|
|
// Stop listening to changes.
|
|
|
|
this.prefersDark?.removeEventListener('change', this.toggleDarkModeListener);
|
|
|
|
|
|
|
|
this.toggleDarkMode(colorScheme == CoreColorScheme.DARK);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Listener function to toggle dark mode.
|
|
|
|
*
|
|
|
|
* @param e Event object.
|
|
|
|
*/
|
|
|
|
protected toggleDarkModeListener = (e: MediaQueryListEvent): void => {
|
|
|
|
document.body.classList.toggle('dark', e.matches);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Toggles dark mode based on enabled boolean.
|
|
|
|
*
|
|
|
|
* @param enable True to enable dark mode, false to disable.
|
|
|
|
*/
|
|
|
|
protected toggleDarkMode(enable: boolean = false): void {
|
|
|
|
document.body.classList.toggle('dark', enable);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
export const CoreSettingsHelper = makeSingleton(CoreSettingsHelperProvider);
|