Merge pull request #1856 from sammarshallou/MOBILE-2905
MOBILE-2905 Allow user to manage storage within coursemain
commit
386c95d42f
|
@ -881,6 +881,11 @@
|
||||||
"addon.notifications.notifications": "local_moodlemobileapp",
|
"addon.notifications.notifications": "local_moodlemobileapp",
|
||||||
"addon.notifications.playsound": "local_moodlemobileapp",
|
"addon.notifications.playsound": "local_moodlemobileapp",
|
||||||
"addon.notifications.therearentnotificationsyet": "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.AD": "countries",
|
||||||
"assets.countries.AE": "countries",
|
"assets.countries.AE": "countries",
|
||||||
"assets.countries.AF": "countries",
|
"assets.countries.AF": "countries",
|
||||||
|
|
|
@ -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:"
|
||||||
|
}
|
|
@ -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>
|
|
@ -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 {
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -126,6 +126,7 @@ import { AddonNotificationsModule } from '@addon/notifications/notifications.mod
|
||||||
import { AddonRemoteThemesModule } from '@addon/remotethemes/remotethemes.module';
|
import { AddonRemoteThemesModule } from '@addon/remotethemes/remotethemes.module';
|
||||||
import { AddonQbehaviourModule } from '@addon/qbehaviour/qbehaviour.module';
|
import { AddonQbehaviourModule } from '@addon/qbehaviour/qbehaviour.module';
|
||||||
import { AddonQtypeModule } from '@addon/qtype/qtype.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.
|
// For translate loader. AoT requires an exported function for factories.
|
||||||
export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
|
export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
|
||||||
|
@ -244,7 +245,8 @@ export const CORE_PROVIDERS: any[] = [
|
||||||
AddonNotificationsModule,
|
AddonNotificationsModule,
|
||||||
AddonRemoteThemesModule,
|
AddonRemoteThemesModule,
|
||||||
AddonQbehaviourModule,
|
AddonQbehaviourModule,
|
||||||
AddonQtypeModule
|
AddonQtypeModule,
|
||||||
|
AddonStorageManagerModule
|
||||||
],
|
],
|
||||||
bootstrap: [IonicApp],
|
bootstrap: [IonicApp],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
|
|
|
@ -881,6 +881,11 @@
|
||||||
"addon.notifications.notifications": "Notifications",
|
"addon.notifications.notifications": "Notifications",
|
||||||
"addon.notifications.playsound": "Play sound",
|
"addon.notifications.playsound": "Play sound",
|
||||||
"addon.notifications.therearentnotificationsyet": "There are no notifications.",
|
"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.AD": "Andorra",
|
||||||
"assets.countries.AE": "United Arab Emirates",
|
"assets.countries.AE": "United Arab Emirates",
|
||||||
"assets.countries.AF": "Afghanistan",
|
"assets.countries.AF": "Afghanistan",
|
||||||
|
|
Loading…
Reference in New Issue