MOBILE-3565 settings: Add space usage page

main
Pau Ferrer Ocaña 2020-11-12 09:53:56 +01:00
parent a9e8213026
commit d5e95ccd89
11 changed files with 363 additions and 25 deletions

View File

@ -0,0 +1,46 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>{{ 'core.settings.spaceusage' | translate }}</ion-title>
<ion-buttons slot="end">
<!-- @todo <core-navbar-buttons></core-navbar-buttons>-->
<ion-button (click)="showInfo()" [attr.aria-label]="'core.info' | translate">
<ion-icon name="fas-info-circle" slot="icon-only"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher [disabled]="!loaded" (ionRefresh)="refreshData($event)" slot="fixed">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
<ion-item *ngFor="let site of sites" [class.core-selected-item]="site.id == currentSiteId">
<ion-label class="ion-text-wrap">
<h2>
<core-format-text [text]="site.siteName" clean="true" [siteId]="site.id"></core-format-text>
</h2>
<p class="ion-text-wrap">{{ site.fullName }}</p>
<p>{{ site.siteUrl }}</p>
</ion-label>
<p *ngIf="site.spaceUsage != null" slot="end">
{{ site.spaceUsage | coreBytesToSize }}
</p>
<ion-button fill="clear" color="danger" slot="end" (click)="deleteSiteStorage(site)"
[hidden]="site.spaceUsage! + site.cacheEntries! <= 0"
[attr.aria-label]="'core.settings.deletesitefilestitle' | translate">
<ion-icon name="fas-trash" slot="icon-only"></ion-icon>
</ion-button>
</ion-item>
<ion-item-divider>
<ion-label>
<h2>{{ 'core.settings.total' | translate }}</h2>
</ion-label>
<p slot="end" class="ion-margin-end">
{{ totals.spaceUsage | coreBytesToSize }}
</p>
</ion-item-divider>
</core-loading>
</ion-content>

View File

@ -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 {}

View File

@ -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<void> {
// 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<IonRefresher>): 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<void> {
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.
}

View File

@ -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).
}
/**

View File

@ -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),

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -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,8 +1055,9 @@ 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<CoreSiteBasicInfo[]> {
return this.getSites(ids).then((sites) => {
async getSortedSites(ids?: string[]): Promise<CoreSiteBasicInfo[]> {
const sites = await this.getSites(ids);
// Sort sites by url and ful lname.
sites.sort((a, b) => {
// First compare by site url without the protocol.
@ -1080,7 +1081,6 @@ export class CoreSitesProvider {
});
return sites;
});
}
/**

View File

@ -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;
};
/**

View File

@ -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

View File

@ -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));