diff --git a/src/addon/storagemanager/lang/en.json b/src/addon/storagemanager/lang/en.json index a3491b8b9..8efc60a30 100644 --- a/src/addon/storagemanager/lang/en.json +++ b/src/addon/storagemanager/lang/en.json @@ -1,5 +1,6 @@ { "deletecourse": "Offload all course data", + "deletecourses": "Offload all courses data", "deletedatafrom": "Offload data from {{name}}", "info": "Files stored on your device make the app work faster and enable the app to be used offline. You can safely offload files if you need to free up storage space.", "managestorage": "Manage storage", diff --git a/src/addon/storagemanager/pages/course-storage/course-storage.ts b/src/addon/storagemanager/pages/course-storage/course-storage.ts index ff9196403..6bdeecc52 100644 --- a/src/addon/storagemanager/pages/course-storage/course-storage.ts +++ b/src/addon/storagemanager/pages/course-storage/course-storage.ts @@ -98,7 +98,17 @@ export class AddonStorageManagerCourseStoragePage { * * (This works by deleting data for each module on the course that has data.) */ - deleteForCourse(): void { + async deleteForCourse(): Promise { + try { + await this.domUtils.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + } catch (error) { + if (!error.coreCanceled) { + throw error; + } + + return; + } + const modules = []; this.sections.forEach((section) => { section.modules.forEach((module) => { @@ -118,7 +128,17 @@ export class AddonStorageManagerCourseStoragePage { * * @param section Section object with information about section and modules */ - deleteForSection(section: any): void { + async deleteForSection(section: any): Promise { + try { + await this.domUtils.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + } catch (error) { + if (!error.coreCanceled) { + throw error; + } + + return; + } + const modules = []; section.modules.forEach((module) => { if (module.totalSize > 0) { @@ -134,10 +154,22 @@ export class AddonStorageManagerCourseStoragePage { * * @param module Module details */ - deleteForModule(module: any): void { - if (module.totalSize > 0) { - this.deleteModules([module]); + async deleteForModule(module: any): Promise { + if (module.totalSize === 0) { + return; } + + try { + await this.domUtils.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + } catch (error) { + if (!error.coreCanceled) { + throw error; + } + + return; + } + + this.deleteModules([module]); } /** diff --git a/src/addon/storagemanager/pages/courses-storage/courses-storage.html b/src/addon/storagemanager/pages/courses-storage/courses-storage.html new file mode 100644 index 000000000..937ddd2be --- /dev/null +++ b/src/addon/storagemanager/pages/courses-storage/courses-storage.html @@ -0,0 +1,40 @@ + + + {{ 'addon.storagemanager.managestorage' | translate }} + + + + + + +

{{ 'core.courses.courses' | translate }}

+

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

+ + +

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

+
+

{{ totalSize | coreBytesToSize }}

+
+ +
+
+
+ + + +

{{ course.displayname }}

+

{{ 'core.downloading' | translate }}

+

+ + {{ course.totalSize | coreBytesToSize }} +

+ +
+
+
+
+
diff --git a/src/addon/storagemanager/pages/courses-storage/courses-storage.module.ts b/src/addon/storagemanager/pages/courses-storage/courses-storage.module.ts new file mode 100644 index 000000000..95a210991 --- /dev/null +++ b/src/addon/storagemanager/pages/courses-storage/courses-storage.module.ts @@ -0,0 +1,36 @@ +// (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 { 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 { AddonStorageManagerCoursesStoragePage } from './courses-storage'; + +@NgModule({ + declarations: [ + AddonStorageManagerCoursesStoragePage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + IonicPageModule.forChild(AddonStorageManagerCoursesStoragePage), + TranslateModule.forChild() + ], +}) +export class AddonStorageManagerCoursesStoragePageModule { +} diff --git a/src/addon/storagemanager/pages/courses-storage/courses-storage.scss b/src/addon/storagemanager/pages/courses-storage/courses-storage.scss new file mode 100644 index 000000000..b26c3fb94 --- /dev/null +++ b/src/addon/storagemanager/pages/courses-storage/courses-storage.scss @@ -0,0 +1,28 @@ +ion-app.app-root page-addon-storagemanager-courses-storage { + + .item-md.item-block .item-inner { + padding-right: 0; + padding-left: 0; + } + + ion-item.course { + border-bottom: 1px solid $list-border-color; + padding-right: 16px; + padding-left: 16px; + + h2 { + font-weight: bold; + font-size: 2rem; + } + + h3 { + color: $subdued-text-color; + } + + &:last-child { + border-bottom: 0; + } + + } + +} diff --git a/src/addon/storagemanager/pages/courses-storage/courses-storage.ts b/src/addon/storagemanager/pages/courses-storage/courses-storage.ts new file mode 100644 index 000000000..05b5aa709 --- /dev/null +++ b/src/addon/storagemanager/pages/courses-storage/courses-storage.ts @@ -0,0 +1,218 @@ +// (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 } from '@angular/core'; +import { IonicPage } from 'ionic-angular'; +import { CoreCourse, CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourses } from '@core/courses/providers/courses'; +import { CoreArray } from '@singletons/array'; +import { CoreCourseModulePrefetch } from '@core/course/providers/module-prefetch-delegate'; +import { CoreConstants } from '@core/constants'; +import { CoreDomUtils } from '@providers/utils/dom'; +import { Translate } from '@singletons/core.singletons'; +import { CoreEvents, CoreEventsProvider, CoreEventObserver } from '@providers/events'; +import { CoreCourseHelper } from '@core/course/providers/helper'; + +/** + * Core course data. + */ +interface Course { + id: number; + displayname: string; +} + +/** + * Downloaded course data. + */ +interface DownloadedCourse extends Course { + totalSize: number; + isDownloading: boolean; +} + +/** + * Page that displays downloaded courses and allows the user to delete them. + */ +@IonicPage({ segment: 'addon-storagemanager-courses-storage' }) +@Component({ + selector: 'page-addon-storagemanager-courses-storage', + templateUrl: 'courses-storage.html', +}) +export class AddonStorageManagerCoursesStoragePage { + + userCourses: Course[] = []; + downloadedCourses: DownloadedCourse[] = []; + completelyDownloadedCourses: DownloadedCourse[] = []; + totalSize = 0; + loaded = false; + + courseStatusObserver: CoreEventObserver; + + /** + * View loaded. + */ + async ionViewDidLoad(): Promise { + this.userCourses = await CoreCourses.instance.getUserCourses(); + this.courseStatusObserver = CoreEvents.instance.on( + CoreEventsProvider.COURSE_STATUS_CHANGED, + ({ courseId, status }) => this.onCourseUpdated(courseId, status), + ); + + const downloadedCourseIds = await CoreCourse.instance.getDownloadedCourseIds(); + const downloadedCourses = await Promise.all( + this.userCourses + .filter((course) => downloadedCourseIds.indexOf(course.id) !== -1) + .map((course) => this.getDownloadedCourse(course)), + ); + + this.setDownloadedCourses(downloadedCourses); + + this.loaded = true; + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + this.courseStatusObserver && this.courseStatusObserver.off(); + } + + /** + * Delete all courses that have been downloaded. + */ + async deleteCompletelyDownloadedCourses(): Promise { + try { + await CoreDomUtils.instance.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + } catch (error) { + if (!error.coreCanceled) { + throw error; + } + + return; + } + + const modal = CoreDomUtils.instance.showModalLoading(); + const deletedCourseIds = this.completelyDownloadedCourses.map((course) => course.id); + + try { + await Promise.all(deletedCourseIds.map((courseId) => CoreCourseHelper.instance.deleteCourseFiles(courseId))); + + this.setDownloadedCourses(this.downloadedCourses.filter((course) => !CoreArray.contains(deletedCourseIds, course.id))); + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, Translate.instance.instant('core.errordeletefile')); + } finally { + modal.dismiss(); + } + } + + /** + * Delete course. + * + * @param course Course to delete. + */ + async deleteCourse(course: DownloadedCourse): Promise { + try { + await CoreDomUtils.instance.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + } catch (error) { + if (!error.coreCanceled) { + throw error; + } + + return; + } + + const modal = CoreDomUtils.instance.showModalLoading(); + + try { + await CoreCourseHelper.instance.deleteCourseFiles(course.id); + + this.setDownloadedCourses(CoreArray.withoutItem(this.downloadedCourses, course)); + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, Translate.instance.instant('core.errordeletefile')); + } finally { + modal.dismiss(); + } + } + + /** + * Handle course updated event. + * + * @param courseId Updated course id. + */ + private async onCourseUpdated(courseId: number, status: string): Promise { + if (courseId == CoreCourseProvider.ALL_COURSES_CLEARED) { + this.setDownloadedCourses([]); + + return; + } + + const course = this.downloadedCourses.find((course) => course.id === courseId); + + if (!course) { + return; + } + + course.isDownloading = status === CoreConstants.DOWNLOADING; + course.totalSize = await this.calculateDownloadedCourseSize(course.id); + + this.setDownloadedCourses(this.downloadedCourses); + } + + /** + * Set downloaded courses data. + * + * @param courses Courses info. + */ + private setDownloadedCourses(courses: DownloadedCourse[]): void { + this.downloadedCourses = courses; + this.completelyDownloadedCourses = courses.filter((course) => !course.isDownloading); + this.totalSize = this.downloadedCourses.reduce((totalSize, course) => totalSize + course.totalSize, 0); + } + + /** + * Get downloaded course data. + * + * @param course Course. + * @return Course info. + */ + private async getDownloadedCourse(course: Course): Promise { + const totalSize = await this.calculateDownloadedCourseSize(course.id); + const status = await CoreCourse.instance.getCourseStatus(course.id); + + return { + ...course, + totalSize, + isDownloading: status === CoreConstants.DOWNLOADING, + }; + } + + /** + * Calculate the size of a downloaded course. + * + * @param courseId Downloaded course id. + * @return Promise to be resolved with the course size. + */ + private async calculateDownloadedCourseSize(courseId: number): Promise { + const sections = await CoreCourse.instance.getSections(courseId); + const modules = CoreArray.flatten(sections.map((section) => section.modules)); + const promisedModuleSizes = modules.map(async (module) => { + const size = await CoreCourseModulePrefetch.instance.getModuleDownloadedSize(module, courseId); + + return isNaN(size) ? 0 : size; + }); + const moduleSizes = await Promise.all(promisedModuleSizes); + + return moduleSizes.reduce((totalSize, moduleSize) => totalSize + moduleSize, 0); + } + +} diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 537dbbacd..167e10333 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1012,6 +1012,7 @@ "addon.notifications.playsound": "Play sound", "addon.notifications.therearentnotificationsyet": "There are no notifications.", "addon.storagemanager.deletecourse": "Offload all course data", + "addon.storagemanager.deletecourses": "Offload all courses data", "addon.storagemanager.deletedatafrom": "Offload data from {{name}}", "addon.storagemanager.info": "Files stored on your device make the app work faster and enable the app to be used offline. You can safely offload files if you need to free up storage space.", "addon.storagemanager.managestorage": "Manage storage", diff --git a/src/core/course/providers/course.ts b/src/core/course/providers/course.ts index 1facbf99f..c6dc1c576 100644 --- a/src/core/course/providers/course.ts +++ b/src/core/course/providers/course.ts @@ -29,6 +29,7 @@ import { CoreSitePluginsProvider } from '@core/siteplugins/providers/siteplugins import { CoreCourseFormatDelegate } from './format-delegate'; import { CorePushNotificationsProvider } from '@core/pushnotifications/providers/pushnotifications'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { makeSingleton } from '@singletons/core.singletons'; /** * Service that provides some features regarding a course. @@ -334,6 +335,23 @@ export class CoreCourseProvider { }); } + /** + * Obtain ids of downloaded courses. + * + * @param siteId Site id. + * @return Resolves with an array containing downloaded course ids. + */ + async getDownloadedCourseIds(siteId?: string): Promise { + const site = await this.sitesProvider.getSite(siteId); + const entries = await site.getDb().getRecordsList(this.COURSE_STATUS_TABLE, 'status', [ + CoreConstants.DOWNLOADED, + CoreConstants.DOWNLOADING, + CoreConstants.OUTDATED, + ]); + + return entries.map((entry) => entry.id); + } + /** * Get a module from Moodle. * @@ -1178,3 +1196,5 @@ export type CoreCourseModuleSummary = { url?: string; // Url. iconurl: string; // Iconurl. }; + +export class CoreCourse extends makeSingleton(CoreCourseProvider) {} diff --git a/src/core/course/providers/helper.ts b/src/core/course/providers/helper.ts index 606e913b2..25d5137eb 100644 --- a/src/core/course/providers/helper.ts +++ b/src/core/course/providers/helper.ts @@ -40,6 +40,8 @@ import { CoreSite } from '@classes/site'; import { CoreLoggerProvider } from '@providers/logger'; import * as moment from 'moment'; import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; +import { CoreArray } from '@singletons/array'; +import { makeSingleton } from '@singletons/core.singletons'; /** * Prefetch info of a module. @@ -1607,4 +1609,24 @@ export class CoreCourseHelperProvider { return this.loginHelper.redirect(CoreLoginHelperProvider.OPEN_COURSE, params, siteId); } } + + /** + * Delete course files. + * + * @param courseId Course id. + * @return Promise to be resolved once the course files are deleted. + */ + async deleteCourseFiles(courseId: number): Promise { + const sections = await this.courseProvider.getSections(courseId); + const modules = CoreArray.flatten(sections.map((section) => section.modules)); + + await Promise.all( + modules.map((module) => this.prefetchDelegate.removeModuleFiles(module, courseId)), + ); + + await this.courseProvider.setCourseStatus(courseId, CoreConstants.NOT_DOWNLOADED); + } + } + +export class CoreCourseHelper extends makeSingleton(CoreCourseHelperProvider) {} diff --git a/src/core/course/providers/module-prefetch-delegate.ts b/src/core/course/providers/module-prefetch-delegate.ts index 50fbdc5db..38a5cc3d0 100644 --- a/src/core/course/providers/module-prefetch-delegate.ts +++ b/src/core/course/providers/module-prefetch-delegate.ts @@ -28,6 +28,7 @@ import { Md5 } from 'ts-md5/dist/md5'; import { Subject, BehaviorSubject, Subscription } from 'rxjs'; import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { CoreFileHelperProvider } from '@providers/file-helper'; +import { makeSingleton } from '@singletons/core.singletons'; /** * Progress of downloading a list of modules. @@ -1467,3 +1468,5 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { } } } + +export class CoreCourseModulePrefetch extends makeSingleton(CoreCourseModulePrefetchDelegate) {} diff --git a/src/core/courses/components/course-options-menu/core-courses-course-options-menu.html b/src/core/courses/components/course-options-menu/core-courses-course-options-menu.html index 9e17624b8..53682fd43 100644 --- a/src/core/courses/components/course-options-menu/core-courses-course-options-menu.html +++ b/src/core/courses/components/course-options-menu/core-courses-course-options-menu.html @@ -3,6 +3,10 @@

{{ prefetch.title | translate }}

+ + +

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

+

{{ 'core.courses.hidecourse' | translate }}

diff --git a/src/core/courses/components/course-options-menu/course-options-menu.ts b/src/core/courses/components/course-options-menu/course-options-menu.ts index 02aae3630..fba1d2de2 100644 --- a/src/core/courses/components/course-options-menu/course-options-menu.ts +++ b/src/core/courses/components/course-options-menu/course-options-menu.ts @@ -15,6 +15,7 @@ import { Component, OnInit } from '@angular/core'; import { NavParams, ViewController } from 'ionic-angular'; import { CoreCoursesProvider } from '../../providers/courses'; +import { CoreConstants } from '@core/constants'; /** * This component is meant to display a popover with the course options. @@ -25,12 +26,14 @@ import { CoreCoursesProvider } from '../../providers/courses'; }) export class CoreCoursesCourseOptionsMenuComponent implements OnInit { course: any; // The course. + courseStatus: string; // The course status. prefetch: any; // The prefecth info. downloadCourseEnabled: boolean; constructor(navParams: NavParams, private viewCtrl: ViewController, private coursesProvider: CoreCoursesProvider) { this.course = navParams.get('course') || {}; + this.courseStatus = navParams.get('courseStatus') || CoreConstants.NOT_DOWNLOADED; this.prefetch = navParams.get('prefetch') || {}; } diff --git a/src/core/courses/components/course-progress/course-progress.ts b/src/core/courses/components/course-progress/course-progress.ts index cafc7c142..88923c044 100644 --- a/src/core/courses/components/course-progress/course-progress.ts +++ b/src/core/courses/components/course-progress/course-progress.ts @@ -22,6 +22,8 @@ import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseHelperProvider } from '@core/course/providers/helper'; import { CoreCoursesCourseOptionsMenuComponent } from '../course-options-menu/course-options-menu'; +import { Translate } from '@singletons/core.singletons'; +import { CoreConstants } from '@core/constants'; /** * This component is meant to display a course for a list of courses with progress. @@ -40,6 +42,7 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy { @Input() showAll = false; // If true, will show all actions, options, star and progress. @Input() showDownload = true; // If true, will show download button. Only works if the options menu is not shown. + courseStatus = CoreConstants.NOT_DOWNLOADED; isDownloading: boolean; prefetchCourseData = { downloadSucceeded: false, @@ -104,7 +107,10 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy { }, this.sitesProvider.getCurrentSiteId()); // Determine course prefetch icon. - this.courseHelper.getCourseStatusIconAndTitle(this.course.id).then((data) => { + this.courseProvider.getCourseStatus(this.course.id).then((status) => { + const data = this.courseHelper.getCourseStatusIconAndTitleFromStatus(status); + + this.courseStatus = status; this.prefetchCourseData.prefetchCourseIcon = data.icon; this.prefetchCourseData.title = data.title; @@ -152,6 +158,31 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy { }); } + /** + * Delete the course. + */ + async deleteCourse(): Promise { + try { + await this.domUtils.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + } catch (error) { + if (!error.coreCanceled) { + throw error; + } + + return; + } + + const modal = this.domUtils.showModalLoading(); + + try { + await this.courseHelper.deleteCourseFiles(this.course.id); + } catch (error) { + this.domUtils.showErrorModalDefault(error, Translate.instance.instant('core.errordeletefile')); + } finally { + modal.dismiss(); + } + } + /** * Update the course status icon and title. * @@ -160,6 +191,7 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy { protected updateCourseStatus(status: string): void { const statusData = this.courseHelper.getCourseStatusIconAndTitleFromStatus(status); + this.courseStatus = status; this.prefetchCourseData.prefetchCourseIcon = statusData.icon; this.prefetchCourseData.title = statusData.title; } @@ -175,6 +207,7 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy { const popover = this.popoverCtrl.create(CoreCoursesCourseOptionsMenuComponent, { course: this.course, + courseStatus: this.courseStatus, prefetch: this.prefetchCourseData }); popover.onDidDismiss((action) => { @@ -185,6 +218,11 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy { this.prefetchCourse(e); } break; + case 'delete': + if (this.courseStatus == 'downloaded' || this.courseStatus == 'outdated') { + this.deleteCourse(); + } + break; case 'hide': this.setCourseHidden(true); break; diff --git a/src/core/courses/pages/dashboard/dashboard.html b/src/core/courses/pages/dashboard/dashboard.html index 4ad0dcd61..b4d21e8bd 100644 --- a/src/core/courses/pages/dashboard/dashboard.html +++ b/src/core/courses/pages/dashboard/dashboard.html @@ -10,6 +10,8 @@ + + diff --git a/src/core/courses/pages/dashboard/dashboard.ts b/src/core/courses/pages/dashboard/dashboard.ts index 99ade84ca..49f2924bb 100644 --- a/src/core/courses/pages/dashboard/dashboard.ts +++ b/src/core/courses/pages/dashboard/dashboard.ts @@ -122,6 +122,13 @@ export class CoreCoursesDashboardPage implements OnDestroy { this.tabsComponent && this.tabsComponent.ionViewDidLeave(); } + /** + * Open page to manage courses storage. + */ + manageCoursesStorage(): void { + this.navCtrl.push('AddonStorageManagerCoursesStoragePage'); + } + /** * Go to search courses. */ diff --git a/src/core/courses/providers/courses.ts b/src/core/courses/providers/courses.ts index 2ef00a33c..fc56eb385 100644 --- a/src/core/courses/providers/courses.ts +++ b/src/core/courses/providers/courses.ts @@ -17,6 +17,7 @@ import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSite } from '@classes/site'; +import { makeSingleton } from '@singletons/core.singletons'; /** * Data sent to the EVENT_MY_COURSES_UPDATED. @@ -1152,3 +1153,5 @@ export class CoreCoursesProvider { }); } } + +export class CoreCourses extends makeSingleton(CoreCoursesProvider) {} diff --git a/src/singletons/array.ts b/src/singletons/array.ts new file mode 100644 index 000000000..a35ca1450 --- /dev/null +++ b/src/singletons/array.ts @@ -0,0 +1,65 @@ +// (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. + +/** + * Singleton with helper functions for arrays. + */ +export class CoreArray { + + /** + * Check whether an array contains an item. + * + * @param arr Array. + * @param item Item. + * @return Whether item is within the array. + */ + static contains(arr: T[], item: T): boolean { + return arr.indexOf(item) !== -1; + } + + /** + * Flatten the first dimension of a multi-dimensional array. + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat#reduce_and_concat + * + * @param arr Original array. + * @return Flattened array. + */ + static flatten(arr: T[][]): T[] { + if ('flat' in arr) { + return (arr as any).flat(); + } + + return [].concat(...arr); + } + + /** + * Obtain a new array without the specified item. + * + * @param arr Array. + * @param item Item to remove. + * @return Array without the specified item. + */ + static withoutItem(arr: T[], item: T): T[] { + const newArray = [...arr]; + const index = arr.indexOf(item); + + if (index !== -1) { + newArray.splice(index, 1); + } + + return newArray; + } + +}