Merge pull request #1856 from sammarshallou/MOBILE-2905

MOBILE-2905 Allow user to manage storage within course
main
Juan Leyva 2019-04-29 17:56:53 +02:00 committed by GitHub
commit 386c95d42f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 405 additions and 1 deletions

View File

@ -881,6 +881,11 @@
"addon.notifications.notifications": "local_moodlemobileapp",
"addon.notifications.playsound": "local_moodlemobileapp",
"addon.notifications.therearentnotificationsyet": "local_moodlemobileapp",
"addon.storagemanager.deletecourse": "local_moodlemobileapp",
"addon.storagemanager.deletedatafrom": "local_moodlemobileapp",
"addon.storagemanager.info": "local_moodlemobileapp",
"addon.storagemanager.managestorage": "local_moodlemobileapp",
"addon.storagemanager.storageused": "local_moodlemobileapp",
"assets.countries.AD": "countries",
"assets.countries.AE": "countries",
"assets.countries.AF": "countries",

View File

@ -0,0 +1,7 @@
{
"deletecourse": "Offload all course data",
"deletedatafrom": "Offload data from {{name}}",
"info": "Files stored on your device make the app work faster, and when offline. You can safely offload them if you need to free up storage space.",
"managestorage": "Manage storage",
"storageused": "File storage used:"
}

View File

@ -0,0 +1,62 @@
<ion-header>
<ion-navbar core-back-button>
<ion-title>{{ 'addon.storagemanager.managestorage' | translate }}</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded">
<ion-card class="wholecourse">
<ion-card-header>
<h1 text-wrap>{{ course.displayname }}</h1>
<p text-wrap>{{ 'addon.storagemanager.info' | translate }}</p>
<ion-item no-padding padding-top>
<ion-row class="size">
<ion-icon name="cube"></ion-icon>
{{ 'addon.storagemanager.storageused' | translate }}
{{ totalSize | coreBytesToSize }}
</ion-row>
<button ion-button icon-only item-end no-padding (click)="deleteForCourse()" [disabled]="totalSize == 0">
<core-icon name="trash" label="{{ 'addon.storagemanager.deletecourse' | translate }}"></core-icon>
</button>
</ion-item>
</ion-card-header>
</ion-card>
<ng-container *ngFor="let section of sections">
<ion-card *ngIf="section.totalSize > 0" class="section">
<ion-card-header>
<ion-item no-padding>
<ion-row>
<h2 text-wrap>{{ section.name }}</h2>
</ion-row>
<ion-row class="size">
<ion-icon name="cube"></ion-icon>
{{ section.totalSize | coreBytesToSize }}
</ion-row>
<button ion-button icon-only item-end no-padding (click)="deleteForSection(section)">
<core-icon name="trash" label="{{ 'addon.storagemanager.deletedatafrom' | translate: { name: section.name } }}"></core-icon>
</button>
</ion-item>
</ion-card-header>
<ion-card-content>
<ng-container *ngFor="let module of section.modules">
<div *ngIf="module.totalSize > 0">
<ion-item no-padding>
<ion-row class="{{module.handlerData.class}}">
<img *ngIf="module.handlerData.icon" [src]="module.handlerData.icon" alt="" role="presentation" class="core-module-icon"
>{{ module.name }}
</ion-row>
<ion-row class="size">
<ion-icon name="cube"></ion-icon>
{{ module.totalSize | coreBytesToSize }}
</ion-row>
<button ion-button icon-only outline item-end (click)="deleteForModule(module)">
<core-icon name="trash" label="{{ 'addon.storagemanager.deletedatafrom' | translate: { name: module.name } }}"></core-icon>
</button>
</ion-item>
</div>
</ng-container>
</ion-card-content>
</ion-card>
</ng-container>
</core-loading>
</ion-content>

View File

@ -0,0 +1,36 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module';
import { AddonStorageManagerCourseStoragePage } from './course-storage';
@NgModule({
declarations: [
AddonStorageManagerCourseStoragePage,
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
IonicPageModule.forChild(AddonStorageManagerCourseStoragePage),
TranslateModule.forChild()
],
})
export class AddonStorageManagerCourseStoragePageModule {
}

