forked from CIT/Vmeda.Online
		
	
						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" />
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										18
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										18
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user