diff --git a/src/addons/addons.module.ts b/src/addons/addons.module.ts index 0a0ab6342..42e4642f9 100644 --- a/src/addons/addons.module.ts +++ b/src/addons/addons.module.ts @@ -31,6 +31,7 @@ import { AddonBlogModule } from './blog/blog.module'; import { AddonRemoteThemesModule } from './remotethemes/remotethemes.module'; import { AddonNotesModule } from './notes/notes.module'; import { AddonCompetencyModule } from './competency/competency.module'; +import { AddonStorageManagerModule } from './storagemanager/storagemanager.module'; @NgModule({ imports: [ @@ -51,6 +52,7 @@ import { AddonCompetencyModule } from './competency/competency.module'; AddonQbehaviourModule, AddonQtypeModule, AddonRemoteThemesModule, + AddonStorageManagerModule, ], }) export class AddonsModule {} diff --git a/src/addons/mod/resource/pages/index/index.html b/src/addons/mod/resource/pages/index/index.html index 16007eb9d..143b52f75 100644 --- a/src/addons/mod/resource/pages/index/index.html +++ b/src/addons/mod/resource/pages/index/index.html @@ -15,7 +15,7 @@ diff --git a/src/addons/mod/resource/services/resource-helper.ts b/src/addons/mod/resource/services/resource-helper.ts index 33c37bfe7..48d170947 100644 --- a/src/addons/mod/resource/services/resource-helper.ts +++ b/src/addons/mod/resource/services/resource-helper.ts @@ -133,7 +133,7 @@ export class AddonModResourceHelperProvider { return false; } - return mimetype == 'text/html'; + return mimetype == 'text/html' || mimetype == 'application/xhtml+xml'; } /** diff --git a/src/addons/storagemanager/lang.json b/src/addons/storagemanager/lang.json new file mode 100644 index 000000000..8efc60a30 --- /dev/null +++ b/src/addons/storagemanager/lang.json @@ -0,0 +1,8 @@ +{ + "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", + "storageused": "File storage used:" +} diff --git a/src/addons/storagemanager/pages/course-storage/course-storage.html b/src/addons/storagemanager/pages/course-storage/course-storage.html new file mode 100644 index 000000000..0eedaf27c --- /dev/null +++ b/src/addons/storagemanager/pages/course-storage/course-storage.html @@ -0,0 +1,67 @@ + + + + + + {{ 'addon.storagemanager.managestorage' | translate }} + + + + + + + {{ course.displayname }} + {{ course.fullname }} +

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

+ + + +

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

+
+

{{ totalSize | coreBytesToSize }}

+ + + + +
+
+
+ + + + + +

{{ section.name }}

+
+

{{ section.totalSize | coreBytesToSize }}

+ + + + +
+
+ + + + + +

+ {{ module.name }} +

+
+

{{ module.totalSize | coreBytesToSize }}

+ + + + +
+
+
+
+
+
+
diff --git a/src/addons/storagemanager/pages/course-storage/course-storage.scss b/src/addons/storagemanager/pages/course-storage/course-storage.scss new file mode 100644 index 000000000..c1c3a4bf0 --- /dev/null +++ b/src/addons/storagemanager/pages/course-storage/course-storage.scss @@ -0,0 +1,11 @@ +:host { + ion-card.section ion-card-header { + margin-bottom: 8px; + padding-top: 8px; + padding-bottom: 8px; + } + ion-card.section h2 { + font-weight: bold; + font-size: 1.2rem; + } +} diff --git a/src/addons/storagemanager/pages/course-storage/course-storage.ts b/src/addons/storagemanager/pages/course-storage/course-storage.ts new file mode 100644 index 000000000..b71582d05 --- /dev/null +++ b/src/addons/storagemanager/pages/course-storage/course-storage.ts @@ -0,0 +1,228 @@ +// (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 { CoreConstants } from '@/core/constants'; +import { Component, OnInit } from '@angular/core'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreCourseHelper, CoreCourseModule, CoreCourseSection } from '@features/course/services/course-helper'; +import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; +import { CoreEnrolledCourseData } from '@features/courses/services/courses'; +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { Translate } from '@singletons'; + +/** + * Page that displays the amount of file storage used by each activity on the course, and allows + * the user to delete these files. + */ +@Component({ + selector: 'page-addon-storagemanager-course-storage', + templateUrl: 'course-storage.html', + styleUrls: ['course-storage.scss'], +}) +export class AddonStorageManagerCourseStoragePage implements OnInit { + + course!: CoreEnrolledCourseData; + loaded = false; + sections: AddonStorageManagerCourseSection[] = []; + totalSize = 0; + + /** + * View loaded. + */ + async ngOnInit(): Promise { + this.course = CoreNavigator.getRouteParam('course')!; + + this.sections = await CoreCourse.getSections(this.course.id, false, true); + CoreCourseHelper.addHandlerDataForModules(this.sections, this.course.id); + + this.totalSize = 0; + + const promises: Promise[] = []; + this.sections.forEach((section) => { + section.totalSize = 0; + section.modules.forEach((module) => { + module.parentSection = section; + module.totalSize = 0; + // 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 = CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, this.course.id).then((size) => { + // There are some cases where the return from this is not a valid number. + if (!isNaN(size)) { + module.totalSize = Number(size); + section.totalSize! += size; + this.totalSize += size; + } + + return; + }); + promises.push(promise); + }); + }); + + await Promise.all(promises); + this.loaded = true; + + if (this.totalSize == 0) { + this.markCourseAsNotDownloaded(); + } + } + + /** + * 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.) + */ + async deleteForCourse(): Promise { + try { + await CoreDomUtils.showDeleteConfirm('core.course.confirmdeletestoreddata'); + } catch (error) { + if (!CoreDomUtils.isCanceledError(error)) { + throw error; + } + + return; + } + + const modules: AddonStorageManagerModule[] = []; + this.sections.forEach((section) => { + section.modules.forEach((module) => { + if (module.totalSize && 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 section Section object with information about section and modules + */ + async deleteForSection(section: AddonStorageManagerCourseSection): Promise { + try { + await CoreDomUtils.showDeleteConfirm('core.course.confirmdeletestoreddata'); + } catch (error) { + if (!CoreDomUtils.isCanceledError(error)) { + throw error; + } + + return; + } + + const modules: AddonStorageManagerModule[] = []; + section.modules.forEach((module) => { + if (module.totalSize && module.totalSize > 0) { + modules.push(module); + } + }); + + this.deleteModules(modules); + } + + /** + * The user has requested a delete for a module's data + * + * @param module Module details + */ + async deleteForModule(module: AddonStorageManagerModule): Promise { + if (module.totalSize === 0) { + return; + } + + try { + await CoreDomUtils.showDeleteConfirm('core.course.confirmdeletestoreddata'); + } catch (error) { + if (!CoreDomUtils.isCanceledError(error)) { + throw error; + } + + return; + } + + this.deleteModules([module]); + } + + /** + * Deletes the specified modules, showing the loading overlay while it happens. + * + * @param modules Modules to delete + * @return Promise Once deleting has finished + */ + protected async deleteModules(modules: AddonStorageManagerModule[]): Promise { + const modal = await CoreDomUtils.showModalLoading(); + + const promises: Promise[] = []; + modules.forEach((module) => { + // Remove the files. + const promise = CoreCourseHelper.removeModuleStoredData(module, this.course.id).then(() => { + // When the files and cache are removed, update the size. + module.parentSection!.totalSize! -= module.totalSize!; + this.totalSize -= module.totalSize!; + module.totalSize = 0; + + return; + }); + + promises.push(promise); + }); + + try { + await Promise.all(promises); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, Translate.instant('core.errordeletefile')); + } finally { + modal.dismiss(); + + // @TODO This is a workaround that should be more specific solving MOBILE-3305. + // Also should take into account all modules are not downloaded. + + // Mark course as not downloaded if course size is 0. + if (this.totalSize == 0) { + this.markCourseAsNotDownloaded(); + } + } + } + + /** + * Mark course as not downloaded. + */ + protected markCourseAsNotDownloaded(): void { + // @TODO This is a workaround that should be more specific solving MOBILE-3305. + // Also should take into account all modules are not downloaded. + // Check after MOBILE-3188 is integrated. + + CoreCourse.setCourseStatus(this.course.id, CoreConstants.NOT_DOWNLOADED); + } + +} + +type AddonStorageManagerCourseSection = Omit & { + totalSize?: number; + modules: AddonStorageManagerModule[]; +}; + +type AddonStorageManagerModule = CoreCourseModule & { + parentSection?: AddonStorageManagerCourseSection; + totalSize?: number; +}; diff --git a/src/addons/storagemanager/pages/courses-storage/courses-storage.html b/src/addons/storagemanager/pages/courses-storage/courses-storage.html new file mode 100644 index 000000000..c2a89bfe6 --- /dev/null +++ b/src/addons/storagemanager/pages/courses-storage/courses-storage.html @@ -0,0 +1,48 @@ + + + + + + {{ 'addon.storagemanager.managestorage' | translate }} + + + + + + + {{ 'core.courses.courses' | translate }} +

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

+ + +

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

+

{{ totalSize | coreBytesToSize }}

+ + + + +
+
+
+ + + + + +

{{ course.displayname }}

+

{{ course.fullname }}

+

{{ 'core.downloading' | translate }}

+
+

{{ course.totalSize | coreBytesToSize }}

+ + + + +
+
+
+
+
+
diff --git a/src/addons/storagemanager/pages/courses-storage/courses-storage.scss b/src/addons/storagemanager/pages/courses-storage/courses-storage.scss new file mode 100644 index 000000000..92372be0b --- /dev/null +++ b/src/addons/storagemanager/pages/courses-storage/courses-storage.scss @@ -0,0 +1,14 @@ +@import "~theme/globals"; + +:host { + ion-item.course { + h2 { + font-weight: bold; + } + + h3 { + color: $subdued-text-color; + } + } + +} diff --git a/src/addons/storagemanager/pages/courses-storage/courses-storage.ts b/src/addons/storagemanager/pages/courses-storage/courses-storage.ts new file mode 100644 index 000000000..ccae867a4 --- /dev/null +++ b/src/addons/storagemanager/pages/courses-storage/courses-storage.ts @@ -0,0 +1,209 @@ +// (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 { CoreConstants } from '@/core/constants'; +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { CoreCourse, CoreCourseProvider } from '@features/course/services/course'; +import { CoreCourseHelper } from '@features/course/services/course-helper'; +import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; +import { CoreCourses, CoreEnrolledCourseData } from '@features/courses/services/courses'; +import { CoreDomUtils } from '@services/utils/dom'; +import { Translate } from '@singletons'; +import { CoreArray } from '@singletons/array'; +import { CoreEventObserver, CoreEvents } from '@singletons/events'; + +/** + * Page that displays downloaded courses and allows the user to delete them. + */ +@Component({ + selector: 'page-addon-storagemanager-courses-storage', + templateUrl: 'courses-storage.html', + styleUrls: ['courses-storage.scss'], +}) +export class AddonStorageManagerCoursesStoragePage implements OnInit, OnDestroy { + + userCourses: CoreEnrolledCourseData[] = []; + downloadedCourses: DownloadedCourse[] = []; + completelyDownloadedCourses: DownloadedCourse[] = []; + totalSize = 0; + loaded = false; + + courseStatusObserver?: CoreEventObserver; + + /** + * View loaded. + */ + async ngOnInit(): Promise { + this.userCourses = await CoreCourses.getUserCourses(); + this.courseStatusObserver = CoreEvents.on( + CoreEvents.COURSE_STATUS_CHANGED, + ({ courseId, status }) => this.onCourseUpdated(courseId, status), + ); + + const downloadedCourseIds = await CoreCourse.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?.off(); + } + + /** + * Delete all courses that have been downloaded. + */ + async deleteCompletelyDownloadedCourses(): Promise { + try { + await CoreDomUtils.showDeleteConfirm('core.course.confirmdeletestoreddata'); + } catch (error) { + if (!CoreDomUtils.isCanceledError(error)) { + throw error; + } + + return; + } + + const modal = await CoreDomUtils.showModalLoading(); + const deletedCourseIds = this.completelyDownloadedCourses.map((course) => course.id); + + try { + await Promise.all(deletedCourseIds.map((courseId) => CoreCourseHelper.deleteCourseFiles(courseId))); + + this.setDownloadedCourses(this.downloadedCourses.filter((course) => !CoreArray.contains(deletedCourseIds, course.id))); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, Translate.instant('core.errordeletefile')); + } finally { + modal.dismiss(); + } + } + + /** + * Delete course. + * + * @param course Course to delete. + */ + async deleteCourse(course: DownloadedCourse): Promise { + try { + await CoreDomUtils.showDeleteConfirm('core.course.confirmdeletestoreddata'); + } catch (error) { + if (!CoreDomUtils.isCanceledError(error)) { + throw error; + } + + return; + } + + const modal = await CoreDomUtils.showModalLoading(); + + try { + await CoreCourseHelper.deleteCourseFiles(course.id); + + this.setDownloadedCourses(CoreArray.withoutItem(this.downloadedCourses, course)); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, Translate.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.sort((a, b) => b.totalSize - a.totalSize); + 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: CoreEnrolledCourseData): Promise { + const totalSize = await this.calculateDownloadedCourseSize(course.id); + const status = await CoreCourse.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.getSections(courseId); + const modules = CoreArray.flatten(sections.map((section) => section.modules)); + const promisedModuleSizes = modules.map(async (module) => { + const size = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, courseId); + + return isNaN(size) ? 0 : size; + }); + const moduleSizes = await Promise.all(promisedModuleSizes); + + return moduleSizes.reduce((totalSize, moduleSize) => totalSize + moduleSize, 0); + } + +} + +/** + * Downloaded course data. + */ +interface DownloadedCourse extends CoreEnrolledCourseData { + totalSize: number; + isDownloading: boolean; +} diff --git a/src/addons/storagemanager/services/handlers/course-menu.ts b/src/addons/storagemanager/services/handlers/course-menu.ts new file mode 100644 index 000000000..3d9cc77d1 --- /dev/null +++ b/src/addons/storagemanager/services/handlers/course-menu.ts @@ -0,0 +1,59 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreCourseOptionsMenuHandler, CoreCourseOptionsMenuHandlerData } from '@features/course/services/course-options-delegate'; +import { CoreCourseAnyCourseDataWithOptions } from '@features/courses/services/courses'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to inject an option into course menu so that user can get to the manage storage page. + */ +@Injectable( { providedIn: 'root' }) +export class AddonStorageManagerCourseMenuHandlerService implements CoreCourseOptionsMenuHandler { + + name = 'AddonStorageManager'; + priority = 500; + isMenuHandler = true; + + /** + * @inheritdoc + */ + async isEnabledForCourse(): Promise { + return true; + } + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return true; + } + + /** + * @inheritdoc + */ + getMenuDisplayData( + course: CoreCourseAnyCourseDataWithOptions, + ): CoreCourseOptionsMenuHandlerData { + return { + icon: 'fas-archive', + title: 'addon.storagemanager.managestorage', + page: 'storage/' + course.id, + class: 'addon-storagemanager-coursemenu-handler', + }; + } + +} +export const AddonStorageManagerCourseMenuHandler = makeSingleton(AddonStorageManagerCourseMenuHandlerService); diff --git a/src/addons/storagemanager/storagemanager-lazy.module.ts b/src/addons/storagemanager/storagemanager-lazy.module.ts new file mode 100644 index 000000000..7eeeac233 --- /dev/null +++ b/src/addons/storagemanager/storagemanager-lazy.module.ts @@ -0,0 +1,44 @@ +// (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 { RouterModule, Routes } from '@angular/router'; + +import { CoreSharedModule } from '@/core/shared.module'; +import { AddonStorageManagerCoursesStoragePage } from './pages/courses-storage/courses-storage'; +import { AddonStorageManagerCourseStoragePage } from './pages/course-storage/course-storage'; + +const routes: Routes = [ + { + path: 'storage', + component: AddonStorageManagerCoursesStoragePage, + }, + { + path: 'storage/:courseId', + component: AddonStorageManagerCourseStoragePage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + ], + exports: [RouterModule], + declarations: [ + AddonStorageManagerCoursesStoragePage, + AddonStorageManagerCourseStoragePage, + ], +}) +export class AddonStorageManagerLazyModule {} diff --git a/src/addons/storagemanager/storagemanager.module.ts b/src/addons/storagemanager/storagemanager.module.ts new file mode 100644 index 000000000..771b965cb --- /dev/null +++ b/src/addons/storagemanager/storagemanager.module.ts @@ -0,0 +1,46 @@ +// (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, APP_INITIALIZER } from '@angular/core'; +import { Routes } from '@angular/router'; +import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate'; +import { CoreMainMenuRoutingModule } from '@features/mainmenu/mainmenu-routing.module'; +import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; +import { AddonStorageManagerCourseMenuHandler } from './services/handlers/course-menu'; + +const routes: Routes = [ + { + path: '', + loadChildren: () => import('@addons/storagemanager/storagemanager-lazy.module').then(m => m.AddonStorageManagerLazyModule), + }, +]; + +@NgModule({ + imports: [ + CoreMainMenuTabRoutingModule.forChild(routes), + CoreMainMenuRoutingModule.forChild({ children: routes }), + ], + exports: [CoreMainMenuRoutingModule], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + deps: [], + useFactory: () => async () => { + CoreCourseOptionsDelegate.registerHandler(AddonStorageManagerCourseMenuHandler.instance); + }, + }, + ], +}) +export class AddonStorageManagerModule {} diff --git a/src/assets/exttomime.json b/src/assets/exttomime.json index 2728d8b3d..719abc607 100644 --- a/src/assets/exttomime.json +++ b/src/assets/exttomime.json @@ -5,7 +5,8 @@ "3dml": {"type":"text/vnd.in3d.3dml"}, "3ds": {"type":"image/x-3ds"}, "3g2": {"type":"video/3gpp2"}, -"3gp": {"type":"video/quicktime","icon":"quicktime","string":"video","groups":["video"]}, +"3gp": {"type":"video/3gpp","icon":"quicktime","string":"video","groups":["video"]}, +"3gpp": {"type":"video/3gpp","icon":"quicktime","string":"video","groups":["video"]}, "7z": {"type":"application/x-7z-compressed","icon":"archive","string":"archive","groups":["archive"]}, "a": {"type":"application/octet-stream"}, "aab": {"type":"application/x-authorware-bin"}, @@ -1269,4 +1270,4 @@ "zmm": {"type":"application/vnd.handheld-entertainment+xml"}, "zoo": {"type":"application/octet-stream"}, "zsh": {"type":"text/x-script.zsh"} -} \ No newline at end of file +} diff --git a/src/assets/mimetoext.json b/src/assets/mimetoext.json index 438731f8b..5059dc720 100644 --- a/src/assets/mimetoext.json +++ b/src/assets/mimetoext.json @@ -1018,7 +1018,7 @@ "text/x-vcard": ["vcf"], "text/xml": ["resx","jcb","jcw","jmt","jmx","jcl","xsl","rhb","sqt","xml","jqz"], "text/yaml": ["yaml","yml"], -"video/3gpp": ["3gp"], +"video/3gpp": ["3gp", "3gpp"], "video/3gpp2": ["3g2"], "video/animaflex": ["afl"], "video/avi": ["avi"], @@ -1092,4 +1092,4 @@ "x-world/x-vrt": ["vrt"], "xgl/drawing": ["xgz"], "xgl/movie": ["xmz"] -} \ No newline at end of file +} diff --git a/src/core/features/courses/pages/dashboard/dashboard.ts b/src/core/features/courses/pages/dashboard/dashboard.ts index 7aa5bd0a2..1c67c49c9 100644 --- a/src/core/features/courses/pages/dashboard/dashboard.ts +++ b/src/core/features/courses/pages/dashboard/dashboard.ts @@ -160,8 +160,7 @@ export class CoreCoursesDashboardPage implements OnInit, OnDestroy { * Open page to manage courses storage. */ manageCoursesStorage(): void { - // AddonStorageManagerCoursesStoragePage - // @todo this.navCtrl.navigateForward(['/main/home/courses/storage']); + CoreNavigator.navigateToSitePath('/storage'); } /** diff --git a/src/core/features/settings/lang.json b/src/core/features/settings/lang.json index 96673bccc..9252a449f 100644 --- a/src/core/features/settings/lang.json +++ b/src/core/features/settings/lang.json @@ -6,7 +6,8 @@ "cannotsyncoffline": "Cannot synchronise offline.", "cannotsyncwithoutwifi": "Cannot synchronise because the current settings only allow to synchronise when connected to Wi-Fi. Please connect to a Wi-Fi network.", "colorscheme": "Color Scheme", - "colorscheme-auto": "Auto (based on system settings)", + "colorscheme-system": "System default", + "colorscheme-system-notice": "System default mode will depend on your device support.", "colorscheme-dark": "Dark", "colorscheme-light": "Light", "compilationinfo": "Compilation info", diff --git a/src/core/features/settings/pages/general/general.html b/src/core/features/settings/pages/general/general.html index d7689e890..5cd2ad929 100644 --- a/src/core/features/settings/pages/general/general.html +++ b/src/core/features/settings/pages/general/general.html @@ -32,7 +32,8 @@ - +

{{ 'core.settings.colorscheme' | translate }}

{{ 'core.settings.forcedsetting' | translate }}

@@ -43,6 +44,9 @@ {{ 'core.settings.colorscheme-' + scheme | translate }}
+ +

{{ 'core.settings.colorscheme-system-notice' | translate }}

+

{{ 'core.settings.enablerichtexteditor' | translate }}

diff --git a/src/core/features/settings/pages/general/general.ts b/src/core/features/settings/pages/general/general.ts index 9ac33264d..ff2270908 100644 --- a/src/core/features/settings/pages/general/general.ts +++ b/src/core/features/settings/pages/general/general.ts @@ -20,6 +20,7 @@ import { CoreLang } from '@services/lang'; import { CoreDomUtils } from '@services/utils/dom'; import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { CoreSettingsHelper, CoreColorScheme, CoreZoomLevel } from '../../services/settings-helper'; +import { CoreApp } from '@services/app'; /** * Page that displays the general settings. @@ -42,6 +43,7 @@ export class CoreSettingsGeneralPage { colorSchemes: CoreColorScheme[] = []; selectedScheme: CoreColorScheme = CoreColorScheme.LIGHT; colorSchemeDisabled = false; + isAndroid = false; constructor() { this.asyncInit(); @@ -72,14 +74,8 @@ export class CoreSettingsGeneralPage { this.colorSchemes.push(CoreColorScheme.LIGHT); this.selectedScheme = this.colorSchemes[0]; } else { - this.colorSchemes.push(CoreColorScheme.LIGHT); - this.colorSchemes.push(CoreColorScheme.DARK); - - if (window.matchMedia('(prefers-color-scheme: dark)').matches || - window.matchMedia('(prefers-color-scheme: light)').matches) { - this.colorSchemes.push(CoreColorScheme.AUTO); - } - + this.isAndroid = CoreApp.isAndroid(); + this.colorSchemes = CoreSettingsHelper.getAllowedColorSchemes(); this.selectedScheme = await CoreConfig.get(CoreConstants.SETTINGS_COLOR_SCHEME, CoreColorScheme.LIGHT); } } diff --git a/src/core/features/settings/services/settings-helper.ts b/src/core/features/settings/services/settings-helper.ts index 074433da2..6bbe6e12c 100644 --- a/src/core/features/settings/services/settings-helper.ts +++ b/src/core/features/settings/services/settings-helper.ts @@ -43,7 +43,7 @@ export interface CoreSiteSpaceUsage { * Constants to define color schemes. */ export const enum CoreColorScheme { - AUTO = 'auto', + SYSTEM = 'system', LIGHT = 'light', DARK = 'dark', } @@ -65,6 +65,7 @@ export class CoreSettingsHelperProvider { protected syncPromises: { [s: string]: Promise } = {}; protected prefersDark?: MediaQueryList; + protected colorSchemes: CoreColorScheme[] = []; constructor() { if (!CoreConstants.CONFIG.forceColorScheme) { @@ -404,13 +405,37 @@ export class CoreSettingsHelperProvider { document.documentElement.style.zoom = zoom + '%'; } + /** + * Get system allowed color schemes. + * + * @return Allowed color schemes. + */ + getAllowedColorSchemes(): CoreColorScheme[] { + if (this.colorSchemes.length > 0) { + return this.colorSchemes; + } + + if (!CoreConstants.CONFIG.forceColorScheme) { + this.colorSchemes.push(CoreColorScheme.LIGHT); + this.colorSchemes.push(CoreColorScheme.DARK); + + if (this.canIUsePrefersColorScheme()) { + this.colorSchemes.push(CoreColorScheme.SYSTEM); + } + } else { + this.colorSchemes = [CoreConstants.CONFIG.forceColorScheme]; + } + + return this.colorSchemes; + } + /** * Set body color scheme. * * @param colorScheme Name of the color scheme. */ setColorScheme(colorScheme: CoreColorScheme): void { - if (colorScheme == CoreColorScheme.AUTO && this.prefersDark) { + if (colorScheme == CoreColorScheme.SYSTEM && this.prefersDark) { // Listen for changes to the prefers-color-scheme media query. this.prefersDark.addEventListener('change', this.toggleDarkModeListener); @@ -423,6 +448,18 @@ export class CoreSettingsHelperProvider { } } + /** + * Check if device can detect color scheme system preference. + * https://caniuse.com/prefers-color-scheme + * + * @returns if the color scheme system preference is avalaible. + */ + canIUsePrefersColorScheme(): boolean { + // The following check will check browser support but system may differ from that. + // @todo Detect SO support to watch media query. + return window.matchMedia('(prefers-color-scheme)').media !== 'not all'; + } + /** * Listener function to toggle dark mode. * diff --git a/src/core/features/sitehome/pages/index/index.ts b/src/core/features/sitehome/pages/index/index.ts index 19534eb1f..591da63fd 100644 --- a/src/core/features/sitehome/pages/index/index.ts +++ b/src/core/features/sitehome/pages/index/index.ts @@ -211,7 +211,7 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { * Open page to manage courses storage. */ manageCoursesStorage(): void { - // @todo this.navCtrl.navigateForward(['/main/home/courses/storage']); + CoreNavigator.navigateToSitePath('/storage'); } /** diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index 5a5d4ecd1..801d0a114 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -309,7 +309,7 @@ export class CoreSitesProvider { siteUrl = CoreUrlUtils.removeUrlParams(siteUrl); try { - data = await Http.post(siteUrl + '/login/token.php', {}).pipe(timeout(CoreWS.getRequestTimeout())) + data = await Http.post(siteUrl + '/login/token.php', { appsitecheck: 1 }).pipe(timeout(CoreWS.getRequestTimeout())) .toPromise(); } catch (error) { // Default error messages are kinda bad, return our own message. diff --git a/src/core/services/utils/iframe.ts b/src/core/services/utils/iframe.ts index f14a84b34..064f24380 100644 --- a/src/core/services/utils/iframe.ts +++ b/src/core/services/utils/iframe.ts @@ -340,7 +340,7 @@ export class CoreIframeUtilsProvider { // Find the link being clicked. let el: Element | null = event.target as Element; - while (el && el.tagName !== 'A') { + while (el && el.tagName !== 'A' && el.tagName !== 'a') { el = el.parentElement; } diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index 34ec0ce08..641e22700 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -52,6 +52,15 @@ ion-item.ion-text-wrap ion-label { } } +@each $color-name, $value in $colors { + $value: map-get($colors, $color-name); + $base: map-get($value, base); + + .text-#{$color-name}, + p.text-#{$color-name} { + color: $base; + } +} // Ionic toolbar. ion-toolbar ion-back-button, @@ -466,3 +475,8 @@ ion-button.core-button-select { display: none !important; } } + +// Hide virtual utilities. +.core-browser-copy-area { + display: none; +}