View File

@ -0,0 +1,28 @@
ion-app.app-root page-addon-storagemanager-course-storage {
.item-md.item-block .item-inner {
padding-right: 0;
padding-left: 0;
}
ion-card.section ion-card-header.card-header {
border-bottom: 1px solid $list-border-color;
margin-bottom: 8px;
padding-top: 8px;
padding-bottom: 8px;
}
ion-card.section h2 {
font-weight: bold;
font-size: 2rem;
}
.size {
margin-top: 4px;
}
.size ion-icon {
margin-right: 4px;
}
.core-module-icon {
margin-right: 4px;
width: 16px;
height: 16px;
display: inline;
}
}

View File

@ -0,0 +1,163 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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, ViewChild } from '@angular/core';
import { IonicPage, Content, NavParams } from 'ionic-angular';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { TranslateService } from '@ngx-translate/core';
/**
* Page that displays the amount of file storage used by each activity on the course, and allows
* the user to delete these files.
*/
@IonicPage({ segment: 'addon-storagemanager-course-storage' })
@Component({
selector: 'page-addon-storagemanager-course-storage',
templateUrl: 'course-storage.html',
})
export class AddonStorageManagerCourseStoragePage {
@ViewChild(Content) content: Content;
course: any;
loaded: boolean;
sections: any;
totalSize: number;
constructor(navParams: NavParams,
private courseProvider: CoreCourseProvider,
private prefetchDelegate: CoreCourseModulePrefetchDelegate,
private courseHelperProvider: CoreCourseHelperProvider,
private domUtils: CoreDomUtilsProvider,
private translate: TranslateService) {
this.course = navParams.get('course');
}
/**
* View loaded.
*/
ionViewDidLoad(): void {
this.courseProvider.getSections(this.course.id, false, true).then((sections) => {
this.courseHelperProvider.addHandlerDataForModules(sections, this.course.id);
this.sections = sections;
this.totalSize = 0;
const allPromises = [];
this.sections.forEach((section) => {
section.totalSize = 0;
section.modules.forEach((module) => {
module.parentSection = section;
// Note: This function only gets the size for modules which are downloadable.
// For other modules it always returns 0, even if they have downloaded some files.
// However there is no 100% reliable way to actually track the files in this case.
// You can maybe guess it based on the component and componentid.
// But these aren't necessarily consistent, for example mod_frog vs mmaModFrog.
// There is nothing enforcing correct values.
// Most modules which have large files are downloadable, so I think this is sufficient.
const promise = this.prefetchDelegate.getModuleDownloadedSize(module, this.course.id).
then((size) => {
module.totalSize = size;
section.totalSize += size;
this.totalSize += size;
});
allPromises.push(promise);
});
});
Promise.all(allPromises).then(() => {
this.loaded = true;
});
});
}
/**
* The user has requested a delete for the whole course data.
*
* (This works by deleting data for each module on the course that has data.)
*/
deleteForCourse(): void {
const modules = [];
this.sections.forEach((section) => {
section.modules.forEach((module) => {
if (module.totalSize > 0) {
modules.push(module);
}
});
});
this.deleteModules(modules);
}
/**
* The user has requested a delete for a section's data.
*
* (This works by deleting data for each module in the section that has data.)
*
* @param {any} section Section object with information about section and modules
*/
deleteForSection(section: any): void {
const modules = [];
section.modules.forEach((module) => {
if (module.totalSize > 0) {
modules.push(module);
}
});
this.deleteModules(modules);
}
/**
* The user has requested a delete for a module's data
*
* @param {any} module Module details
*/
deleteForModule(module: any): void {
if (module.totalSize > 0) {
this.deleteModules([module]);
}
}
/**
* Deletes the specified modules, showing the loading overlay while it happens.
*
* @param {any[]} modules Modules to delete
* @return Promise<void> Once deleting has finished
*/
protected deleteModules(modules: any[]): Promise<void> {
const modal = this.domUtils.showModalLoading();
const promises = [];
modules.forEach((module) => {
// Remove the files.
const promise = this.prefetchDelegate.removeModuleFiles(module, this.course.id).then(() => {
// When the files are removed, update the size.
module.parentSection.totalSize -= module.totalSize;
this.totalSize -= module.totalSize;
module.totalSize = 0;
});
promises.push(promise);
});
return Promise.all(promises).then(() => {
modal.dismiss();
}).catch((error) => {
modal.dismiss();
this.domUtils.showErrorModalDefault(error, this.translate.instant('core.errordeletefile'));
});
}
}

