diff --git a/scripts/langindex.json b/scripts/langindex.json index 7f4fb4d09..bab183d79 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -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", diff --git a/src/addon/storagemanager/lang/en.json b/src/addon/storagemanager/lang/en.json new file mode 100644 index 000000000..a65866e55 --- /dev/null +++ b/src/addon/storagemanager/lang/en.json @@ -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:" +} diff --git a/src/addon/storagemanager/pages/course-storage/course-storage.html b/src/addon/storagemanager/pages/course-storage/course-storage.html new file mode 100644 index 000000000..8dd91ea1e --- /dev/null +++ b/src/addon/storagemanager/pages/course-storage/course-storage.html @@ -0,0 +1,62 @@ + + + {{ 'addon.storagemanager.managestorage' | translate }} + + + + + + +

{{ course.displayname }}

+

{{ 'addon.storagemanager.info' | translate }}

+ + + + {{ 'addon.storagemanager.storageused' | translate }} + {{ totalSize | coreBytesToSize }} + + + +
+
+ + + + + +

{{ section.name }}

+
+ + + {{ section.totalSize | coreBytesToSize }} + + +
+
+ + +
+ + + {{ module.name }} + + + + {{ module.totalSize | coreBytesToSize }} + + + +
+
+
+
+
+
+
diff --git a/src/addon/storagemanager/pages/course-storage/course-storage.module.ts b/src/addon/storagemanager/pages/course-storage/course-storage.module.ts new file mode 100644 index 000000000..19db12630 --- /dev/null +++ b/src/addon/storagemanager/pages/course-storage/course-storage.module.ts @@ -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 { +} diff --git a/src/addon/storagemanager/pages/course-storage/course-storage.scss b/src/addon/storagemanager/pages/course-storage/course-storage.scss new file mode 100644 index 000000000..2c03e4984 --- /dev/null +++ b/src/addon/storagemanager/pages/course-storage/course-storage.scss @@ -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; + } +} diff --git a/src/addon/storagemanager/pages/course-storage/course-storage.ts b/src/addon/storagemanager/pages/course-storage/course-storage.ts new file mode 100644 index 000000000..ed0403151 --- /dev/null +++ b/src/addon/storagemanager/pages/course-storage/course-storage.ts @@ -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 Once deleting has finished + */ + protected deleteModules(modules: any[]): Promise { + 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')); + }); + } +} diff --git a/src/addon/storagemanager/providers/coursemenu-handler.ts b/src/addon/storagemanager/providers/coursemenu-handler.ts new file mode 100644 index 000000000..e2aad3def --- /dev/null +++ b/src/addon/storagemanager/providers/coursemenu-handler.ts @@ -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} True + */ + isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise { + return true; + } + + /** + * Check if the handler is enabled on a site level. + * + * @return {boolean | Promise} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + 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' + }; + } +} diff --git a/src/addon/storagemanager/storagemanager.module.ts b/src/addon/storagemanager/storagemanager.module.ts new file mode 100644 index 000000000..9590abb60 --- /dev/null +++ b/src/addon/storagemanager/storagemanager.module.ts @@ -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); + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 71ce98128..aecc2cbe8 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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: [ diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index a6b658a3d..ef8a0e835 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -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",