diff --git a/src/app/core/settings/pages/space-usage/space-usage.html b/src/app/core/settings/pages/space-usage/space-usage.html new file mode 100644 index 000000000..ad08d1c8d --- /dev/null +++ b/src/app/core/settings/pages/space-usage/space-usage.html @@ -0,0 +1,46 @@ + + + + + + {{ 'core.settings.spaceusage' | translate }} + + + + + + + + + + + + + + + + + + + {{ site.fullName }} + {{ site.siteUrl }} + + + {{ site.spaceUsage | coreBytesToSize }} + + + + + + + + {{ 'core.settings.total' | translate }} + + + {{ totals.spaceUsage | coreBytesToSize }} + + + + diff --git a/src/app/core/settings/pages/space-usage/space-usage.page.module.ts b/src/app/core/settings/pages/space-usage/space-usage.page.module.ts new file mode 100644 index 000000000..336081181 --- /dev/null +++ b/src/app/core/settings/pages/space-usage/space-usage.page.module.ts @@ -0,0 +1,49 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { RouterModule, Routes } from '@angular/router'; +import { CommonModule } from '@angular/common'; +import { IonicModule } from '@ionic/angular'; + +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; + +import { CoreSettingsSpaceUsagePage } from './space-usage.page'; + +const routes: Routes = [ + { + path: '', + component: CoreSettingsSpaceUsagePage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + ], + declarations: [ + CoreSettingsSpaceUsagePage, + ], + exports: [RouterModule], +}) +export class CoreSettingsSpaceUsagePageModule {} diff --git a/src/app/core/settings/pages/space-usage/space-usage.page.ts b/src/app/core/settings/pages/space-usage/space-usage.page.ts new file mode 100644 index 000000000..8d37f95a5 --- /dev/null +++ b/src/app/core/settings/pages/space-usage/space-usage.page.ts @@ -0,0 +1,157 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { IonRefresher } from '@ionic/angular'; + +import { CoreSiteBasicInfo, CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { Translate } from '@singletons/core.singletons'; +import { CoreEventObserver, CoreEvents, CoreEventSiteUpdatedData } from '@singletons/events'; + +import { CoreSettingsHelper, CoreSiteSpaceUsage } from '../../services/settings.helper'; + +/** + * Page that displays the space usage settings. + */ +@Component({ + selector: 'page-core-app-settings-space-usage', + templateUrl: 'space-usage.html', +}) +export class CoreSettingsSpaceUsagePage implements OnInit, OnDestroy { + + loaded = false; + sites: CoreSiteBasicInfoWithUsage[] = []; + currentSiteId = ''; + totals: CoreSiteSpaceUsage = { + cacheEntries: 0, + spaceUsage: 0, + }; + + protected sitesObserver: CoreEventObserver; + + constructor() { + this.currentSiteId = CoreSites.instance.getCurrentSiteId(); + + this.sitesObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, async (data: CoreEventSiteUpdatedData) => { + const site = await CoreSites.instance.getSite(data.siteId); + + const siteEntry = this.sites.find((siteEntry) => siteEntry.id == site.id); + if (siteEntry) { + const siteInfo = site.getInfo(); + + siteEntry.siteName = site.getSiteName(); + + if (siteInfo) { + siteEntry.siteUrl = siteInfo.siteurl; + siteEntry.fullName = siteInfo.fullname; + } + } + }); + } + + /** + * View loaded. + */ + ngOnInit(): void { + this.loadSiteData().finally(() => { + this.loaded = true; + }); + } + + /** + * Convenience function to load site data/usage and calculate the totals. + * + * @return Resolved when done. + */ + protected async loadSiteData(): Promise { + // Calculate total usage. + let totalSize = 0; + let totalEntries = 0; + + this.sites = await CoreSites.instance.getSortedSites(); + + const settingsHelper = CoreSettingsHelper.instance; + + // Get space usage. + await Promise.all(this.sites.map(async (site) => { + const siteInfo = await settingsHelper.getSiteSpaceUsage(site.id); + + site.cacheEntries = siteInfo.cacheEntries; + site.spaceUsage = siteInfo.spaceUsage; + + totalSize += site.spaceUsage || 0; + totalEntries += site.cacheEntries || 0; + })); + + this.totals.spaceUsage = totalSize; + this.totals.cacheEntries = totalEntries; + } + + /** + * Refresh the data. + * + * @param event Refresher event. + */ + refreshData(event?: CustomEvent): void { + this.loadSiteData().finally(() => { + event?.detail.complete(); + }); + } + + /** + * Deletes files of a site and the tables that can be cleared. + * + * @param siteData Site object with space usage. + */ + async deleteSiteStorage(siteData: CoreSiteBasicInfoWithUsage): Promise { + try { + const newInfo = await CoreSettingsHelper.instance.deleteSiteStorage(siteData.siteName || '', siteData.id); + + this.totals.spaceUsage -= siteData.spaceUsage! - newInfo.spaceUsage; + this.totals.spaceUsage -= siteData.cacheEntries! - newInfo.cacheEntries; + + siteData.spaceUsage = newInfo.spaceUsage; + siteData.cacheEntries = newInfo.cacheEntries; + } catch { + // Ignore cancelled confirmation modal. + } + } + + /** + * Show information about space usage actions. + */ + showInfo(): void { + CoreDomUtils.instance.showAlert( + Translate.instance.instant('core.help'), + Translate.instance.instant('core.settings.spaceusagehelp'), + ); + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.sitesObserver?.off(); + } + +} + +/** + * Basic site info with space usage and cache entries that can be erased. + */ +export interface CoreSiteBasicInfoWithUsage extends CoreSiteBasicInfo { + cacheEntries?: number; // Number of cached entries that can be cleared. + spaceUsage?: number; // Space used in this site. +} diff --git a/src/app/core/settings/services/settings.helper.ts b/src/app/core/settings/services/settings.helper.ts index 840300527..736ac57a2 100644 --- a/src/app/core/settings/services/settings.helper.ts +++ b/src/app/core/settings/services/settings.helper.ts @@ -31,8 +31,8 @@ import { makeSingleton, Translate } from '@singletons/core.singletons'; * Object with space usage and cache entries that can be erased. */ export interface CoreSiteSpaceUsage { - cacheEntries?: number; // Number of cached entries that can be cleared. - spaceUsage?: number; // Space used in this site (total files + estimate of cache). + cacheEntries: number; // Number of cached entries that can be cleared. + spaceUsage: number; // Space used in this site (total files + estimate of cache). } /** diff --git a/src/app/core/settings/settings-routing.module.ts b/src/app/core/settings/settings-routing.module.ts index aa46c4a83..66bb7de18 100644 --- a/src/app/core/settings/settings-routing.module.ts +++ b/src/app/core/settings/settings-routing.module.ts @@ -24,6 +24,12 @@ const routes: Routes = [ path: 'general', loadChildren: () => import('./pages/general/general.page.module').then( m => m.CoreSettingsGeneralPageModule), }, + { + path: 'spaceusage', + loadChildren: () => + import('@core/settings/pages/space-usage/space-usage.page.module') + .then(m => m.CoreSettingsSpaceUsagePageModule), + }, { path: '', loadChildren: () => import('./pages/app/app.page.module').then( m => m.CoreSettingsAppPageModule), diff --git a/src/app/pipes/bytes-to-size.pipe.ts b/src/app/pipes/bytes-to-size.pipe.ts new file mode 100644 index 000000000..97d18678b --- /dev/null +++ b/src/app/pipes/bytes-to-size.pipe.ts @@ -0,0 +1,55 @@ +// (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 { Pipe, PipeTransform } from '@angular/core'; + +import { CoreLogger } from '@singletons/logger'; +import { CoreTextUtils } from '@services/utils/text'; + +/** + * Pipe to turn a number in bytes to a human readable size (e.g. 5,25 MB). + */ +@Pipe({ + name: 'coreBytesToSize', +}) +export class CoreBytesToSizePipe implements PipeTransform { + + protected logger: CoreLogger; + + constructor() { + this.logger = CoreLogger.getInstance('CoreBytesToSizePipe'); + } + + /** + * Takes a number and turns it to a human readable size. + * + * @param value The bytes to convert. + * @return Readable bytes. + */ + transform(value: number | string): string { + if (typeof value == 'string') { + // Convert the value to a number. + const numberValue = parseInt(value, 10); + if (isNaN(numberValue)) { + this.logger.error('Invalid value received', value); + + return value; + } + value = numberValue; + } + + return CoreTextUtils.instance.bytesToSize(value); + } + +} diff --git a/src/app/pipes/pipes.module.ts b/src/app/pipes/pipes.module.ts index 4108c1255..492bf7e08 100644 --- a/src/app/pipes/pipes.module.ts +++ b/src/app/pipes/pipes.module.ts @@ -17,6 +17,7 @@ import { CoreCreateLinksPipe } from './create-links.pipe'; import { CoreFormatDatePipe } from './format-date.pipe'; import { CoreNoTagsPipe } from './no-tags.pipe'; import { CoreTimeAgoPipe } from './time-ago.pipe'; +import { CoreBytesToSizePipe } from './bytes-to-size.pipe'; @NgModule({ declarations: [ @@ -24,6 +25,7 @@ import { CoreTimeAgoPipe } from './time-ago.pipe'; CoreNoTagsPipe, CoreTimeAgoPipe, CoreFormatDatePipe, + CoreBytesToSizePipe, ], imports: [], exports: [ @@ -31,6 +33,7 @@ import { CoreTimeAgoPipe } from './time-ago.pipe'; CoreNoTagsPipe, CoreTimeAgoPipe, CoreFormatDatePipe, + CoreBytesToSizePipe, ], }) export class CorePipesModule {} diff --git a/src/app/services/sites.ts b/src/app/services/sites.ts index 7bcccfc8b..665798c64 100644 --- a/src/app/services/sites.ts +++ b/src/app/services/sites.ts @@ -1038,7 +1038,7 @@ export class CoreSitesProvider { id: site.id, siteUrl: site.siteUrl, fullName: siteInfo?.fullname, - siteName: CoreConstants.CONFIG.sitename ?? siteInfo?.sitename, + siteName: CoreConstants.CONFIG.sitename == '' ? siteInfo?.sitename: CoreConstants.CONFIG.sitename, avatar: siteInfo?.userpictureurl, siteHomeId: siteInfo?.siteid || 1, }; @@ -1055,32 +1055,32 @@ export class CoreSitesProvider { * @param ids IDs of the sites to get. If not defined, return all sites. * @return Promise resolved when the sites are retrieved. */ - getSortedSites(ids?: string[]): Promise { - return this.getSites(ids).then((sites) => { - // Sort sites by url and ful lname. - sites.sort((a, b) => { - // First compare by site url without the protocol. - const urlA = a.siteUrl.replace(/^https?:\/\//, '').toLowerCase(); - const urlB = b.siteUrl.replace(/^https?:\/\//, '').toLowerCase(); - const compare = urlA.localeCompare(urlB); + async getSortedSites(ids?: string[]): Promise { + const sites = await this.getSites(ids); - if (compare !== 0) { - return compare; - } + // Sort sites by url and ful lname. + sites.sort((a, b) => { + // First compare by site url without the protocol. + const urlA = a.siteUrl.replace(/^https?:\/\//, '').toLowerCase(); + const urlB = b.siteUrl.replace(/^https?:\/\//, '').toLowerCase(); + const compare = urlA.localeCompare(urlB); - // If site url is the same, use fullname instead. - const fullNameA = a.fullName?.toLowerCase().trim(); - const fullNameB = b.fullName?.toLowerCase().trim(); + if (compare !== 0) { + return compare; + } - if (!fullNameA || !fullNameB) { - return 0; - } + // If site url is the same, use fullname instead. + const fullNameA = a.fullName?.toLowerCase().trim(); + const fullNameB = b.fullName?.toLowerCase().trim(); - return fullNameA.localeCompare(fullNameB); - }); + if (!fullNameA || !fullNameB) { + return 0; + } - return sites; + return fullNameA.localeCompare(fullNameB); }); + + return sites; } /** diff --git a/src/app/singletons/events.ts b/src/app/singletons/events.ts index 4bc192fa3..eeb8f554c 100644 --- a/src/app/singletons/events.ts +++ b/src/app/singletons/events.ts @@ -16,6 +16,7 @@ import { Params } from '@angular/router'; import { Subject } from 'rxjs'; import { CoreLogger } from '@singletons/logger'; +import { CoreSiteInfoResponse } from '@classes/site'; /** * Observer instance to stop listening to an event. @@ -192,13 +193,24 @@ export class CoreEvents { } +/** + * Some events contains siteId added by the trigger function. This type is intended to be combined with others. + */ +export type CoreEventSiteData = { + siteId?: string; +}; + +/** + * Data passed to SITE_UPDATED event. + */ +export type CoreEventSiteUpdatedData = CoreEventSiteData & CoreSiteInfoResponse; + /** * Data passed to SESSION_EXPIRED event. */ -export type CoreEventSessionExpiredData = { +export type CoreEventSessionExpiredData = CoreEventSiteData & { pageName?: string; params?: Params; - siteId?: string; }; /** diff --git a/src/theme/app.scss b/src/theme/app.scss index 3995d3bef..1ad9fce1b 100644 --- a/src/theme/app.scss +++ b/src/theme/app.scss @@ -105,6 +105,13 @@ ion-list.list-md { font-size: 14px; } +// Item styles +.item.core-selected-item { + // TODO: Add safe are to border and RTL + border-inline-start: var(--selected-item-border-width) solid var(--selected-item-color); + --ion-safe-area-left: calc(-1 * var(--selected-item-border-width)); +} + // Avatar // ------------------------- // Large centered avatar diff --git a/src/theme/variables.scss b/src/theme/variables.scss index e7f6acc4d..c9e862cbd 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -131,6 +131,9 @@ } } + --selected-item-color: var(--custom-selected-item-color, var(--core-color)); + --selected-item-border-width: var(--custom-selected-item-border-width, 5px); + --drop-shadow: var(--custom-drop-shadow, 0, 0, 0, 0.2); --core-login-background: var(--custom-login-background, var(--white));
{{ site.fullName }}
{{ site.siteUrl }}
+ {{ site.spaceUsage | coreBytesToSize }} +
+ {{ totals.spaceUsage | coreBytesToSize }} +