View File

@ -0,0 +1,62 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { CoreCourseOptionsMenuHandler, CoreCourseOptionsMenuHandlerData } from '@core/course/providers/options-delegate';
/**
* Handler to inject an option into course menu so that user can get to the manage storage page.
*/
@Injectable()
export class AddonStorageManagerCourseMenuHandler implements CoreCourseOptionsMenuHandler {
name = 'AddonStorageManager';
priority = 500;
isMenuHandler = true;
/**
* Checks if the handler is enabled for specified course. This handler is always available.
*
* @param {number} courseId Course id
* @param {any} accessData Access data
* @param {any} [navOptions] Navigation options if any
* @param {any} [admOptions] Admin options if any
* @return {boolean | Promise<boolean>} True
*/
isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise<boolean> {
return true;
}
/**
* Check if the handler is enabled on a site level.
*
* @return {boolean | Promise<boolean>} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return true;
}
/**
* Returns the data needed to render the handler.
*
* @return {CoreCourseOptionsMenuHandlerData} Data needed to render the handler.
*/
getMenuDisplayData(): CoreCourseOptionsMenuHandlerData {
return {
icon: 'cube',
title: 'addon.storagemanager.managestorage',
page: 'AddonStorageManagerCourseStoragePage',
class: 'addon-storagemanager-coursemenu-handler'
};
}
}

View File

@ -0,0 +1,34 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { AddonStorageManagerCourseMenuHandler } from '@addon/storagemanager/providers/coursemenu-handler';
import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate';
@NgModule({
declarations: [],
imports: [
],
providers: [
AddonStorageManagerCourseMenuHandler
],
exports: []
})
export class AddonStorageManagerModule {
constructor(private courseOptionsDelegate: CoreCourseOptionsDelegate,
private courseMenuHandler: AddonStorageManagerCourseMenuHandler) {
// Register handlers.
this.courseOptionsDelegate.registerHandler(this.courseMenuHandler);
}
}

View File

@ -126,6 +126,7 @@ import { AddonNotificationsModule } from '@addon/notifications/notifications.mod
import { AddonRemoteThemesModule } from '@addon/remotethemes/remotethemes.module';
import { AddonQbehaviourModule } from '@addon/qbehaviour/qbehaviour.module';
import { AddonQtypeModule } from '@addon/qtype/qtype.module';
import { AddonStorageManagerModule } from '@addon/storagemanager/storagemanager.module';
// For translate loader. AoT requires an exported function for factories.
export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
@ -244,7 +245,8 @@ export const CORE_PROVIDERS: any[] = [
AddonNotificationsModule,
AddonRemoteThemesModule,
AddonQbehaviourModule,
AddonQtypeModule
AddonQtypeModule,
AddonStorageManagerModule
],
bootstrap: [IonicApp],
entryComponents: [

View File

@ -881,6 +881,11 @@
"addon.notifications.notifications": "Notifications",
"addon.notifications.playsound": "Play sound",
"addon.notifications.therearentnotificationsyet": "There are no notifications.",
"addon.storagemanager.deletecourse": "Offload all course data",
"addon.storagemanager.deletedatafrom": "Offload data from {{name}}",
"addon.storagemanager.info": "Files stored on your device make the app work faster, and when offline. You can safely offload them if you need to free up storage space.",
"addon.storagemanager.managestorage": "Manage storage",
"addon.storagemanager.storageused": "File storage used:",
"assets.countries.AD": "Andorra",
"assets.countries.AE": "United Arab Emirates",
"assets.countries.AF": "Afghanistan",