commit
771ee90d6a
|
@ -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" />
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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}">
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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}">
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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!);
|
||||
|
|
|
@ -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}">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -366,14 +366,6 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
gotoCourseDownloads(): void {
|
||||
CoreNavigator.navigateToSitePath(
|
||||
`storage/${this.course.id}`,
|
||||
{ params: { title: this.course.fullname } },
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue