Merge pull request #3211 from dpalou/MOBILE-3833

Mobile 3833
main
Pau Ferrer Ocaña 2022-03-30 11:27:20 +02:00 committed by GitHub
commit 771ee90d6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 295 additions and 183 deletions

View File

@ -130,11 +130,6 @@
<param name="android-package" value="org.apache.cordova.geolocation.Geolocation" />
</feature>
</config-file>
<config-file parent="/*" target="res/xml/config.xml">
<feature name="Globalization">
<param name="android-package" value="org.apache.cordova.globalization.Globalization" />
</feature>
</config-file>
<config-file parent="/*" target="res/xml/config.xml">
<feature name="InAppBrowser">
<param name="android-package" value="org.apache.cordova.inappbrowser.InAppBrowser" />

18
package-lock.json generated
View File

@ -71,7 +71,6 @@
"cordova-plugin-file": "6.0.2",
"cordova-plugin-file-opener2": "3.0.5",
"cordova-plugin-geolocation": "4.1.0",
"cordova-plugin-globalization": "1.11.0",
"cordova-plugin-ionic-keyboard": "2.2.0",
"cordova-plugin-media": "5.0.4",
"cordova-plugin-media-capture": "3.0.3",
@ -11389,18 +11388,6 @@
}
}
},
"node_modules/cordova-plugin-globalization": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/cordova-plugin-globalization/-/cordova-plugin-globalization-1.11.0.tgz",
"integrity": "sha1-6sMVgQAphJOvowvolA5pj2HvvP4=",
"engines": {
"cordovaDependencies": {
"2.0.0": {
"cordova": ">100"
}
}
}
},
"node_modules/cordova-plugin-ionic-keyboard": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/cordova-plugin-ionic-keyboard/-/cordova-plugin-ionic-keyboard-2.2.0.tgz",
@ -40148,11 +40135,6 @@
"resolved": "https://registry.npmjs.org/cordova-plugin-geolocation/-/cordova-plugin-geolocation-4.1.0.tgz",
"integrity": "sha512-y5io/P10xGMxSn2KEqfv/fExK47eA1pmSonJdmDqDsaSADV9JpgdPx0mUSA08+5pzma/OS9R0LoODeDPx7Jvjg=="
},
"cordova-plugin-globalization": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/cordova-plugin-globalization/-/cordova-plugin-globalization-1.11.0.tgz",
"integrity": "sha1-6sMVgQAphJOvowvolA5pj2HvvP4="
},
"cordova-plugin-ionic-keyboard": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/cordova-plugin-ionic-keyboard/-/cordova-plugin-ionic-keyboard-2.2.0.tgz",

View File

@ -100,7 +100,6 @@
"cordova-plugin-file": "6.0.2",
"cordova-plugin-file-opener2": "3.0.5",
"cordova-plugin-geolocation": "4.1.0",
"cordova-plugin-globalization": "1.11.0",
"cordova-plugin-ionic-keyboard": "2.2.0",
"cordova-plugin-media": "5.0.4",
"cordova-plugin-media-capture": "3.0.3",
@ -236,7 +235,6 @@
"ANDROIDX_VERSION": "1.0.0",
"ANDROIDX_APPCOMPAT_VERSION": "1.3.1"
},
"cordova-plugin-globalization": {},
"@moodlehq/cordova-plugin-file-transfer": {},
"cordova-plugin-prevent-override": {},
"cordova-plugin-androidx-adapter": {}
@ -245,4 +243,4 @@
"optionalDependencies": {
"keytar": "7.2.0"
}
}
}

View File

@ -1460,6 +1460,7 @@
"core.block.tour_navigation_dashboard_content": "tool_usertours",
"core.block.tour_navigation_dashboard_title": "tool_usertours",
"core.browser": "local_moodlemobileapp",
"core.calculating": "local_moodlemobileapp",
"core.cancel": "moodle",
"core.cannotconnect": "local_moodlemobileapp",
"core.cannotconnecttrouble": "local_moodlemobileapp",
@ -1680,6 +1681,7 @@
"core.editor.underline": "atto_underline/pluginname",
"core.editor.unorderedlist": "atto_unorderedlist/pluginname",
"core.emptysplit": "local_moodlemobileapp",
"core.endonesteptour": "tool_usertours",
"core.error": "moodle",
"core.errorchangecompletion": "local_moodlemobileapp",
"core.errordeletefile": "local_moodlemobileapp",
@ -2349,7 +2351,6 @@
"core.usernotfullysetup": "error",
"core.users": "moodle",
"core.usersuspended": "tool_reportbuilder",
"core.endonesteptour": "tool_usertours",
"core.view": "moodle",
"core.viewcode": "local_moodlemobileapp",
"core.vieweditor": "local_moodlemobileapp",

