+
+
+
+
+
+
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",