Merge pull request #2709 from crazyserver/MOBILE-3635

Mobile 3635
main
Dani Palou 2021-03-17 11:42:43 +01:00 committed by GitHub
commit 1e3863e949
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 811 additions and 23 deletions

View File

@ -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 {}

View File

@ -15,7 +15,7 @@
</ion-header>
<ion-content>
<ion-refresher slot="fixed"
[disabled]="!activityComponent?.loaded || activityComponent?.mode != 'external'"
[disabled]="!activityComponent?.loaded || activityComponent?.mode == 'iframe'"
(ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>

View File

@ -133,7 +133,7 @@ export class AddonModResourceHelperProvider {
return false;
}
return mimetype == 'text/html';
return mimetype == 'text/html' || mimetype == 'application/xhtml+xml';
}
/**

View File

@ -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:"
}

View File

@ -0,0 +1,67 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>{{ 'addon.storagemanager.managestorage' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded">
<ion-card class="wholecourse">
<ion-card-header>
<ion-card-title *ngIf="course.displayname">{{ course.displayname }}</ion-card-title>
<ion-card-title *ngIf="!course.displayname">{{ course.fullname }}</ion-card-title>
<p class="ion-text-wrap">{{ 'addon.storagemanager.info' | translate }}</p>
<ion-item class="size ion-text-wrap ion-no-padding" lines="none">
<ion-icon name="fas-archive" slot="start"></ion-icon>
<ion-label>
<h2 class="ion-text-wrap">{{ 'addon.storagemanager.storageused' | translate }}</h2>
</ion-label>
<p slot="end" class="ion-text-end">{{ totalSize | coreBytesToSize }}</p>
<ion-button slot="end" (click)="deleteForCourse()" [disabled]="totalSize == 0">
<ion-icon name="fas-trash" slot="icon-only"
ariaLabel="{{ 'addon.storagemanager.deletecourse' | translate }}">
</ion-icon>
</ion-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 class="ion-no-padding">
<ion-label>
<h2 class="ion-text-wrap">{{ section.name }}</h2>
</ion-label>
<p slot="end" class="ion-text-end">{{ section.totalSize | coreBytesToSize }}</p>
<ion-button slot="end" (click)="deleteForSection(section)">
<ion-icon name="fas-trash" slot="icon-only"
ariaLabel="{{ 'addon.storagemanager.deletedatafrom' | translate: { name: section.name } }}">
</ion-icon>
</ion-button>
</ion-item>
</ion-card-header>
<ion-card-content>
<ng-container *ngFor="let module of section.modules">
<ion-item class="ion-no-padding" *ngIf="module.totalSize! > 0">
<img *ngIf="module.handlerData!.icon" [src]="module.handlerData!.icon" alt="" role="presentation"
class="core-module-icon" slot="start">
<ion-label class="ion-text-wrap">
<h3 class="{{module.handlerData!.class}} addon-storagemanager-module-size">
{{ module.name }}
</h3>
</ion-label>
<p slot="end" class="ion-text-end">{{ module.totalSize | coreBytesToSize }}</p>
<ion-button fill="clear" slot="end" (click)="deleteForModule(module)">
<ion-icon name="fas-trash" slot="icon-only"
ariaLabel="{{ 'addon.storagemanager.deletedatafrom' | translate: { name: module.name } }}">
</ion-icon>
</ion-button>
</ion-item>
</ng-container>
</ion-card-content>
</ion-card>
</ng-container>
</core-loading>
</ion-content>

View File

@ -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;
}
}

View File

@ -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<void> {
this.course = CoreNavigator.getRouteParam<CoreEnrolledCourseData>('course')!;
this.sections = await CoreCourse.getSections(this.course.id, false, true);
CoreCourseHelper.addHandlerDataForModules(this.sections, this.course.id);
this.totalSize = 0;
const promises: Promise<void>[] = [];
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<void> {
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<void> {
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<void> {
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<void> Once deleting has finished
*/
protected async deleteModules(modules: AddonStorageManagerModule[]): Promise<void> {
const modal = await CoreDomUtils.showModalLoading();
const promises: Promise<void>[] = [];
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<CoreCourseSection, 'modules'> & {
totalSize?: number;
modules: AddonStorageManagerModule[];
};
type AddonStorageManagerModule = CoreCourseModule & {
parentSection?: AddonStorageManagerCourseSection;
totalSize?: number;
};

View File

@ -0,0 +1,48 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>{{ 'addon.storagemanager.managestorage' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded">
<ion-card>
<ion-card-header>
<ion-card-title class="ion-text-wrap">{{ 'core.courses.courses' | translate }}</ion-card-title>
<p class="ion-text-wrap">{{ 'addon.storagemanager.info' | translate }}</p>
<ion-item class="size ion-text-wrap ion-no-padding" lines="none">
<ion-icon name="fas-archive" slot="start"></ion-icon>
<ion-label><h2 class="ion-text-wrap">{{ 'addon.storagemanager.storageused' | translate }}</h2></ion-label>
<p slot="end" class="ion-text-end">{{ totalSize | coreBytesToSize }}</p>
<ion-button slot="end" (click)="deleteCompletelyDownloadedCourses()"
[disabled]="completelyDownloadedCourses.length === 0">
<ion-icon name="fas-trash" slot="icon-only"
ariaLabel="{{ 'addon.storagemanager.deletecourses' | translate }}">
</ion-icon>
</ion-button>
</ion-item>
</ion-card-header>
</ion-card>
<ion-card>
<ion-card-content class="ion-no-padding">
<ion-list>
<ion-item *ngFor="let course of downloadedCourses" class="course">
<ion-label class="ion-text-wrap">
<h2 *ngIf="course.displayname">{{ course.displayname }}</h2>
<h2 *ngIf="!course.displayname">{{ course.fullname }}</h2>
<h3 *ngIf="course.isDownloading">{{ 'core.downloading' | translate }}</h3>
</ion-label>
<p slot="end" class="ion-text-end">{{ course.totalSize | coreBytesToSize }}</p>
<ion-button slot="end" (click)="deleteCourse(course)" [disabled]="course.isDownloading">
<ion-icon name="fas-trash" slot="icon-only"
ariaLabel="{{ 'addon.storagemanager.deletedatafrom' | translate: { name: course.displayname } }}">
</ion-icon>
</ion-button>
</ion-item>
</ion-list>
</ion-card-content>
</ion-card>
</core-loading>
</ion-content>

View File

@ -0,0 +1,14 @@
@import "~theme/globals";
:host {
ion-item.course {
h2 {
font-weight: bold;
}
h3 {
color: $subdued-text-color;
}
}
}

View File

@ -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<void> {
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<void> {
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<void> {
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<void> {
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<DownloadedCourse> {
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<number> {
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;
}

View File

@ -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<boolean> {
return true;
}
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
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);

View File

@ -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 {}

View File

@ -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 {}

View File

@ -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"}
}
}

View File

@ -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"]
}
}

View File

@ -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');
}
/**

View File

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

View File

@ -32,7 +32,8 @@
</ion-segment-button>
</ion-segment>
</ion-item>
<ion-item class="ion-text-wrap core-settings-general-color-scheme" *ngIf="colorSchemes.length > 0">
<ion-item class="ion-text-wrap core-settings-general-color-scheme" *ngIf="colorSchemes.length > 0"
[lines]="selectedScheme=='system' && isAndroid ? 'none' : 'inset'">
<ion-label>
<h2>{{ 'core.settings.colorscheme' | translate }}</h2>
<p *ngIf="colorSchemeDisabled" class="text-danger">{{ 'core.settings.forcedsetting' | translate }}</p>
@ -43,6 +44,9 @@
{{ 'core.settings.colorscheme-' + scheme | translate }}</ion-select-option>
</ion-select>
</ion-item>
<ion-item text-wrap *ngIf="colorSchemes.length > 0 && selectedScheme=='system' && isAndroid">
<ion-label><p>{{ 'core.settings.colorscheme-system-notice' | translate }}</p></ion-label>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<h2>{{ 'core.settings.enablerichtexteditor' | translate }}</h2>

View File

@ -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);
}
}

View File

@ -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<void> } = {};
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.
*

View File

@ -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');
}
/**

View File

@ -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.

View File

@ -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;
}

View File

@ -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;
}