View File

@ -23,8 +23,8 @@
<ion-item class="ion-text-wrap core-group-selector">
<ion-label id="addon-bigbluebuttonbn-groupslabel">
<ng-container *ngIf="groupInfo.separateGroups">{{'core.groupsseparate' | translate }}</ng-container>
<ng-container *ngIf="groupInfo.visibleGroups">{{'core.groupsvisible' | translate }}</ng-container>
<ng-container *ngIf="groupInfo.separateGroups">{{ 'core.groupsseparate' | translate }}</ng-container>
<ng-container *ngIf="groupInfo.visibleGroups">{{ 'core.groupsvisible' | translate }}</ng-container>
</ion-label>
<ion-select [(ngModel)]="groupId" (ionChange)="groupChanged()" aria-labelledby="addon-bigbluebuttonbn-groupslabel"
interface="action-sheet" [interfaceOptions]="{header: 'core.group' | translate}">

View File

@ -218,6 +218,12 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
}
this.groupInfo = await CoreGroups.getActivityGroupInfo(this.database.coursemodule);
if (this.groupInfo.visibleGroups && this.groupInfo.groups?.length) {
// There is a bug in Moodle with All participants and visible groups (MOBILE-3597). Remove it.
this.groupInfo.groups = this.groupInfo.groups.filter(group => group.id !== 0);
this.groupInfo.defaultGroupId = this.groupInfo.groups[0].id;
}
this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo);
this.access = await AddonModData.getDatabaseAccessInformation(this.database.id, {

View File

@ -20,8 +20,8 @@
<core-loading [hideUntil]="loaded">
<ion-item class="ion-text-wrap core-group-selector" *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)">
<ion-label id="addon-data-groupslabel">
<ng-container *ngIf="groupInfo.separateGroups">{{ 'core.groupsvisible' | translate }}</ng-container>
<ng-container *ngIf="groupInfo.visibleGroups">{{ 'core.groupsseparate' | translate }}</ng-container>
<ng-container *ngIf="groupInfo.separateGroups">{{ 'core.groupsseparate' | translate }}</ng-container>
<ng-container *ngIf="groupInfo.visibleGroups">{{ 'core.groupsvisible' | translate }}</ng-container>
</ion-label>
<ion-select [(ngModel)]="selectedGroup" (ionChange)="setGroup(selectedGroup)" aria-labelledby="addon-data-groupslabel"
interface="action-sheet" [interfaceOptions]="{header: 'core.group' | translate}">

View File

@ -163,8 +163,10 @@ export class AddonModDataEditPage implements OnInit {
const entry = await AddonModDataHelper.fetchEntry(this.database, this.fieldsArray, this.entryId || 0);
this.entry = entry.entry;
// Load correct group.
this.selectedGroup = this.entry.groupid;
if (this.entryId) {
// Load correct group.
this.selectedGroup = this.entry.groupid;
}
// Check permissions when adding a new entry or offline entry.
if (!this.isEditing) {
@ -172,6 +174,12 @@ export class AddonModDataEditPage implements OnInit {
if (refresh) {
this.groupInfo = await CoreGroups.getActivityGroupInfo(this.database.coursemodule);
if (this.groupInfo.visibleGroups && this.groupInfo.groups?.length) {
// There is a bug in Moodle with All participants and visible groups (MOBILE-3597). Remove it.
this.groupInfo.groups = this.groupInfo.groups.filter(group => group.id !== 0);
this.groupInfo.defaultGroupId = this.groupInfo.groups[0].id;
}
this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo);
this.initialSelectedGroup = this.selectedGroup;
}

View File

@ -174,6 +174,12 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy {
this.access = await AddonModData.getDatabaseAccessInformation(this.database.id, { cmId: this.moduleId });
this.groupInfo = await CoreGroups.getActivityGroupInfo(this.database.coursemodule);
if (this.groupInfo.visibleGroups && this.groupInfo.groups?.length) {
// There is a bug in Moodle with All participants and visible groups (MOBILE-3597). Remove it.
this.groupInfo.groups = this.groupInfo.groups.filter(group => group.id !== 0);
this.groupInfo.defaultGroupId = this.groupInfo.groups[0].id;
}
this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo);
const actions = AddonModDataHelper.getActions(this.database, this.access, this.entry!);

View File

@ -61,8 +61,8 @@
<ion-list *ngIf="access && access.canviewanalysis && !access.isempty">
<ion-item class="ion-text-wrap core-group-selector" *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)">
<ion-label id="addon-feedback-groupslabel">
<ng-container *ngIf="groupInfo.separateGroups">{{'core.groupsseparate' | translate }}</ng-container>
<ng-container *ngIf="groupInfo.visibleGroups">{{'core.groupsvisible' | translate }}</ng-container>
<ng-container *ngIf="groupInfo.separateGroups">{{ 'core.groupsseparate' | translate }}</ng-container>
<ng-container *ngIf="groupInfo.visibleGroups">{{ 'core.groupsvisible' | translate }}</ng-container>
</ion-label>
<ion-select [(ngModel)]="group" (ionChange)="setGroup(group)" aria-labelledby="addon-feedback-groupslabel"
interface="action-sheet" [interfaceOptions]="{header: 'core.group' | translate}">

View File

@ -24,15 +24,18 @@
<ion-label>
<p class="item-heading ion-text-wrap">{{ 'addon.storagemanager.totaldownloads' | translate }}</p>
</ion-label>
<ion-badge color="light" slot="end">{{ totalSize | coreBytesToSize }}
<ion-badge color="light" slot="end">
<ng-container *ngIf="!calculatingSize">{{ totalSize | coreBytesToSize }}</ng-container>
<ng-container *ngIf="calculatingSize">{{ 'core.calculating' | translate }}</ng-container>
</ion-badge>
</ion-item>
<ion-button *ngIf="downloadCourseEnabled" (click)="prefetchCourse()" expand="block" fill="outline" class="ion-no-margin">
<ion-button *ngIf="downloadCourseEnabled" (click)="prefetchCourse()" expand="block" fill="outline" class="ion-no-margin"
[disabled]="prefetchCourseData.loading">
<ion-icon *ngIf="!prefetchCourseData.loading" [name]="prefetchCourseData.icon" slot="start"></ion-icon>
<ion-spinner *ngIf="prefetchCourseData.loading" slot="start"></ion-spinner>
{{ prefetchCourseData.statusTranslatable | translate }}
</ion-button>
<ion-button *ngIf="totalSize > 0" (click)="deleteForCourse()" expand="block" color="danger"
<ion-button [disabled]="calculatingSize || totalSize <= 0" (click)="deleteForCourse()" expand="block" color="danger"
class="ion-no-margin ion-margin-top">
<ion-icon name="fas-trash" slot="start" [attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate:
{ name: title }">
@ -44,7 +47,13 @@
<ng-container *ngFor="let section of sections">
<ion-card class="section" *ngIf="section.modules.length > 0">
<ion-card-header>
<ion-item class="ion-no-padding" lines="full">
<ion-item class="ion-no-padding" [lines]="section.expanded ? 'full' : 'none'" button detail="false"
(click)="toggleExpand($event, section)" [class.core-course-storage-section-expanded]="section.expanded"
[attr.aria-label]="(section.expanded ? 'core.collapse' : 'core.expand') | translate"
[attr.aria-expanded]="section.expanded" [attr.aria-controls]="'core-course-storage-section-' + section.id">
<ion-icon name="fas-chevron-right" flip-rtl slot="start" class="expandable-status-icon"
[class.expandable-status-icon-expanded]="section.expanded">
</ion-icon>
<ion-label>
<p class="item-heading ion-text-wrap">
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="section.course"
@ -52,18 +61,22 @@
</core-format-text>
</p>
<ion-badge [color]="section.downloadStatus == statusDownloaded ? 'success' : 'light'"
*ngIf="section.totalSize > 0">
*ngIf="!section.calculatingSize && section.totalSize > 0">
<ion-icon name="fam-cloud-done" *ngIf="section.downloadStatus == statusDownloaded"
[attr.aria-label]="'core.downloaded' | translate">
</ion-icon>{{ section.totalSize | coreBytesToSize }}
</ion-badge>
<ion-badge color="light" *ngIf="section.calculatingSize">
{{ 'core.calculating' | translate }}
</ion-badge>
<!-- Download progress. -->
<p *ngIf="downloadEnabled && section.isDownloading">
<core-progress-bar [progress]="section.total == 0 ? -1 : section.count / section.total">
</core-progress-bar>
</p>
</ion-label>
<div class="storage-buttons" slot="end" *ngIf="section.totalSize > 0 || downloadEnabled">
<div class="storage-buttons" slot="end"
*ngIf="(!section.calculatingSize && section.totalSize > 0) || downloadEnabled">
<div *ngIf="downloadEnabled" slot="end" class="core-button-spinner">
<core-download-refresh *ngIf="!section.isDownloading && section.downloadStatus != statusDownloaded"
[status]="section.downloadStatus" [enabled]="true" (action)="prefecthSection(section)"
@ -77,7 +90,8 @@
{{section.count}} / {{section.total}}
</ion-badge>
</div>
<ion-button (click)="deleteForSection(section)" *ngIf="section.totalSize > 0" color="danger" fill="clear">
<ion-button (click)="deleteForSection(section)" *ngIf="!section.calculatingSize && section.totalSize > 0"
color="danger" fill="clear">
<ion-icon name="fas-trash" slot="icon-only"
[attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: section.name }">
</ion-icon>
@ -85,40 +99,46 @@
</div>
</ion-item>
</ion-card-header>
<ion-card-content>
<ng-container *ngFor="let module of section.modules">
<ion-item class="ion-no-padding core-course-storage-activity" *ngIf="downloadEnabled || module.totalSize > 0">
<core-mod-icon slot="start" *ngIf="module.handlerData.icon" [modicon]="module.handlerData.icon"
[modname]="module.modname" [componentId]="module.instance">
</core-mod-icon>
<ion-label class="ion-text-wrap">
<h3 class="{{module.handlerData!.class}} addon-storagemanager-module-size">
<core-format-text [text]="module.handlerData.title" [courseId]="module.course" contextLevel="module"
[contextInstanceId]="module.id" [adaptImg]="false">
</core-format-text>
</h3>
<ion-badge [color]="module.downloadStatus == statusDownloaded ? 'success' : 'light'"
*ngIf="module.totalSize > 0">
<ion-icon name="fam-cloud-done" *ngIf="module.downloadStatus == statusDownloaded"
[attr.aria-label]="'core.downloaded' | translate">
</ion-icon>{{ module.totalSize | coreBytesToSize }}
</ion-badge>
</ion-label>
<ion-card-content id="core-course-storage-section-{{section.id}}">
<ng-container *ngIf="section.expanded">
<ng-container *ngFor="let module of section.modules">
<ion-item class="ion-no-padding core-course-storage-activity"
*ngIf="downloadEnabled || (!module.calculatingSize && module.totalSize > 0)">
<core-mod-icon slot="start" *ngIf="module.handlerData.icon" [modicon]="module.handlerData.icon"
[modname]="module.modname" [componentId]="module.instance">
</core-mod-icon>
<ion-label class="ion-text-wrap">
<h3 class="{{module.handlerData!.class}} addon-storagemanager-module-size">
<core-format-text [text]="module.handlerData.title" [courseId]="module.course" contextLevel="module"
[contextInstanceId]="module.id" [adaptImg]="false">
</core-format-text>
</h3>
<ion-badge [color]="module.downloadStatus == statusDownloaded ? 'success' : 'light'"
*ngIf="!module.calculatingSize && module.totalSize > 0">
<ion-icon name="fam-cloud-done" *ngIf="module.downloadStatus == statusDownloaded"
[attr.aria-label]="'core.downloaded' | translate">
</ion-icon>{{ module.totalSize | coreBytesToSize }}
</ion-badge>
<ion-badge color="light" *ngIf="module.calculatingSize">
{{ 'core.calculating' | translate }}
</ion-badge>
</ion-label>
<div class="storage-buttons" slot="end">
<core-download-refresh *ngIf="downloadEnabled && module.handlerData?.showDownloadButton &&
module.downloadStatus != statusDownloaded" [status]="module.downloadStatus" [enabled]="true"
[canTrustDownload]="true" [loading]="module.spinner || module.handlerData.spinner"
(action)="prefetchModule(module, section)">
</core-download-refresh>
<ion-button fill="clear" (click)="deleteForModule(module, section)" *ngIf="module.totalSize > 0"
color="danger">
<ion-icon name="fas-trash" slot="icon-only"
[attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: module.name }">
</ion-icon>
</ion-button>
</div>
</ion-item>
<div class="storage-buttons" slot="end">
<core-download-refresh *ngIf="downloadEnabled && module.handlerData?.showDownloadButton &&
module.downloadStatus != statusDownloaded" [status]="module.downloadStatus" [enabled]="true"
[canTrustDownload]="true" [loading]="module.spinner || module.handlerData.spinner"
(action)="prefetchModule(module, section)">
</core-download-refresh>
<ion-button fill="clear" (click)="deleteForModule(module, section)"
*ngIf="!module.calculatingSize && module.totalSize > 0" color="danger">
<ion-icon name="fas-trash" slot="icon-only"
[attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: module.name }">
</ion-icon>
</ion-button>
</div>
</ion-item>
</ng-container>
</ng-container>
</ion-card-content>
</ion-card>

View File

@ -13,7 +13,7 @@
// limitations under the License.
import { CoreConstants } from '@/core/constants';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Component, ElementRef, OnDestroy, OnInit } from '@angular/core';
import { CoreCourse, CoreCourseProvider } from '@features/course/services/course';
import {
CoreCourseHelper,
@ -30,6 +30,7 @@ import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
import { CoreDom } from '@singletons/dom';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
/**
@ -48,6 +49,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
loaded = false;
sections: AddonStorageManagerCourseSection[] = [];
totalSize = 0;
calculatingSize = true;
downloadEnabled = false;
downloadCourseEnabled = false;
@ -61,6 +63,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
statusDownloaded = CoreConstants.DOWNLOADED;
protected initialSectionId?: number;
protected siteUpdatedObserver?: CoreEventObserver;
protected courseStatusObserver?: CoreEventObserver;
protected sectionStatusObserver?: CoreEventObserver;
@ -68,7 +71,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
protected isDestroyed = false;
protected isGuest = false;
constructor() {
constructor(protected elementRef: ElementRef) {
// Refresh the enabled flags if site is updated.
this.siteUpdatedObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
@ -99,21 +102,38 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
}
this.isGuest = !!CoreNavigator.getRouteBooleanParam('isGuest');
this.initialSectionId = CoreNavigator.getRouteNumberParam('sectionId');
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
this.downloadEnabled = !CoreSites.getRequiredCurrentSite().isOfflineDisabled();
const sections = await CoreCourse.getSections(this.courseId, false, true);
const sections = (await CoreCourse.getSections(this.courseId, false, true))
.filter((section) => !CoreCourseHelper.isSectionStealth(section));
this.sections = (await CoreCourseHelper.addHandlerDataForModules(sections, this.courseId)).sections
.map((section) => ({ ...section, totalSize: 0 }));
.map(section => ({
...section,
totalSize: 0,
calculatingSize: true,
expanded: section.id === this.initialSectionId,
modules: section.modules.map(module => ({
...module,
calculatingSize: true,
})),
}));
this.loaded = true;
CoreDom.scrollToElement(
this.elementRef.nativeElement,
'.core-course-storage-section-expanded',
{ addYAxis: -10 },
);
await Promise.all([
this.loadSizes(),
this.initSizes(),
this.initCoursePrefetch(),
this.initModulePrefetch(),
]);
this.loaded = true;
}
/**
@ -239,15 +259,9 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
/**
* Init section, course and modules sizes.
*/
protected async loadSizes(): Promise<void> {
this.totalSize = 0;
const promises: Promise<void>[] = [];
this.sections.forEach((section) => {
section.totalSize = 0;
section.modules.forEach((module) => {
module.totalSize = 0;
protected async initSizes(): Promise<void> {
await Promise.all(this.sections.map(async (section) => {
await Promise.all(section.modules.map(async (module) => {
// 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.
@ -255,21 +269,22 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
// 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.courseId).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;
}
const size = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, this.courseId);
return;
});
promises.push(promise);
});
});
// 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;
}
await Promise.all(promises);
module.calculatingSize = false;
}));
section.calculatingSize = false;
}));
this.calculatingSize = false;
// Mark course as not downloaded if course size is 0.
if (this.totalSize == 0) {
@ -277,6 +292,56 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
}
}
/**
* Update the sizes of some modules.
*
* @param modules Modules.
* @param section Section the modules belong to.
* @return Promise resolved when done.
*/
protected async updateModulesSizes(
modules: AddonStorageManagerModule[],
section?: AddonStorageManagerCourseSection,
): Promise<void> {
this.calculatingSize = true;
await Promise.all(modules.map(async (module) => {
if (module.calculatingSize) {
return;
}
module.calculatingSize = true;
if (!section) {
section = this.sections.find((section) => section.modules.some((mod) => mod.id === module.id));
if (section) {
section.calculatingSize = true;
}
}
try {
const size = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, this.courseId);
const diff = (isNaN(size) ? 0 : size) - (module.totalSize ?? 0);
module.totalSize = Number(size);
this.totalSize += diff;
if (section) {
section.totalSize += diff;
}
} catch {
// Ignore errors, it shouldn't happen.
} finally {
module.calculatingSize = false;
}
}));
this.calculatingSize = false;
if (section) {
section.calculatingSize = false;
}
}
/**
* The user has requested a delete for the whole course data.
*
@ -401,7 +466,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
} finally {
modal.dismiss();
await this.loadSizes();
await this.updateModulesSizes(modules, section);
CoreCourseHelper.calculateSectionsStatus(this.sections, this.courseId, false, false);
}
}
@ -450,7 +515,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingsection', true);
}
} finally {
await this.loadSizes();
await this.updateModulesSizes(section.modules, section);
}
} catch (error) {
// User cancelled or there was an error calculating the size.
@ -496,7 +561,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
} finally {
module.spinner = false;
await this.loadSizes();
await this.updateModulesSizes([module]);
}
}
@ -587,6 +652,18 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
}
}
/**
* Toggle expand status.
*
* @param event Event object.
* @param section Section to expand / collapse.
*/
toggleExpand(event: Event, section: AddonStorageManagerCourseSection): void {
section.expanded = !section.expanded;
event.stopPropagation();
event.preventDefault();
}
/**
* @inheritdoc
*/
@ -608,11 +685,14 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
type AddonStorageManagerCourseSection = Omit<CoreCourseSectionWithStatus, 'modules'> & {
totalSize: number;
calculatingSize: boolean;
expanded: boolean;
modules: AddonStorageManagerModule[];
};
type AddonStorageManagerModule = CoreCourseModuleData & {
totalSize?: number;
calculatingSize: boolean;
prefetchHandler?: CoreCourseModulePrefetchHandler;
spinner?: boolean;
downloadStatus?: string;

View File

@ -21,6 +21,7 @@ import { makeSingleton, Translate } from '@singletons';
import { CoreNavigator } from '@services/navigator';
import { Params } from '@angular/router';
import { CoreContentLinksChooseSiteModalComponent } from '../components/choose-site-modal/choose-site-modal';
import { CoreCustomURLSchemes } from '@services/urlschemes';
/**
* Service that provides some features regarding content links.
@ -138,6 +139,12 @@ export class CoreContentLinksHelperProvider {
openBrowserRoot?: boolean,
): Promise<boolean> {
try {
if (CoreCustomURLSchemes.isCustomURL(url)) {
await CoreCustomURLSchemes.handleCustomURL(url);
return true;
}
if (checkRoot) {
const data = await CoreSites.isStoredRootURL(url, username);

View File

@ -1,3 +1,8 @@
<core-navbar-buttons slot="end" prepend>
<ion-button fill="clear" (click)="gotoCourseDownloads()" [attr.aria-label]="'addon.storagemanager.coursedownloads' | translate">
<ion-icon name="fas-cloud-download-alt" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</core-navbar-buttons>
<core-dynamic-component [component]="courseFormatComponent" [data]="data">
<!-- Default course format. -->
<core-loading [hideUntil]="loaded">

View File

@ -391,29 +391,38 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
);
}
/**
* Get selected section ID. If viewing all sections, use current scrolled section.
*
* @return Section ID, undefined if not found.
*/
protected async getSelectedSectionId(): Promise<number | undefined> {
if (this.selectedSection?.id !== this.allSectionsId) {
return this.selectedSection?.id;
}
// Check current scrolled section.
const allSectionElements: NodeListOf<HTMLElement> =
this.elementRef.nativeElement.querySelectorAll('section.core-course-module-list-wrapper');
const scroll = await this.content.getScrollElement();
const containerTop = scroll.getBoundingClientRect().top;
const element = Array.from(allSectionElements).find((element) => {
const position = element.getBoundingClientRect();
// The bottom is inside the container or lower.
return position.bottom >= containerTop;
});
return Number(element?.getAttribute('id')) || undefined;
}
/**
* Display the course index modal.
*/
async openCourseIndex(): Promise<void> {
let selectedId = this.selectedSection?.id;
if (selectedId == this.allSectionsId) {
// Check current scrolled section.
const allSectionElements: NodeListOf<HTMLElement> =
this.elementRef.nativeElement.querySelectorAll('section.section-wrapper');
const scroll = await this.content.getScrollElement();
const containerTop = scroll.getBoundingClientRect().top;
const element = Array.from(allSectionElements).find((element) => {
const position = element.getBoundingClientRect();
// The bottom is inside the container or lower.
return position.bottom >= containerTop;
});
selectedId = Number(element?.getAttribute('id')) || undefined;
}
const selectedId = await this.getSelectedSectionId();
const data = await CoreDomUtils.openModal<CoreCourseIndexSectionWithModule>({
component: CoreCourseCourseIndexComponent,
@ -453,6 +462,23 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
this.moduleId = data.moduleId;
}
/**
* Open course downloads page.
*/
async gotoCourseDownloads(): Promise<void> {
const selectedId = await this.getSelectedSectionId();
CoreNavigator.navigateToSitePath(
`storage/${this.course.id}`,
{
params: {
title: this.course.fullname,
sectionId: selectedId,
},
},
);
}
/**
* Function called when selected section changes.
*

View File

@ -48,34 +48,36 @@
<ion-icon name="fas-eye-slash" *ngIf="!section.visible && section.uservisible" slot="end" class="restricted"
[attr.aria-label]="'core.course.hiddenfromstudents' | translate"></ion-icon>
</ion-item>
<ng-container *ngIf="section.expanded">
<ng-container *ngFor="let module of section.modules">
<ion-item class="module" [class.item-dimmed]="!module.visible" [class.item-hightlighted]="section.highlighted"
(click)="selectSectionOrModule($event, section.id, module.id)" button>
<ion-icon class="completioninfo completion_none" name="" *ngIf="module.completionStatus === undefined"
slot="start" aria-hidden="true"></ion-icon>
<ion-icon class="completioninfo completion_incomplete" name="far-circle" *ngIf="module.completionStatus === 0"
slot="start" [attr.aria-label]="'core.course.todo' | translate">
</ion-icon>
<ion-icon class="completioninfo completion_complete" name="fas-circle"
*ngIf="module.completionStatus === 1 || module.completionStatus === 2" color="success" slot="start"
[attr.aria-label]="'core.course.done' | translate">
</ion-icon>
<ion-icon class="completioninfo completion_fail" name="fas-circle" *ngIf="module.completionStatus === 3"
color="danger" slot="start" [attr.aria-label]="'core.course.failed' | translate">
</ion-icon>
<ion-label>
<p class="item-heading">
<core-format-text [text]="module.name" contextLevel="module" [contextInstanceId]="module.id"
[courseId]="module.course">
</core-format-text>
</p>
</ion-label>
<ion-icon name="fas-lock" *ngIf="!module.uservisible" slot="end" class="restricted"
[attr.aria-label]="'core.restricted' | translate"></ion-icon>
</ion-item>
<div id="core-course-index-section-{{section.id}}">
<ng-container *ngIf="section.expanded">
<ng-container *ngFor="let module of section.modules">
<ion-item class="module" [class.item-dimmed]="!module.visible" [class.item-hightlighted]="section.highlighted"
(click)="selectSectionOrModule($event, section.id, module.id)" button>
<ion-icon class="completioninfo completion_none" name="" *ngIf="module.completionStatus === undefined"
slot="start" aria-hidden="true"></ion-icon>
<ion-icon class="completioninfo completion_incomplete" name="far-circle"
*ngIf="module.completionStatus === 0" slot="start" [attr.aria-label]="'core.course.todo' | translate">
</ion-icon>
<ion-icon class="completioninfo completion_complete" name="fas-circle"
*ngIf="module.completionStatus === 1 || module.completionStatus === 2" color="success" slot="start"
[attr.aria-label]="'core.course.done' | translate">
</ion-icon>
<ion-icon class="completioninfo completion_fail" name="fas-circle" *ngIf="module.completionStatus === 3"
color="danger" slot="start" [attr.aria-label]="'core.course.failed' | translate">
</ion-icon>
<ion-label>
<p class="item-heading">
<core-format-text [text]="module.name" contextLevel="module" [contextInstanceId]="module.id"
[courseId]="module.course">
</core-format-text>
</p>
</ion-label>
<ion-icon name="fas-lock" *ngIf="!module.uservisible" slot="end" class="restricted"
[attr.aria-label]="'core.restricted' | translate"></ion-icon>
</ion-item>
</ng-container>
</ng-container>
</ng-container>
</div>
</ng-container>
</ng-container>
</ion-list>

View File

@ -1,8 +1,3 @@
<core-navbar-buttons slot="end" prepend>
<ion-button fill="clear" (click)="gotoCourseDownloads()" [attr.aria-label]="'addon.storagemanager.coursedownloads' | translate">
<ion-icon name="fas-cloud-download-alt" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</core-navbar-buttons>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!dataLoaded || !displayRefresher" (ionRefresh)="doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>

View File

@ -366,14 +366,6 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
}
}
gotoCourseDownloads(): void {
CoreNavigator.navigateToSitePath(
`storage/${this.course.id}`,
{ params: { title: this.course.fullname } },
);
}
/**
* @inheritdoc
*/

View File

@ -17,10 +17,11 @@ import { CoreIframeUtils } from '@services/utils/iframe';
import { Platform } from '@singletons';
export default async function(): Promise<void> {
await Platform.ready();
if (!CoreApp.isIOS() || !('WKUserScript' in window)) {
return;
}
await Platform.ready();
CoreIframeUtils.injectiOSScripts(window);
}

View File

@ -13,6 +13,7 @@
"areyousure": "Are you sure?",
"back": "Back",
"browser": "Browser",
"calculating": "Calculating",
"cancel": "Cancel",
"cannotconnect": "Cannot connect",
"cannotconnecttrouble": "We're having trouble connecting to your site.",

View File

@ -446,26 +446,13 @@ export class CoreIframeUtilsProvider {
} else {
element.setAttribute('src', url);
}
} else if (CoreUrlUtils.isLocalFileUrl(url)) {
// It's a local file.
const filename = url.substring(url.lastIndexOf('/') + 1);
if (!CoreFileHelper.isOpenableInApp({ filename })) {
try {
await CoreFileHelper.showConfirmOpenUnsupportedFile();
} catch (error) {
return; // Cancelled, stop.
}
}
} else {
try {
await CoreUtils.openFile(url);
// It's an external link or a local file, check if it can be opened in the app.
await CoreWindow.open(url, name);
} catch (error) {
CoreDomUtils.showErrorModal(error);
}
} else {
// It's an external link, check if it can be opened in the app.
await CoreWindow.open(url, name);
}
}