MOBILE-3833 course: Collapse sections in downloads page

main
Dani Palou 2022-03-29 12:39:18 +02:00
parent 37af8e3c69
commit 0298273fc4
7 changed files with 152 additions and 100 deletions

View File

@ -47,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"
@ -93,44 +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.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>
<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.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>
<div class="storage-buttons" slot="end">
<core-download-refresh *ngIf="downloadEnabled && module.handlerData?.showDownloadButton &&
module.downloadStatus != statusDownloaded" [status]="module.downloadStatus" [enabled]="true"
[canTrustDownload]="true" [loading]="module.spinner || module.handlerData.spinner"
(action)="prefetchModule(module, section)">
</core-download-refresh>
<ion-button fill="clear" (click)="deleteForModule(module, section)"
*ngIf="!module.calculatingSize && module.totalSize > 0" color="danger">
<ion-icon name="fas-trash" slot="icon-only"
[attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: module.name }">
</ion-icon>
</ion-button>
</div>
</ion-item>
</ng-container>
</ng-container>
</ion-card-content>
</ion-card>

View File

@ -13,7 +13,7 @@
// limitations under the License.
import { CoreConstants } from '@/core/constants';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Component, ElementRef, OnDestroy, OnInit } from '@angular/core';
import { CoreCourse, CoreCourseProvider } from '@features/course/services/course';
import {
CoreCourseHelper,
@ -30,6 +30,7 @@ import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
import { CoreDom } from '@singletons/dom';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
/**
@ -62,6 +63,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
statusDownloaded = CoreConstants.DOWNLOADED;
protected initialSectionId?: number;
protected siteUpdatedObserver?: CoreEventObserver;
protected courseStatusObserver?: CoreEventObserver;
protected sectionStatusObserver?: CoreEventObserver;
@ -69,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();
@ -100,16 +102,19 @@ 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,
calculatingSize: true,
expanded: section.id === this.initialSectionId,
modules: section.modules.map(module => ({
...module,
calculatingSize: true,
@ -118,6 +123,12 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
this.loaded = true;
CoreDom.scrollToElement(
this.elementRef.nativeElement,
'.core-course-storage-section-expanded',
{ addYAxis: -10 },
);
await Promise.all([
this.initSizes(),
this.initCoursePrefetch(),
@ -641,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
*/
@ -663,6 +686,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
type AddonStorageManagerCourseSection = Omit<CoreCourseSectionWithStatus, 'modules'> & {
totalSize: number;
calculatingSize: boolean;
expanded: boolean;
modules: AddonStorageManagerModule[];
};

View File

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

View File

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

View File

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

View File

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

View File

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