Merge pull request #4125 from crazyserver/MOBILE-4628

Mobile 4628
main
Dani Palou 2024-07-29 17:08:15 +02:00 committed by GitHub
commit 9e75f4c01f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 175 additions and 114 deletions

View File

@ -1168,6 +1168,7 @@
"addon.storagemanager.deletecourses": "local_moodlemobileapp",
"addon.storagemanager.deletedata": "local_moodlemobileapp",
"addon.storagemanager.deletedatafrom": "local_moodlemobileapp",
"addon.storagemanager.downloaddatafrom": "local_moodlemobileapp",
"addon.storagemanager.downloadedcourses": "local_moodlemobileapp",
"addon.storagemanager.downloads": "local_moodlemobileapp",
"addon.storagemanager.errordeletedownloadeddata": "local_moodlemobileapp",

View File

@ -11,6 +11,7 @@
"deletecourses": "Delete downloaded data from all courses",
"deletedata": "Delete downloaded data",
"deletedatafrom": "Delete all downloaded data from '{{name}}'",
"downloaddatafrom": "Download {{name}}",
"downloadedcourses": "Downloaded courses",
"downloads": "Downloads",
"errordeletedownloadeddata": "Error deleting downloaded data.",

View File

@ -45,98 +45,102 @@
</ion-button>
</ion-card-header>
</ion-card>
<ng-container *ngFor="let section of sections">
<ion-card class="section" *ngIf="section.modules.length > 0">
<ion-card-header>
<ion-item [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" aria-hidden="true" />
<ion-label>
<p class="item-heading ion-text-wrap">
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="section.course"
[adaptImg]="false" />
</p>
<ion-badge [color]="section.downloadStatus === statusDownloaded ? 'success' : 'light'"
*ngIf="!section.calculatingSize && section.totalSize > 0">
<ion-icon name="fam-cloud-done" *ngIf="section.downloadStatus === statusDownloaded"
[attr.aria-label]="'core.downloaded' | translate" />{{ 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) * 100" />
</p>
</ion-label>
<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)"
[loading]="section.isDownloading || section.isCalculating" [canTrustDownload]="true" />
<ion-badge class="core-course-download-section-progress"
*ngIf="section.isDownloading && section.count < section.total" role="progressbar"
[attr.aria-valuemax]="section.total" [attr.aria-valuenow]="section.count"
[attr.aria-valuetext]="'core.course.downloadsectionprogressdescription' | translate:section">
{{section.count}} / {{section.total}}
<ion-accordion-group [multiple]="true" (ionChange)="accordionGroupChange($event.detail)" #accordionGroup>
<ng-container *ngFor="let section of sections">
<ion-card class="section" *ngIf="section.modules.length > 0">
<ion-accordion [value]="section.id" toggleIconSlot="start">
<ion-item [detail]="false" slot="header" class="card-header">
<ion-label>
<p class="item-heading ion-text-wrap">
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="section.course"
[adaptImg]="false" />
</p>
<ion-badge [color]="section.downloadStatus === statusDownloaded ? 'success' : 'light'"
*ngIf="!section.calculatingSize && section.totalSize > 0">
<ion-icon name="fam-cloud-done" *ngIf="section.downloadStatus === statusDownloaded"
[attr.aria-label]="'core.downloaded' | translate" />{{ section.totalSize | coreBytesToSize }}
</ion-badge>
</div>
<ion-button (click)="deleteForSection($event, 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-button>
</div>
</ion-item>
</ion-card-header>
<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="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" [fallbackTranslation]="module.modplural"
[isBranded]="module.branded" />
<ion-label class="ion-text-wrap">
<p class="item-heading {{module.handlerData!.class}} addon-storagemanager-module-size">
<core-format-text [text]="module.handlerData.title" [courseId]="module.course" contextLevel="module"
[contextInstanceId]="module.id" [adaptImg]="false" />
</p>
<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" />{{ module.totalSize | coreBytesToSize }}
</ion-badge>
<ion-badge color="light" *ngIf="module.calculatingSize ||
(section.isDownloading && module.downloadStatus === statusDownloaded)">
{{ 'core.calculating' | translate }}
</ion-badge>
</ion-label>
<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) * 100" />
</p>
</ion-label>
<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)"
[loading]="section.isDownloading || section.isCalculating" [canTrustDownload]="true"
[statusesTranslatable]="{notdownloaded: 'addon.storagemanager.downloaddatafrom' }"
[statusSubject]="section.name" />
<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)" />
<ion-button fill="clear" (click)="deleteForModule($event, 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-button>
<p *ngIf="!downloadEnabled || !module.handlerData?.showDownloadButton" class="sr-only">
{{ 'core.notdownloadable' | translate }}
</p>
<ion-badge class="core-course-download-section-progress"
*ngIf="section.isDownloading && section.count < section.total" role="progressbar"
[attr.aria-valuemax]="section.total" [attr.aria-valuenow]="section.count"
[attr.aria-valuetext]="'core.course.downloadsectionprogressdescription' | translate:section">
{{section.count}} / {{section.total}}
</ion-badge>
</div>
</ion-item>
</ng-container>
</ng-container>
</ion-card-content>
</ion-card>
</ng-container>
<ion-button (click)="deleteForSection($event, 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-button>
</div>
</ion-item>
<ion-card-content slot="content">
<ng-container *ngIf="section.expanded">
<ng-container *ngFor="let module of section.modules">
<ion-item class="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"
[fallbackTranslation]="module.modplural" [isBranded]="module.branded" />
<ion-label class="ion-text-wrap">
<p class="item-heading {{module.handlerData!.class}} addon-storagemanager-module-size">
<core-format-text [text]="module.handlerData.title" [courseId]="module.course"
contextLevel="module" [contextInstanceId]="module.id" [adaptImg]="false" />
</p>
<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" />{{ module.totalSize |
coreBytesToSize }}
</ion-badge>
<ion-badge color="light" *ngIf="module.calculatingSize ||
(section.isDownloading && module.downloadStatus === statusDownloaded)">
{{ '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)"
[statusesTranslatable]="{notdownloaded: 'addon.storagemanager.downloaddatafrom' }"
[statusSubject]="module.name" />
<ion-button fill="clear" (click)="deleteForModule($event, 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-button>
<p *ngIf="!downloadEnabled || !module.handlerData?.showDownloadButton" class="sr-only">
{{ 'core.notdownloadable' | translate }}
</p>
</div>
</ion-item>
</ng-container>
</ng-container>
</ion-card-content>
</ion-accordion>
</ion-card>
</ng-container>
</ion-accordion-group>
</core-loading>
</ion-content>

View File

@ -8,13 +8,20 @@
}
ion-card.section {
ion-card-header {
ion-item.card-header {
padding: 0;
--border-width: 0px;
.item-heading {
font: var(--mdl-typography-heading4-font);
}
}
.accordion-expanded {
ion-item.card-header {
--border-width: 0 0 1px 0;
}
}
ion-card-content {
padding: 0;

View File

@ -13,7 +13,7 @@
// limitations under the License.
import { CoreConstants, DownloadStatus } from '@/core/constants';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { CoreCourse, CoreCourseProvider } from '@features/course/services/course';
import {
CoreCourseHelper,
@ -25,6 +25,7 @@ import {
CoreCourseModulePrefetchDelegate,
CoreCourseModulePrefetchHandler } from '@features/course/services/module-prefetch-delegate';
import { CoreCourseAnyCourseData, CoreCourses } from '@features/courses/services/courses';
import { AccordionGroupChangeEventDetail, IonAccordionGroup } from '@ionic/angular';
import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
@ -40,11 +41,13 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events';
@Component({
selector: 'page-addon-storagemanager-course-storage',
templateUrl: 'course-storage.html',
styleUrls: ['course-storage.scss'],
styleUrl: 'course-storage.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
@ViewChild('accordionGroup', { static: true }) accordionGroup!: IonAccordionGroup;
courseId!: number;
title = '';
loaded = false;
@ -64,7 +67,6 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
statusDownloaded = DownloadStatus.DOWNLOADED;
protected initialSectionId?: number;
protected siteUpdatedObserver?: CoreEventObserver;
protected courseStatusObserver?: CoreEventObserver;
protected sectionStatusObserver?: CoreEventObserver;
@ -106,7 +108,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
this.isGuest = CoreNavigator.getRouteBooleanParam('isGuest') ??
(await CoreCourseHelper.courseUsesGuestAccessInfo(this.courseId)).guestAccess;
this.initialSectionId = CoreNavigator.getRouteNumberParam('sectionId');
const initialSectionId = CoreNavigator.getRouteNumberParam('sectionId');
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
this.downloadEnabled = !CoreSites.getRequiredCurrentSite().isOfflineDisabled();
@ -118,7 +120,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
...section,
totalSize: 0,
calculatingSize: true,
expanded: section.id === this.initialSectionId,
expanded: section.id === initialSectionId,
modules: section.modules.map(module => ({
...module,
calculatingSize: true,
@ -127,9 +129,11 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
this.loaded = true;
this.accordionGroup.value = String(initialSectionId);
CoreDom.scrollToElement(
this.elementRef.nativeElement,
'.core-course-storage-section-expanded',
'.accordion-expanded',
{ addYAxis: -10 },
);
@ -719,12 +723,17 @@ 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();
accordionGroupChange(event: AccordionGroupChangeEventDetail): void {
this.sections.forEach((section) => {
section.expanded = false;
});
event.value.forEach((sectionId) => {
const section = this.sections.find((section) => section.id === Number(sectionId));
if (section) {
section.expanded = true;
}
});
}
/**

View File

@ -40,4 +40,4 @@ const routes: Routes = [
AddonStorageManagerCourseStoragePage,
],
})
export class AddonStorageManagerLazyModule {}
export default class AddonStorageManagerLazyModule {}

View File

@ -23,7 +23,7 @@ import { AddonStorageManagerSettingsHandler } from './services/handlers/settings
const routes: Routes = [
{
path: '',
loadChildren: () => import('@addons/storagemanager/storagemanager-lazy.module').then(m => m.AddonStorageManagerLazyModule),
loadChildren: () => import('@addons/storagemanager/storagemanager-lazy.module'),
},
];

View File

@ -1,23 +1,25 @@
<ng-container *ngIf="enabled && !loading">
<!-- Download button. -->
<ion-button *ngIf="status === statusNotDownloaded" fill="clear" (click)="download($event, false)" @coreShowHideAnimation
[ariaLabel]="(statusTranslatable || 'core.download') | translate">
[ariaLabel]="(statusTranslatable || translates.notdownloaded) | translate: { name : statusSubject }">
<ion-icon slot="icon-only" name="fas-cloud-arrow-down" aria-hidden="true" />
</ion-button>
<!-- Refresh button. -->
<ion-button *ngIf="status === statusOutdated || (status === statusDownloaded && !canTrustDownload)" fill="clear"
(click)="download($event, true)" @coreShowHideAnimation [ariaLabel]="(statusTranslatable || 'core.refresh') | translate">
(click)="download($event, true)" @coreShowHideAnimation
[ariaLabel]="(statusTranslatable || translates.outdated) | translate: { name : statusSubject }">
<ion-icon slot="icon-only" name="fam-cloud-refresh" aria-hidden="true" />
</ion-button>
<!-- Downloaded status icon. -->
<ion-icon *ngIf="status === statusDownloaded && canTrustDownload" class="core-icon-downloaded ion-padding-horizontal" color="success"
name="fam-cloud-done" [attr.aria-label]="(statusTranslatable || 'core.downloaded') | translate" role="status" />
name="fam-cloud-done" [attr.aria-label]="(statusTranslatable || translates.downloaded) | translate: { name : statusSubject }"
role="status" />
<ion-spinner *ngIf="status === statusDownloading" @coreShowHideAnimation
[attr.aria-label]="(statusTranslatable || 'core.downloading') | translate" />
[attr.aria-label]="(statusTranslatable || translates.downloading) | translate: { name : statusSubject }" />
</ng-container>
<!-- Spinner. -->
<ion-spinner *ngIf="loading" @coreShowHideAnimation [attr.aria-label]="'core.loading' | translate" />
<ion-spinner *ngIf="loading" @coreShowHideAnimation [attr.aria-label]="translates.loading | translate: { name : statusSubject }" />

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { DownloadStatus } from '@/core/constants';
import { CoreAnimations } from '@components/animations';
@ -29,24 +29,45 @@ import { CoreAnimations } from '@components/animations';
styleUrls: ['download-refresh.scss'],
animations: [CoreAnimations.SHOW_HIDE],
})
export class CoreDownloadRefreshComponent {
export class CoreDownloadRefreshComponent implements OnInit {
@Input() status?: DownloadStatus; // Download status.
@Input() statusTranslatable?: string; // Download status translatable string.
@Input() statusesTranslatable?: Partial<CoreDownloadStatusTranslatable>; // Download statuses translatable strings.
@Input() statusSubject = ''; // Status subject to use on name filed in the translatable string.
@Input() enabled = false; // Whether the download is enabled.
@Input() loading = true; // Force loading status when is not downloading.
@Input() canTrustDownload = false; // If false, refresh will be shown if downloaded.
@Output() action: EventEmitter<boolean>; // Will emit an event when the item clicked.
/**
* @deprecated since 4.5. Use statusesTranslatable instead.
*/
@Input() statusTranslatable?: string; // Download status translatable string.
statusDownloaded = DownloadStatus.DOWNLOADED;
statusNotDownloaded = DownloadStatus.DOWNLOADABLE_NOT_DOWNLOADED;
statusOutdated = DownloadStatus.OUTDATED;
statusDownloading = DownloadStatus.DOWNLOADING;
translates: CoreDownloadStatusTranslatable = {
downloaded: 'core.downloaded',
notdownloaded: 'core.download',
outdated: 'core.refresh',
downloading: 'core.downloading',
loading: 'core.loading',
};
constructor() {
this.action = new EventEmitter();
}
/**
* @inheritdoc
*/
ngOnInit(): void {
this.translates = Object.assign(this.translates, this.statusesTranslatable || {});
}
/**
* Download clicked.
*
@ -61,3 +82,11 @@ export class CoreDownloadRefreshComponent {
}
}
export type CoreDownloadStatusTranslatable = {
downloaded: string;
notdownloaded: string;
outdated: string;
downloading: string;
loading: string;
};

View File

@ -176,7 +176,8 @@
</ion-label>
</ion-item>
<ion-button fill="outline" expand="block" *ngIf="canPrefetch && displayOptions.displayPrefetch" class="ion-text-wrap"
(click)="prefetch()" [disabled]="prefetchDisabled">
(click)="prefetch()" [disabled]="prefetchDisabled"
[attr.aria-label]="'addon.storagemanager.downloaddatafrom' | translate:{name: module?.name}">
<ion-icon *ngIf="!prefetchLoading" name="fas-cloud-arrow-down" slot="start" aria-hidden="true" />
<ion-spinner *ngIf="prefetchLoading" slot="start" aria-hidden="true" />
<ion-label>

View File

@ -10,8 +10,8 @@
<ng-container *ngIf="isEnrolled && layout !== 'summarycard'">
<div class="core-button-spinner" *ngIf="!courseOptionMenuEnabled && showDownload">
<core-download-refresh [status]="prefetchCourseData.status" [enabled]="showDownload"
[statusTranslatable]="prefetchCourseData.statusTranslatable" [canTrustDownload]="false"
[loading]="prefetchCourseData.loading" (action)="prefetchCourse()" />
[statusesTranslatable]="statusesTranslatable" [canTrustDownload]="false" [loading]="prefetchCourseData.loading"
(action)="prefetchCourse()" />
</div>
<div class="core-button-spinner" *ngIf="courseOptionMenuEnabled">

View File

@ -27,6 +27,7 @@ import { CoreCourseListItem, CoreCourses, CoreCoursesProvider } from '../../serv
import { CoreCoursesHelper, CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses-helper';
import { CoreCoursesCourseOptionsMenuComponent } from '../course-options-menu/course-options-menu';
import { CoreEnrolHelper } from '@features/enrol/services/enrol-helper';
import { CoreDownloadStatusTranslatable } from '@components/download-refresh/download-refresh';
/**
* This directive is meant to display an item for a list of courses.
@ -55,6 +56,12 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On
loading: true,
};
statusesTranslatable: Partial<CoreDownloadStatusTranslatable> = {
downloaded: 'core.course.refreshcourse',
notdownloaded: 'core.course.downloadcourse',
outdated: 'core.course.downloadcourse',
};
showSpinner = false;
courseOptionMenuEnabled = false;
progress = -1;