forked from EVOgeek/Vmeda.Online
		
	
						commit
						8736014b35
					
				| @ -649,7 +649,6 @@ | ||||
|   "addon.mod_forum.posttomygroups": "forum", | ||||
|   "addon.mod_forum.privatereply": "forum", | ||||
|   "addon.mod_forum.re": "forum", | ||||
|   "addon.mod_forum.refreshdiscussions": "local_moodlemobileapp", | ||||
|   "addon.mod_forum.refreshposts": "local_moodlemobileapp", | ||||
|   "addon.mod_forum.removefromfavourites": "forum", | ||||
|   "addon.mod_forum.reply": "forum", | ||||
| @ -1115,6 +1114,7 @@ | ||||
|   "addon.storagemanager.deletedata": "local_moodlemobileapp", | ||||
|   "addon.storagemanager.deletedatafrom": "local_moodlemobileapp", | ||||
|   "addon.storagemanager.downloadedcourses": "local_moodlemobileapp", | ||||
|   "addon.storagemanager.downloads": "local_moodlemobileapp", | ||||
|   "addon.storagemanager.errordeletedownloadeddata": "local_moodlemobileapp", | ||||
|   "addon.storagemanager.managedownloads": "local_moodlemobileapp", | ||||
|   "addon.storagemanager.totaldownloads": "local_moodlemobileapp", | ||||
| @ -1742,6 +1742,7 @@ | ||||
|   "core.grades.fail": "grades", | ||||
|   "core.grades.feedback": "grades", | ||||
|   "core.grades.grade": "grades", | ||||
|   "core.grades.gradebook": "grades", | ||||
|   "core.grades.gradeitem": "grades", | ||||
|   "core.grades.gradepass": "grades", | ||||
|   "core.grades.grades": "grades", | ||||
|  | ||||
| @ -1,29 +1,8 @@ | ||||
| <!-- Buttons to add to the header. --> | ||||
| <core-navbar-buttons slot="end"> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" | ||||
|             iconAction="fas-external-link-alt" [showBrowserWarning]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="assign && (description || (assign.introattachments && assign.introattachments.length))" | ||||
|             [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" iconAction="fas-arrow-right"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper" | ||||
|             (action)="gotoBlog()"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" | ||||
|             (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600" | ||||
|             [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" | ||||
|             [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" | ||||
|             [iconAction]="prefetchStatusIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}" | ||||
|             iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|     </core-context-menu> | ||||
|     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> | ||||
|         <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
|  | ||||
| @ -23,7 +23,6 @@ import { CoreGroupInfo, CoreGroups } from '@services/groups'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreTimeUtils } from '@services/utils/time'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { Translate } from '@singletons'; | ||||
| @ -161,104 +160,75 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Expand the description. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     expandDescription(ev?: Event): void { | ||||
|         ev?.preventDefault(); | ||||
|         ev?.stopPropagation(); | ||||
| 
 | ||||
|         if (this.assign && (this.description || this.assign.introattachments)) { | ||||
|             CoreTextUtils.viewText(Translate.instant('core.description'), this.description || '', { | ||||
|                 component: this.component, | ||||
|                 componentId: this.module.id, | ||||
|                 files: this.assign.introattachments, | ||||
|                 filter: true, | ||||
|                 contextLevel: 'module', | ||||
|                 instanceId: this.module.id, | ||||
|                 courseId: this.courseId, | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get assignment data. | ||||
|      * | ||||
|      * @param refresh If it's refreshing content. | ||||
|      * @param sync If it should try to sync. | ||||
|      * @param showErrors If show errors to the user of hide them. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async fetchContent(refresh = false, sync = false, showErrors = false): Promise<void> { | ||||
|     protected async fetchContent(refresh?: boolean, sync = false, showErrors = false): Promise<void> { | ||||
| 
 | ||||
|         // Get assignment data.
 | ||||
|         try { | ||||
|             this.assign = await AddonModAssign.getAssignment(this.courseId, this.module.id); | ||||
|         this.assign = await AddonModAssign.getAssignment(this.courseId, this.module.id); | ||||
| 
 | ||||
|             this.dataRetrieved.emit(this.assign); | ||||
|             this.description = this.assign.intro; | ||||
|         this.dataRetrieved.emit(this.assign); | ||||
|         this.description = this.assign.intro; | ||||
| 
 | ||||
|             if (sync) { | ||||
|                 // Try to synchronize the assign.
 | ||||
|                 await CoreUtils.ignoreErrors(this.syncActivity(showErrors)); | ||||
|             } | ||||
|         if (sync) { | ||||
|             // Try to synchronize the assign.
 | ||||
|             await CoreUtils.ignoreErrors(this.syncActivity(showErrors)); | ||||
|         } | ||||
| 
 | ||||
|             // Check if there's any offline data for this assign.
 | ||||
|             this.hasOffline = await AddonModAssignOffline.hasAssignOfflineData(this.assign.id); | ||||
|         // Check if there's any offline data for this assign.
 | ||||
|         this.hasOffline = await AddonModAssignOffline.hasAssignOfflineData(this.assign.id); | ||||
| 
 | ||||
|             // Get assignment submissions.
 | ||||
|             const submissions = await AddonModAssign.getSubmissions(this.assign.id, { cmId: this.module.id }); | ||||
|             const time = CoreTimeUtils.timestamp(); | ||||
|         // Get assignment submissions.
 | ||||
|         const submissions = await AddonModAssign.getSubmissions(this.assign.id, { cmId: this.module.id }); | ||||
|         const time = CoreTimeUtils.timestamp(); | ||||
| 
 | ||||
|             this.canViewAllSubmissions = submissions.canviewsubmissions; | ||||
|         this.canViewAllSubmissions = submissions.canviewsubmissions; | ||||
| 
 | ||||
|             if (submissions.canviewsubmissions) { | ||||
|         if (submissions.canviewsubmissions) { | ||||
| 
 | ||||
|                 // Calculate the messages to display about time remaining and late submissions.
 | ||||
|                 if (this.assign.duedate > 0) { | ||||
|                     if (this.assign.duedate - time <= 0) { | ||||
|                         this.timeRemaining = Translate.instant('addon.mod_assign.assignmentisdue'); | ||||
|                     } else { | ||||
|                         this.timeRemaining = CoreTimeUtils.formatDuration(this.assign.duedate - time, 3); | ||||
| 
 | ||||
|                         if (this.assign.cutoffdate) { | ||||
|                             if (this.assign.cutoffdate > time) { | ||||
|                                 this.lateSubmissions = Translate.instant( | ||||
|                                     'addon.mod_assign.latesubmissionsaccepted', | ||||
|                                     { $a: CoreTimeUtils.userDate(this.assign.cutoffdate * 1000) }, | ||||
|                                 ); | ||||
|                             } else { | ||||
|                                 this.lateSubmissions = Translate.instant('addon.mod_assign.nomoresubmissionsaccepted'); | ||||
|                             } | ||||
|                         } else { | ||||
|                             this.lateSubmissions = ''; | ||||
|                         } | ||||
|                     } | ||||
|             // Calculate the messages to display about time remaining and late submissions.
 | ||||
|             if (this.assign.duedate > 0) { | ||||
|                 if (this.assign.duedate - time <= 0) { | ||||
|                     this.timeRemaining = Translate.instant('addon.mod_assign.assignmentisdue'); | ||||
|                 } else { | ||||
|                     this.timeRemaining = ''; | ||||
|                     this.lateSubmissions = ''; | ||||
|                     this.timeRemaining = CoreTimeUtils.formatDuration(this.assign.duedate - time, 3); | ||||
| 
 | ||||
|                     if (this.assign.cutoffdate) { | ||||
|                         if (this.assign.cutoffdate > time) { | ||||
|                             this.lateSubmissions = Translate.instant( | ||||
|                                 'addon.mod_assign.latesubmissionsaccepted', | ||||
|                                 { $a: CoreTimeUtils.userDate(this.assign.cutoffdate * 1000) }, | ||||
|                             ); | ||||
|                         } else { | ||||
|                             this.lateSubmissions = Translate.instant('addon.mod_assign.nomoresubmissionsaccepted'); | ||||
|                         } | ||||
|                     } else { | ||||
|                         this.lateSubmissions = ''; | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 // Check if groupmode is enabled to avoid showing wrong numbers.
 | ||||
|                 this.groupInfo = await CoreGroups.getActivityGroupInfo(this.assign.cmid, false); | ||||
| 
 | ||||
|                 await this.setGroup(CoreGroups.validateGroupId(this.group, this.groupInfo)); | ||||
| 
 | ||||
|                 return; | ||||
|             } else { | ||||
|                 this.timeRemaining = ''; | ||||
|                 this.lateSubmissions = ''; | ||||
|             } | ||||
| 
 | ||||
|             try { | ||||
|                 // Check if the user can view their own submission.
 | ||||
|                 await AddonModAssign.getSubmissionStatus(this.assign.id, { cmId: this.module.id }); | ||||
|                 this.canViewOwnSubmission = true; | ||||
|             } catch (error) { | ||||
|                 this.canViewOwnSubmission = false; | ||||
|             // Check if groupmode is enabled to avoid showing wrong numbers.
 | ||||
|             this.groupInfo = await CoreGroups.getActivityGroupInfo(this.assign.cmid, false); | ||||
| 
 | ||||
|                 if (error.errorcode !== 'nopermission') { | ||||
|                     throw error; | ||||
|                 } | ||||
|             await this.setGroup(CoreGroups.validateGroupId(this.group, this.groupInfo)); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             // Check if the user can view their own submission.
 | ||||
|             await AddonModAssign.getSubmissionStatus(this.assign.id, { cmId: this.module.id }); | ||||
|             this.canViewOwnSubmission = true; | ||||
|         } catch (error) { | ||||
|             this.canViewOwnSubmission = false; | ||||
| 
 | ||||
|             if (error.errorcode !== 'nopermission') { | ||||
|                 throw error; | ||||
|             } | ||||
|         } finally { | ||||
|             this.fillContextMenu(refresh); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -1,22 +1,8 @@ | ||||
| <!-- Buttons to add to the header. --> | ||||
| <core-navbar-buttons slot="end"> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" | ||||
|             iconAction="fas-external-link-alt"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" | ||||
|             (action)="expandDescription()" iconAction="fas-arrow-right"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper" | ||||
|             (action)="gotoBlog()"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && isOnline" [priority]="700" [content]="'core.refresh' | translate" | ||||
|             (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" | ||||
|             [iconAction]="prefetchStatusIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|     </core-context-menu> | ||||
|     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> | ||||
|         <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
|  | ||||
| @ -71,21 +71,18 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async fetchContent(refresh: boolean = false): Promise<void> { | ||||
|         try { | ||||
|             this.bbb = await AddonModBBB.getBBB(this.courseId, this.module.id); | ||||
|     protected async fetchContent(): Promise<void> { | ||||
|         this.bbb = await AddonModBBB.getBBB(this.courseId, this.module.id); | ||||
| 
 | ||||
|             this.description = this.bbb.intro; | ||||
|             this.dataRetrieved.emit(this.bbb); | ||||
|         this.description = this.bbb.intro; | ||||
|         this.dataRetrieved.emit(this.bbb); | ||||
| 
 | ||||
|             this.groupInfo = await CoreGroups.getActivityGroupInfo(this.module.id, false); | ||||
|         this.groupInfo = await CoreGroups.getActivityGroupInfo(this.module.id, false); | ||||
| 
 | ||||
|             this.groupId = CoreGroups.validateGroupId(this.groupId, this.groupInfo); | ||||
|         this.groupId = CoreGroups.validateGroupId(this.groupId, this.groupInfo); | ||||
| 
 | ||||
|         await this.fetchMeetingInfo(); | ||||
| 
 | ||||
|             await this.fetchMeetingInfo(); | ||||
|         } finally { | ||||
|             this.fillContextMenu(refresh); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -1,20 +1,8 @@ | ||||
| <!-- Buttons to add to the header. --> | ||||
| <core-navbar-buttons slot="end"> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" | ||||
|             iconAction="fas-external-link-alt" [showBrowserWarning]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" | ||||
|             (action)="expandDescription()" iconAction="fas-arrow-right"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper" | ||||
|             (action)="gotoBlog()"></core-context-menu-item> | ||||
|         <core-context-menu-item [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" | ||||
|             [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="600" [content]="prefetchText" (action)="prefetch($event)" | ||||
|             [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="500" [content]="'core.clearstoreddata' | translate:{$a: size}" | ||||
|             iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|     </core-context-menu> | ||||
|     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> | ||||
|         <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
|  | ||||
| @ -52,15 +52,11 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async fetchContent(refresh?: boolean): Promise<void> { | ||||
|         try { | ||||
|             await Promise.all([ | ||||
|                 this.loadBook(), | ||||
|                 this.loadTOC(), | ||||
|             ]); | ||||
|         } finally { | ||||
|             this.fillContextMenu(refresh); | ||||
|         } | ||||
|     protected async fetchContent(): Promise<void> { | ||||
|         await Promise.all([ | ||||
|             this.loadBook(), | ||||
|             this.loadTOC(), | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -1,26 +1,8 @@ | ||||
| <!-- Buttons to add to the header. --> | ||||
| <core-navbar-buttons slot="end"> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" | ||||
|             iconAction="fas-external-link-alt" [showBrowserWarning]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" | ||||
|             (action)="expandDescription()" iconAction="fas-arrow-right"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper" | ||||
|             (action)="gotoBlog()"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && isOnline" [priority]="700" [content]="'core.refresh' | translate" | ||||
|             (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600" | ||||
|             [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" | ||||
|             [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" | ||||
|             [iconAction]="prefetchStatusIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|     </core-context-menu> | ||||
|     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> | ||||
|         <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
|  | ||||
| @ -70,27 +70,23 @@ export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComp | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async fetchContent(refresh: boolean = false): Promise<void> { | ||||
|         try { | ||||
|             this.chat = await AddonModChat.getChat(this.courseId, this.module.id); | ||||
|     protected async fetchContent(): Promise<void> { | ||||
|         this.chat = await AddonModChat.getChat(this.courseId, this.module.id); | ||||
| 
 | ||||
|             this.description = this.chat.intro; | ||||
|             const now = CoreTimeUtils.timestamp(); | ||||
|             const span = (this.chat.chattime || 0) - now; | ||||
|         this.description = this.chat.intro; | ||||
|         const now = CoreTimeUtils.timestamp(); | ||||
|         const span = (this.chat.chattime || 0) - now; | ||||
| 
 | ||||
|             if (this.chat.chattime && this.chat.schedule && span > 0) { | ||||
|                 this.chatInfo = { | ||||
|                     date: CoreTimeUtils.userDate(this.chat.chattime * 1000), | ||||
|                     fromnow: CoreTimeUtils.formatTime(span), | ||||
|                 }; | ||||
|             } else { | ||||
|                 this.chatInfo = undefined; | ||||
|             } | ||||
| 
 | ||||
|             this.dataRetrieved.emit(this.chat); | ||||
|         } finally { | ||||
|             this.fillContextMenu(refresh); | ||||
|         if (this.chat.chattime && this.chat.schedule && span > 0) { | ||||
|             this.chatInfo = { | ||||
|                 date: CoreTimeUtils.userDate(this.chat.chattime * 1000), | ||||
|                 fromnow: CoreTimeUtils.formatTime(span), | ||||
|             }; | ||||
|         } else { | ||||
|             this.chatInfo = undefined; | ||||
|         } | ||||
| 
 | ||||
|         this.dataRetrieved.emit(this.chat); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -1,28 +1,8 @@ | ||||
| <!-- Buttons to add to the header. --> | ||||
| <core-navbar-buttons slot="end"> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" | ||||
|             iconAction="fas-external-link-alt" [showBrowserWarning]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" | ||||
|             (action)="expandDescription()" iconAction="fas-arrow-right"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper" | ||||
|             (action)="gotoBlog()"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" | ||||
|             (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600" (action)="doRefresh(null, $event, true)" | ||||
|             [content]="'core.settings.synchronizenow' | translate" [iconAction]="syncIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" | ||||
|             [iconAction]="prefetchStatusIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}" | ||||
|             iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|     </core-context-menu> | ||||
|     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> | ||||
|         <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
|  | ||||
| @ -132,43 +132,39 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> { | ||||
|     protected async fetchContent(refresh?: boolean, sync = false, showErrors = false): Promise<void> { | ||||
|         this.now = Date.now(); | ||||
| 
 | ||||
|         try { | ||||
|             this.choice = await AddonModChoice.getChoice(this.courseId, this.module.id); | ||||
|         this.choice = await AddonModChoice.getChoice(this.courseId, this.module.id); | ||||
| 
 | ||||
|             if (sync) { | ||||
|                 // Try to synchronize the choice.
 | ||||
|                 const updated = await this.syncActivity(showErrors); | ||||
|         if (sync) { | ||||
|             // Try to synchronize the choice.
 | ||||
|             const updated = await this.syncActivity(showErrors); | ||||
| 
 | ||||
|                 if (updated) { | ||||
|                     // Responses were sent, update the choice.
 | ||||
|                     this.choice = await AddonModChoice.getChoice(this.courseId, this.module.id); | ||||
|                 } | ||||
|             if (updated) { | ||||
|                 // Responses were sent, update the choice.
 | ||||
|                 this.choice = await AddonModChoice.getChoice(this.courseId, this.module.id); | ||||
|             } | ||||
| 
 | ||||
|             this.choice.timeopen = (this.choice.timeopen || 0) * 1000; | ||||
|             this.choice.timeclose = (this.choice.timeclose || 0) * 1000; | ||||
|             this.openTimeReadable = CoreTimeUtils.userDate(this.choice.timeopen); | ||||
|             this.closeTimeReadable = CoreTimeUtils.userDate(this.choice.timeclose); | ||||
| 
 | ||||
|             this.description = this.choice.intro; | ||||
|             this.choiceNotOpenYet = !!this.choice.timeopen && this.choice.timeopen > this.now; | ||||
|             this.choiceClosed = !!this.choice.timeclose && this.choice.timeclose <= this.now; | ||||
| 
 | ||||
|             this.dataRetrieved.emit(this.choice); | ||||
| 
 | ||||
|             // Check if there are responses stored in offline.
 | ||||
|             this.hasOffline = await AddonModChoiceOffline.hasResponse(this.choice.id); | ||||
| 
 | ||||
|             // We need fetchOptions to finish before calling fetchResults because it needs hasAnsweredOnline variable.
 | ||||
|             await this.fetchOptions(this.choice); | ||||
| 
 | ||||
|             await this.fetchResults(this.choice); | ||||
|         } finally { | ||||
|             this.fillContextMenu(refresh); | ||||
|         } | ||||
| 
 | ||||
|         this.choice.timeopen = (this.choice.timeopen || 0) * 1000; | ||||
|         this.choice.timeclose = (this.choice.timeclose || 0) * 1000; | ||||
|         this.openTimeReadable = CoreTimeUtils.userDate(this.choice.timeopen); | ||||
|         this.closeTimeReadable = CoreTimeUtils.userDate(this.choice.timeclose); | ||||
| 
 | ||||
|         this.description = this.choice.intro; | ||||
|         this.choiceNotOpenYet = !!this.choice.timeopen && this.choice.timeopen > this.now; | ||||
|         this.choiceClosed = !!this.choice.timeclose && this.choice.timeclose <= this.now; | ||||
| 
 | ||||
|         this.dataRetrieved.emit(this.choice); | ||||
| 
 | ||||
|         // Check if there are responses stored in offline.
 | ||||
|         this.hasOffline = await AddonModChoiceOffline.hasResponse(this.choice.id); | ||||
| 
 | ||||
|         // We need fetchOptions to finish before calling fetchResults because it needs hasAnsweredOnline variable.
 | ||||
|         await this.fetchOptions(this.choice); | ||||
| 
 | ||||
|         await this.fetchResults(this.choice); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -433,7 +429,7 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async dataUpdated(online: boolean): Promise<void> { | ||||
|         if (!online || !this.isPrefetched) { | ||||
|         if (!online || !this.isPrefetched()) { | ||||
|             // Not downloaded, just refresh the data.
 | ||||
|             return this.refreshContent(false); | ||||
|         } | ||||
|  | ||||
| @ -4,35 +4,17 @@ | ||||
|         <ion-icon name="fas-search" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" | ||||
|             iconAction="fas-external-link-alt" [showBrowserWarning]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" | ||||
|             (action)="expandDescription()" iconAction="fas-arrow-right"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper" | ||||
|             (action)="gotoBlog()"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && !(hasOffline || hasOfflineRatings) && isOnline" [priority]="700" | ||||
|             [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && (hasOffline || hasOfflineRatings) && isOnline" [priority]="600" | ||||
|             [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" | ||||
|             [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item [priority]="500" *ngIf="canAdd" [content]="'addon.mod_data.addentries' | translate" iconAction="fas-plus" | ||||
|             (action)="gotoAddEntries()"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item [priority]="400" *ngIf="firstEntry" [content]="'addon.mod_data.single' | translate" iconAction="fas-file" | ||||
|             (action)="gotoEntry(firstEntry)"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="300" [content]="prefetchText" (action)="prefetch($event)" | ||||
|             [iconAction]="prefetchStatusIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="200" [content]="'core.clearstoreddata' | translate:{$a: size}" | ||||
|             iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|     </core-context-menu> | ||||
| 
 | ||||
|     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> | ||||
|         <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
|  | ||||
| @ -202,14 +202,9 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Download data contents. | ||||
|      * | ||||
|      * @param refresh If it's refreshing content. | ||||
|      * @param sync If it should try to sync. | ||||
|      * @param showErrors If show errors to the user of hide them. | ||||
|      * @return Promise resolved when done. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> { | ||||
|     protected async fetchContent(refresh?: boolean, sync = false, showErrors = false): Promise<void> { | ||||
|         let canAdd = false; | ||||
|         let canSearch = false; | ||||
| 
 | ||||
| @ -270,7 +265,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp | ||||
|         } finally { | ||||
|             this.canAdd = canAdd; | ||||
|             this.canSearch = canSearch; | ||||
|             this.fillContextMenu(refresh); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -302,7 +296,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp | ||||
|         this.hasNextPage = numEntries >= AddonModDataProvider.PER_PAGE && ((this.search.page + 1) * | ||||
|             AddonModDataProvider.PER_PAGE) < entries.totalcount; | ||||
| 
 | ||||
|         this.hasOffline = entries.hasOfflineActions; | ||||
|         this.hasOffline = !!entries.hasOfflineActions; | ||||
| 
 | ||||
|         this.hasOfflineRatings = !!entries.hasOfflineRatings; | ||||
| 
 | ||||
|  | ||||
| @ -1,28 +1,8 @@ | ||||
| <!-- Buttons to add to the header. --> | ||||
| <core-navbar-buttons slot="end"> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" | ||||
|             iconAction="fas-external-link-alt" [showBrowserWarning]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" | ||||
|             (action)="expandDescription()" iconAction="fas-arrow-right"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper" | ||||
|             (action)="gotoBlog()"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" | ||||
|             (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600" (action)="doRefresh(null, $event, true)" | ||||
|             [content]="'core.settings.synchronizenow' | translate" [iconAction]="syncIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" | ||||
|             [iconAction]="prefetchStatusIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}" | ||||
|             iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|     </core-context-menu> | ||||
|     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> | ||||
|         <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
|  | ||||
| @ -172,7 +172,7 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> { | ||||
|     protected async fetchContent(refresh?: boolean, sync = false, showErrors = false): Promise<void> { | ||||
|         try { | ||||
|             this.feedback = await AddonModFeedback.getFeedback(this.courseId, this.module.id); | ||||
| 
 | ||||
| @ -201,9 +201,6 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity | ||||
| 
 | ||||
|             await this.fetchFeedbackOverviewData(); | ||||
|         } finally { | ||||
|             // Now fill the context menu.
 | ||||
|             this.fillContextMenu(refresh); | ||||
| 
 | ||||
|             if (this.feedback) { | ||||
|                 // Check if there are responses stored in offline.
 | ||||
|                 this.hasOffline = await AddonModFeedbackOffline.hasFeedbackOfflineData(this.feedback.id); | ||||
|  | ||||
| @ -1,25 +1,8 @@ | ||||
| <!-- Buttons to add to the header. --> | ||||
| <core-navbar-buttons slot="end"> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" | ||||
|             iconAction="fas-external-link-alt" [showBrowserWarning]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" | ||||
|             (action)="expandDescription()" iconAction="fas-arrow-right"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper" | ||||
|             (action)="gotoBlog()"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="!subfolder" [priority]="700" [content]="'core.refresh' | translate" | ||||
|             (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="600" [content]="prefetchText" (action)="prefetch($event)" | ||||
|             [iconAction]="prefetchStatusIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="500" [content]="'core.clearstoreddata' | translate:{$a: size}" | ||||
|             iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|     </core-context-menu> | ||||
|     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> | ||||
|         <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
|  | ||||
| @ -12,7 +12,6 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreConstants } from '@/core/constants'; | ||||
| import { Component, Input, OnInit, Optional } from '@angular/core'; | ||||
| import { Params } from '@angular/router'; | ||||
| import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/main-resource-component'; | ||||
| @ -57,7 +56,6 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo | ||||
|             this.contents = this.subfolder; | ||||
| 
 | ||||
|             this.loaded = true; | ||||
|             this.refreshIcon = CoreConstants.ICON_REFRESH; | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| @ -73,7 +71,6 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo | ||||
|             } | ||||
|         } finally { | ||||
|             this.loaded = true; | ||||
|             this.refreshIcon = CoreConstants.ICON_REFRESH; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -87,24 +84,17 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Download folder contents. | ||||
|      * | ||||
|      * @param refresh Whether we're refreshing data. | ||||
|      * @return Promise resolved when done. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async fetchContent(refresh = false): Promise<void> { | ||||
|         try { | ||||
|             this.folderInstance = await AddonModFolder.getFolder(this.courseId, this.module.id); | ||||
|         this.folderInstance = await AddonModFolder.getFolder(this.courseId, this.module.id); | ||||
| 
 | ||||
|             const contents = await CoreCourse.getModuleContents(this.module, undefined, undefined, false, refresh); | ||||
|         const contents = await CoreCourse.getModuleContents(this.module, undefined, undefined, false, refresh); | ||||
| 
 | ||||
|             this.dataRetrieved.emit(this.folderInstance || this.module); | ||||
|         this.dataRetrieved.emit(this.folderInstance || this.module); | ||||
| 
 | ||||
|             this.description = this.folderInstance ? this.folderInstance.intro : this.module.description; | ||||
|             this.contents = AddonModFolderHelper.formatContents(contents); | ||||
|         } finally { | ||||
|             this.fillContextMenu(refresh); | ||||
|         } | ||||
|         this.description = this.folderInstance ? this.folderInstance.intro : this.module.description; | ||||
|         this.contents = AddonModFolderHelper.formatContents(contents); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -1,33 +1,8 @@ | ||||
| <!-- Buttons to add to the header. --> | ||||
| <core-navbar-buttons slot="end"> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" | ||||
|             iconAction="fas-external-link-alt" [showBrowserWarning]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" | ||||
|             (action)="expandDescription()" iconAction="fas-arrow-right"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper" | ||||
|             (action)="gotoBlog()"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="discussions && discussions.loaded && !(hasOffline || hasOfflineRatings) && isOnline" [priority]="700" | ||||
|             [content]="'addon.mod_forum.refreshdiscussions' | translate" [iconAction]="refreshIcon" [closeOnClick]="false" | ||||
|             (action)="doRefresh(null, $event)"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="discussions && discussions.loaded && (hasOffline || hasOfflineRatings) && isOnline" [priority]="600" | ||||
|             [content]="'core.settings.synchronizenow' | translate" [iconAction]="syncIcon" [closeOnClick]="false" | ||||
|             (action)="doRefresh(null, $event, true)"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" [iconAction]="prefetchStatusIcon" | ||||
|             [closeOnClick]="false" (action)="prefetch($event)"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" iconDescription="fas-archive" iconAction="fas-trash" [priority]="400" | ||||
|             [content]="'core.clearstoreddata' | translate:{$a: size}" [closeOnClick]="false" (action)="removeFiles($event)"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="sortingAvailable" iconAction="fas-sort" [priority]="300" [content]="'core.sort' | translate" | ||||
|             (action)="showSortOrderSelector()"> | ||||
|         </core-context-menu-item> | ||||
|     </core-context-menu> | ||||
|     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> | ||||
|         <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
|  | ||||
| @ -296,13 +296,9 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Download the component contents. | ||||
|      * | ||||
|      * @param refresh Whether we're refreshing data. | ||||
|      * @param sync If the refresh needs syncing. | ||||
|      * @param showErrors Wether to show errors to the user or hide them. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> { | ||||
|     protected async fetchContent(refresh = false, sync = false, showErrors = false): Promise<void> { | ||||
|         this.fetchFailed = false; | ||||
| 
 | ||||
|         try { | ||||
| @ -329,8 +325,6 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom | ||||
|             this.fetchFailed = true; // Set to prevent infinite calls with infinite-loading.
 | ||||
| 
 | ||||
|             throw error; // Pass the error to the parent catch.
 | ||||
|         } finally { | ||||
|             this.fillContextMenu(refresh); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -50,7 +50,6 @@ | ||||
|     "posttomygroups": "Post a copy to all groups", | ||||
|     "privatereply": "Reply privately", | ||||
|     "re": "Re:", | ||||
|     "refreshdiscussions": "Refresh discussions", | ||||
|     "refreshposts": "Refresh posts", | ||||
|     "removefromfavourites": "Unstar this discussion", | ||||
|     "reply": "Reply", | ||||
|  | ||||
| @ -9,33 +9,9 @@ | ||||
|         <ion-icon name="fas-search" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| 
 | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" | ||||
|             iconAction="fas-external-link-alt" [showBrowserWarning]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" | ||||
|             (action)="expandDescription()" iconAction="fas-arrow-right"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper" | ||||
|             (action)="gotoBlog()"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && !(hasOffline || hasOfflineRatings) && isOnline" [priority]="700" | ||||
|             [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && (hasOffline || hasOfflineRatings) && isOnline" [priority]="600" | ||||
|             (action)="doRefresh(null, $event, true)" [content]="'core.settings.synchronizenow' | translate" [iconAction]="syncIcon" | ||||
|             [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="canAdd" [priority]="550" [content]="'addon.mod_glossary.addentry' | translate" | ||||
|             (action)="openNewEntry()" iconAction="fas-plus"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" | ||||
|             [iconAction]="prefetchStatusIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}" | ||||
|             iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|     </core-context-menu> | ||||
|     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> | ||||
|         <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
|  | ||||
| @ -177,39 +177,35 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> { | ||||
|     protected async fetchContent(refresh = false, sync = false, showErrors = false): Promise<void> { | ||||
|         const entries = await this.promisedEntries; | ||||
| 
 | ||||
|         try { | ||||
|             await entries.getSource().loadGlossary(); | ||||
|         await entries.getSource().loadGlossary(); | ||||
| 
 | ||||
|             if (!this.glossary) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             this.description = this.glossary.intro || this.description; | ||||
|             this.canAdd = !!this.glossary.canaddentry || false; | ||||
| 
 | ||||
|             this.dataRetrieved.emit(this.glossary); | ||||
| 
 | ||||
|             if (!entries.getSource().fetchMode) { | ||||
|                 this.switchMode('letter_all'); | ||||
|             } | ||||
| 
 | ||||
|             if (sync) { | ||||
|                 // Try to synchronize the glossary.
 | ||||
|                 await this.syncActivity(showErrors); | ||||
|             } | ||||
| 
 | ||||
|             const [hasOfflineRatings] = await Promise.all([ | ||||
|                 CoreRatingOffline.hasRatings('mod_glossary', 'entry', ContextLevel.MODULE, this.glossary.coursemodule), | ||||
|                 refresh ? entries.reload() : entries.load(), | ||||
|             ]); | ||||
| 
 | ||||
|             this.hasOfflineRatings = hasOfflineRatings; | ||||
|         } finally { | ||||
|             this.fillContextMenu(refresh); | ||||
|         if (!this.glossary) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.description = this.glossary.intro || this.description; | ||||
|         this.canAdd = !!this.glossary.canaddentry || false; | ||||
| 
 | ||||
|         this.dataRetrieved.emit(this.glossary); | ||||
| 
 | ||||
|         if (!entries.getSource().fetchMode) { | ||||
|             this.switchMode('letter_all'); | ||||
|         } | ||||
| 
 | ||||
|         if (sync) { | ||||
|             // Try to synchronize the glossary.
 | ||||
|             await this.syncActivity(showErrors); | ||||
|         } | ||||
| 
 | ||||
|         const [hasOfflineRatings] = await Promise.all([ | ||||
|             CoreRatingOffline.hasRatings('mod_glossary', 'entry', ContextLevel.MODULE, this.glossary.coursemodule), | ||||
|             refresh ? entries.reload() : entries.load(), | ||||
|         ]); | ||||
| 
 | ||||
|         this.hasOfflineRatings = hasOfflineRatings; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -3,33 +3,16 @@ | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="h5pActivity && h5pActivity.enabletracking && accessInfo && !accessInfo.canreviewattempts" | ||||
|             [priority]="1000" [content]="'addon.mod_h5pactivity.attempts_report' | translate" (action)="viewMyAttempts()" | ||||
|             iconAction="stats-chart"> | ||||
|             iconAction="fas-chart-bar"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="canViewAllAttempts" [priority]="1000" [content]="'addon.mod_h5pactivity.attempts_report' | translate" | ||||
|             (action)="viewAllAttempts()" iconAction="stats-chart"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" | ||||
|             iconAction="fas-external-link-alt" [showBrowserWarning]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" | ||||
|             (action)="expandDescription()" iconAction="fas-arrow-right"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper" | ||||
|             (action)="gotoBlog()"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" | ||||
|             (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600" (action)="doRefresh(null, $event, true)" | ||||
|             [content]="'core.settings.synchronizenow' | translate" [iconAction]="syncIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" | ||||
|             [iconAction]="prefetchStatusIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}" | ||||
|             iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false"> | ||||
|             (action)="viewAllAttempts()" iconAction="fas-chart-bar"> | ||||
|         </core-context-menu-item> | ||||
|     </core-context-menu> | ||||
| 
 | ||||
|     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> | ||||
|         <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
|  | ||||
| @ -112,51 +112,47 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> { | ||||
|         try { | ||||
|             this.h5pActivity = await AddonModH5PActivity.getH5PActivity(this.courseId, this.module.id, { | ||||
|                 siteId: this.siteId, | ||||
|             }); | ||||
|     protected async fetchContent(refresh?: boolean, sync = false, showErrors = false): Promise<void> { | ||||
|         this.h5pActivity = await AddonModH5PActivity.getH5PActivity(this.courseId, this.module.id, { | ||||
|             siteId: this.siteId, | ||||
|         }); | ||||
| 
 | ||||
|             this.dataRetrieved.emit(this.h5pActivity); | ||||
|             this.description = this.h5pActivity.intro; | ||||
|             this.displayOptions = CoreH5PHelper.decodeDisplayOptions(this.h5pActivity.displayoptions); | ||||
|         this.dataRetrieved.emit(this.h5pActivity); | ||||
|         this.description = this.h5pActivity.intro; | ||||
|         this.displayOptions = CoreH5PHelper.decodeDisplayOptions(this.h5pActivity.displayoptions); | ||||
| 
 | ||||
|             if (sync) { | ||||
|                 await this.syncActivity(showErrors); | ||||
|             } | ||||
|         if (sync) { | ||||
|             await this.syncActivity(showErrors); | ||||
|         } | ||||
| 
 | ||||
|             await Promise.all([ | ||||
|                 this.checkHasOffline(), | ||||
|                 this.fetchAccessInfo(), | ||||
|                 this.fetchDeployedFileData(), | ||||
|             ]); | ||||
|         await Promise.all([ | ||||
|             this.checkHasOffline(), | ||||
|             this.fetchAccessInfo(), | ||||
|             this.fetchDeployedFileData(), | ||||
|         ]); | ||||
| 
 | ||||
|             this.trackComponent = this.accessInfo?.cansubmit ? AddonModH5PActivityProvider.TRACK_COMPONENT : ''; | ||||
|             this.canViewAllAttempts = !!this.h5pActivity.enabletracking && !!this.accessInfo?.canreviewattempts && | ||||
|         this.trackComponent = this.accessInfo?.cansubmit ? AddonModH5PActivityProvider.TRACK_COMPONENT : ''; | ||||
|         this.canViewAllAttempts = !!this.h5pActivity.enabletracking && !!this.accessInfo?.canreviewattempts && | ||||
|                 AddonModH5PActivity.canGetUsersAttemptsInSite(); | ||||
| 
 | ||||
|             if (this.h5pActivity.package && this.h5pActivity.package[0]) { | ||||
|                 // The online player should use the original file, not the trusted one.
 | ||||
|                 this.onlinePlayerUrl = CoreH5P.h5pPlayer.calculateOnlinePlayerUrl( | ||||
|                     this.site.getURL(), | ||||
|                     this.h5pActivity.package[0].fileurl, | ||||
|                     this.displayOptions, | ||||
|                     this.trackComponent, | ||||
|                 ); | ||||
|             } | ||||
|         if (this.h5pActivity.package && this.h5pActivity.package[0]) { | ||||
|             // The online player should use the original file, not the trusted one.
 | ||||
|             this.onlinePlayerUrl = CoreH5P.h5pPlayer.calculateOnlinePlayerUrl( | ||||
|                 this.site.getURL(), | ||||
|                 this.h5pActivity.package[0].fileurl, | ||||
|                 this.displayOptions, | ||||
|                 this.trackComponent, | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|             if (!this.siteCanDownload || this.state == CoreConstants.DOWNLOADED) { | ||||
|                 // Cannot download the file or already downloaded, play the package directly.
 | ||||
|                 this.play(); | ||||
|         if (!this.siteCanDownload || this.state == CoreConstants.DOWNLOADED) { | ||||
|             // Cannot download the file or already downloaded, play the package directly.
 | ||||
|             this.play(); | ||||
| 
 | ||||
|             } else if ((this.state == CoreConstants.NOT_DOWNLOADED || this.state == CoreConstants.OUTDATED) && CoreApp.isOnline() && | ||||
|         } else if ((this.state == CoreConstants.NOT_DOWNLOADED || this.state == CoreConstants.OUTDATED) && CoreApp.isOnline() && | ||||
|                     this.deployedFile?.filesize && CoreFilepool.shouldDownload(this.deployedFile.filesize)) { | ||||
|                 // Package is small, download it automatically. Don't block this function for this.
 | ||||
|                 this.downloadAutomatically(); | ||||
|             } | ||||
|         } finally { | ||||
|             this.fillContextMenu(refresh); | ||||
|             // Package is small, download it automatically. Don't block this function for this.
 | ||||
|             this.downloadAutomatically(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -529,19 +525,6 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv | ||||
|         this.checkHasOffline(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async gotoBlog(): Promise<void> { | ||||
|         this.isOpeningPage = true; | ||||
| 
 | ||||
|         try { | ||||
|             await super.gotoBlog(); | ||||
|         } finally { | ||||
|             this.isOpeningPage = false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component destroyed. | ||||
|      */ | ||||
|  | ||||
| @ -3,26 +3,10 @@ | ||||
|     <ion-button *ngIf="loaded" (click)="showToc()" aria-haspopup="true" [attr.aria-label]="'addon.mod_imscp.toc' | translate"> | ||||
|         <ion-icon name="fas-bookmark" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" | ||||
|             iconAction="fas-external-link-alt" [showBrowserWarning]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" | ||||
|             (action)="expandDescription()" iconAction="fas-arrow-right"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper" | ||||
|             (action)="gotoBlog()"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" | ||||
|             [iconAction]="refreshIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="600" [content]="prefetchText" (action)="prefetch($event)" | ||||
|             [iconAction]="prefetchStatusIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="500" [content]="'core.clearstoreddata' | translate:{$a: size}" | ||||
|             iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|     </core-context-menu> | ||||
| 
 | ||||
|     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> | ||||
|         <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
|  | ||||
| @ -39,6 +39,7 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom | ||||
| 
 | ||||
|     protected items: AddonModImscpTocItem[] = []; | ||||
|     protected currentHref?: string; | ||||
|     protected displayDescription = false; | ||||
| 
 | ||||
|     constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) { | ||||
|         super('AddonModImscpIndexComponent', courseContentsPage); | ||||
| @ -70,42 +71,33 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Download imscp contents. | ||||
|      * | ||||
|      * @param refresh Whether we're refreshing data. | ||||
|      * @return Promise resolved when done. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async fetchContent(refresh = false): Promise<void> { | ||||
|         try { | ||||
|             const downloadResult = await this.downloadResourceIfNeeded(refresh); | ||||
|         const downloadResult = await this.downloadResourceIfNeeded(refresh); | ||||
| 
 | ||||
|             const imscp = await AddonModImscp.getImscp(this.courseId, this.module.id); | ||||
|             this.description = imscp.intro; | ||||
|             this.dataRetrieved.emit(imscp); | ||||
|         const imscp = await AddonModImscp.getImscp(this.courseId, this.module.id); | ||||
|         this.description = imscp.intro; | ||||
|         this.dataRetrieved.emit(imscp); | ||||
| 
 | ||||
|             // Get contents. No need to refresh, it has been done in downloadResourceIfNeeded.
 | ||||
|             const contents = await CoreCourse.getModuleContents(this.module); | ||||
|         // Get contents. No need to refresh, it has been done in downloadResourceIfNeeded.
 | ||||
|         const contents = await CoreCourse.getModuleContents(this.module); | ||||
| 
 | ||||
|             this.items = AddonModImscp.createItemList(contents); | ||||
|         this.items = AddonModImscp.createItemList(contents); | ||||
| 
 | ||||
|             if (this.items.length && this.currentHref === undefined) { | ||||
|                 this.currentHref = this.items[0].href; | ||||
|             } | ||||
| 
 | ||||
|             try { | ||||
|                 await this.loadItemHref(this.currentHref); | ||||
|             } catch (error) { | ||||
|                 CoreDomUtils.showErrorModalDefault(error, 'addon.mod_imscp.deploymenterror', true); | ||||
| 
 | ||||
|                 throw new CoreSilentError(error); | ||||
|             } | ||||
| 
 | ||||
|             this.warning = downloadResult.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : ''; | ||||
| 
 | ||||
|         } finally { | ||||
|             // Pass false because downloadResourceIfNeeded already invalidates and refresh data if refresh=true.
 | ||||
|             this.fillContextMenu(false); | ||||
|         if (this.items.length && this.currentHref === undefined) { | ||||
|             this.currentHref = this.items[0].href; | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             await this.loadItemHref(this.currentHref); | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.showErrorModalDefault(error, 'addon.mod_imscp.deploymenterror', true); | ||||
| 
 | ||||
|             throw new CoreSilentError(error); | ||||
|         } | ||||
| 
 | ||||
|         this.warning = downloadResult.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : ''; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -1,28 +1,8 @@ | ||||
| <!-- Buttons to add to the header. --> | ||||
| <core-navbar-buttons slot="end"> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" | ||||
|             iconAction="fas-external-link-alt" [showBrowserWarning]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" | ||||
|             (action)="expandDescription()" iconAction="fas-arrow-right"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper" | ||||
|             (action)="gotoBlog()"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" | ||||
|             (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600" (action)="doRefresh(null, $event, true)" | ||||
|             [content]="'core.settings.synchronizenow' | translate" [iconAction]="syncIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" | ||||
|             [iconAction]="prefetchStatusIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}" | ||||
|             iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|     </core-context-menu> | ||||
|     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> | ||||
|         <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
|  | ||||
| @ -73,7 +73,6 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo | ||||
|     canManage?: boolean; // Whether the user can manage the lesson.
 | ||||
|     canViewReports?: boolean; // Whether the user can view the lesson reports.
 | ||||
|     showSpinner?: boolean; // Whether to display a spinner.
 | ||||
|     hasOffline?: boolean; // Whether there's offline data.
 | ||||
|     retakeToReview?: AddonModLessonRetakeFinishedInSyncDBRecord; // A retake to review.
 | ||||
|     preventReasons: AddonModLessonPreventAccessReason[] = []; // List of reasons that prevent the lesson from being seen.
 | ||||
|     leftDuringTimed?: boolean; // Whether the user has started and left a retake.
 | ||||
| @ -136,85 +135,76 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the lesson data. | ||||
|      * | ||||
|      * @param refresh If it's refreshing content. | ||||
|      * @param sync If it should try to sync. | ||||
|      * @param showErrors If show errors to the user of hide them. | ||||
|      * @return Promise resolved when done. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> { | ||||
|         try { | ||||
|             let lessonReady = true; | ||||
|             this.askPassword = false; | ||||
|     protected async fetchContent(refresh?: boolean, sync = false, showErrors = false): Promise<void> { | ||||
|         let lessonReady = true; | ||||
|         this.askPassword = false; | ||||
| 
 | ||||
|             this.lesson = await AddonModLesson.getLesson(this.courseId, this.module.id); | ||||
|         this.lesson = await AddonModLesson.getLesson(this.courseId, this.module.id); | ||||
| 
 | ||||
|             this.dataRetrieved.emit(this.lesson); | ||||
|             this.description = this.lesson.intro; // Show description only if intro is present.
 | ||||
|         this.dataRetrieved.emit(this.lesson); | ||||
|         this.description = this.lesson.intro; // Show description only if intro is present.
 | ||||
| 
 | ||||
|             if (sync) { | ||||
|                 // Try to synchronize the lesson.
 | ||||
|                 await this.syncActivity(showErrors); | ||||
|             } | ||||
|         if (sync) { | ||||
|             // Try to synchronize the lesson.
 | ||||
|             await this.syncActivity(showErrors); | ||||
|         } | ||||
| 
 | ||||
|             this.accessInfo = await AddonModLesson.getAccessInformation(this.lesson.id, { cmId: this.module.id }); | ||||
|             this.canManage = this.accessInfo.canmanage; | ||||
|             this.canViewReports = this.accessInfo.canviewreports; | ||||
|             this.preventReasons = []; | ||||
|             const promises: Promise<void>[] = []; | ||||
|         this.accessInfo = await AddonModLesson.getAccessInformation(this.lesson.id, { cmId: this.module.id }); | ||||
|         this.canManage = this.accessInfo.canmanage; | ||||
|         this.canViewReports = this.accessInfo.canviewreports; | ||||
|         this.preventReasons = []; | ||||
|         const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|             if (AddonModLesson.isLessonOffline(this.lesson)) { | ||||
|                 // Handle status.
 | ||||
|                 this.setStatusListener(); | ||||
|         if (AddonModLesson.isLessonOffline(this.lesson)) { | ||||
|             // Handle status.
 | ||||
|             this.setStatusListener(); | ||||
| 
 | ||||
|                 promises.push(this.loadOfflineData()); | ||||
|             } | ||||
|             promises.push(this.loadOfflineData()); | ||||
|         } | ||||
| 
 | ||||
|             if (this.accessInfo.preventaccessreasons.length) { | ||||
|                 let preventReason = AddonModLesson.getPreventAccessReason(this.accessInfo, false); | ||||
|                 const askPassword = preventReason?.reason == 'passwordprotectedlesson'; | ||||
|         if (this.accessInfo.preventaccessreasons.length) { | ||||
|             let preventReason = AddonModLesson.getPreventAccessReason(this.accessInfo, false); | ||||
|             const askPassword = preventReason?.reason == 'passwordprotectedlesson'; | ||||
| 
 | ||||
|                 if (askPassword) { | ||||
|                     try { | ||||
|                         // The lesson requires a password. Check if there is one in memory or DB.
 | ||||
|                         const password = this.password ? | ||||
|                             this.password : | ||||
|                             await AddonModLesson.getStoredPassword(this.lesson.id); | ||||
|             if (askPassword) { | ||||
|                 try { | ||||
|                     // The lesson requires a password. Check if there is one in memory or DB.
 | ||||
|                     const password = this.password ? | ||||
|                         this.password : | ||||
|                         await AddonModLesson.getStoredPassword(this.lesson.id); | ||||
| 
 | ||||
|                         await this.validatePassword(password); | ||||
|                     await this.validatePassword(password); | ||||
| 
 | ||||
|                         // Now that we have the password, get the access reason again ignoring the password.
 | ||||
|                         preventReason = AddonModLesson.getPreventAccessReason(this.accessInfo, true); | ||||
|                         if (preventReason) { | ||||
|                             this.preventReasons = [preventReason]; | ||||
|                         } | ||||
|                     } catch { | ||||
|                         // No password or the validation failed. Show password form.
 | ||||
|                         this.askPassword = true; | ||||
|                         this.preventReasons = [preventReason!]; | ||||
|                         lessonReady = false; | ||||
|                     // Now that we have the password, get the access reason again ignoring the password.
 | ||||
|                     preventReason = AddonModLesson.getPreventAccessReason(this.accessInfo, true); | ||||
|                     if (preventReason) { | ||||
|                         this.preventReasons = [preventReason]; | ||||
|                     } | ||||
|                 } else { | ||||
|                     // Lesson cannot be started.
 | ||||
|                 } catch { | ||||
|                     // No password or the validation failed. Show password form.
 | ||||
|                     this.askPassword = true; | ||||
|                     this.preventReasons = [preventReason!]; | ||||
|                     lessonReady = false; | ||||
|                 } | ||||
|             } else { | ||||
|                 // Lesson cannot be started.
 | ||||
|                 this.preventReasons = [preventReason!]; | ||||
|                 lessonReady = false; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|             if (this.selectedTab == 1 && this.canViewReports) { | ||||
|                 // Only fetch the report data if the tab is selected.
 | ||||
|                 promises.push(this.fetchReportData()); | ||||
|             } | ||||
|         if (this.selectedTab == 1 && this.canViewReports) { | ||||
|             // Only fetch the report data if the tab is selected.
 | ||||
|             promises.push(this.fetchReportData()); | ||||
|         } | ||||
| 
 | ||||
|             await Promise.all(promises); | ||||
|         await Promise.all(promises); | ||||
| 
 | ||||
|             if (lessonReady) { | ||||
|                 // Lesson can be started, don't ask the password and don't show prevent messages.
 | ||||
|                 this.lessonReady(); | ||||
|             } | ||||
|         } finally { | ||||
|             this.fillContextMenu(refresh); | ||||
|         if (lessonReady) { | ||||
|             // Lesson can be started, don't ask the password and don't show prevent messages.
 | ||||
|             this.lessonReady(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -567,13 +557,9 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Displays some data based on the current status. | ||||
|      * | ||||
|      * @param status The current status. | ||||
|      * @param previousStatus The previous status. If not defined, there is no previous status. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||
|     protected showStatus(status: string, previousStatus?: string): void { | ||||
|     protected showStatus(status: string): void { | ||||
|         this.showSpinner = status == CoreConstants.DOWNLOADING; | ||||
|     } | ||||
| 
 | ||||
| @ -633,8 +619,6 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo | ||||
|         } | ||||
| 
 | ||||
|         this.loaded = false; | ||||
|         this.refreshIcon = CoreConstants.ICON_LOADING; | ||||
|         this.syncIcon = CoreConstants.ICON_LOADING; | ||||
| 
 | ||||
|         try { | ||||
|             await this.validatePassword(<string> password); | ||||
| @ -652,8 +636,6 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo | ||||
|             CoreDomUtils.showErrorModal(error); | ||||
|         } finally { | ||||
|             this.loaded = true; | ||||
|             this.refreshIcon = CoreConstants.ICON_REFRESH; | ||||
|             this.syncIcon = CoreConstants.ICON_SYNC; | ||||
| 
 | ||||
|             CoreForms.triggerFormSubmittedEvent(this.formElement, true, this.siteId); | ||||
|         } | ||||
|  | ||||
| @ -1,26 +1,15 @@ | ||||
| <!-- Buttons to add to the header. --> | ||||
| <core-navbar-buttons slot="end"> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" | ||||
|             iconAction="fas-external-link-alt" [showBrowserWarning]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" | ||||
|             (action)="expandDescription()" iconAction="fas-arrow-right"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper" | ||||
|             (action)="gotoBlog()"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && isOnline" [priority]="700" [content]="'core.refresh' | translate" | ||||
|             (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|     </core-context-menu> | ||||
|     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> | ||||
|         <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
| <core-loading [hideUntil]="loaded" class="safe-area-padding"> | ||||
| 
 | ||||
|     <!-- Activity info. --> | ||||
|     <core-course-module-info [module]="module" [description]="lti && lti.showdescriptionlaunch && description" [component]="component" | ||||
|     <core-course-module-info [module]="module" [description]="displayDescription && description" [component]="component" | ||||
|         [componentId]="componentId" [courseId]="courseId"> | ||||
|     </core-course-module-info> | ||||
| 
 | ||||
|  | ||||
| @ -31,6 +31,7 @@ export class AddonModLtiIndexComponent extends CoreCourseModuleMainActivityCompo | ||||
| 
 | ||||
|     component = AddonModLtiProvider.COMPONENT; | ||||
|     moduleName = 'lti'; | ||||
|     displayDescription = false; | ||||
| 
 | ||||
|     lti?: AddonModLtiLti; // The LTI object.
 | ||||
| 
 | ||||
| @ -55,15 +56,13 @@ export class AddonModLtiIndexComponent extends CoreCourseModuleMainActivityCompo | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async fetchContent(refresh: boolean = false): Promise<void> { | ||||
|         try { | ||||
|             this.lti = await AddonModLti.getLti(this.courseId, this.module.id); | ||||
|     protected async fetchContent(): Promise<void> { | ||||
|         this.lti = await AddonModLti.getLti(this.courseId, this.module.id); | ||||
| 
 | ||||
|             this.description = this.lti.intro; | ||||
|             this.dataRetrieved.emit(this.lti); | ||||
|         } finally { | ||||
|             this.fillContextMenu(refresh); | ||||
|         } | ||||
|         this.description = this.lti.intro; | ||||
| 
 | ||||
|         this.displayDescription = this.lti && !!this.lti.showdescriptionlaunch; | ||||
|         this.dataRetrieved.emit(this.lti); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -1,25 +1,8 @@ | ||||
| <!-- Buttons to add to the header. --> | ||||
| <core-navbar-buttons slot="end"> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" | ||||
|             iconAction="fas-external-link-alt" [showBrowserWarning]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" | ||||
|             (action)="expandDescription()" iconAction="fas-arrow-right"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper" | ||||
|             (action)="gotoBlog()"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" | ||||
|             [iconAction]="refreshIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="600" [content]="prefetchText" (action)="prefetch($event)" | ||||
|             [iconAction]="prefetchStatusIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="500" [content]="'core.clearstoreddata' | translate:{$a: size}" | ||||
|             iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|     </core-context-menu> | ||||
|     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> | ||||
|         <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
|  | ||||
| @ -71,29 +71,22 @@ export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComp | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Download page contents. | ||||
|      * | ||||
|      * @param refresh Whether we're refreshing data. | ||||
|      * @return Promise resolved when done. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async fetchContent(refresh?: boolean): Promise<void> { | ||||
|         try { | ||||
|             // Download the resource if it needs to be downloaded.
 | ||||
|             const downloadResult = await this.downloadResourceIfNeeded(refresh); | ||||
|         // Download the resource if it needs to be downloaded.
 | ||||
|         const downloadResult = await this.downloadResourceIfNeeded(refresh); | ||||
| 
 | ||||
|             // Get contents. No need to refresh, it has been done in downloadResourceIfNeeded.
 | ||||
|             const contents = await CoreCourse.getModuleContents(this.module); | ||||
|         // Get contents. No need to refresh, it has been done in downloadResourceIfNeeded.
 | ||||
|         const contents = await CoreCourse.getModuleContents(this.module); | ||||
| 
 | ||||
|             const results = await Promise.all([ | ||||
|                 this.loadPageData(), | ||||
|                 AddonModPageHelper.getPageHtml(contents, this.module.id), | ||||
|             ]); | ||||
|         const results = await Promise.all([ | ||||
|             this.loadPageData(), | ||||
|             AddonModPageHelper.getPageHtml(contents, this.module.id), | ||||
|         ]); | ||||
| 
 | ||||
|             this.contents = results[1]; | ||||
|             this.warning = downloadResult?.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : ''; | ||||
|         } finally { | ||||
|             this.fillContextMenu(refresh); | ||||
|         } | ||||
|         this.contents = results[1]; | ||||
|         this.warning = downloadResult?.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : ''; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -1,28 +1,8 @@ | ||||
| <!-- Buttons to add to the header. --> | ||||
| <core-navbar-buttons slot="end"> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" | ||||
|             iconAction="fas-external-link-alt" [showBrowserWarning]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" | ||||
|             (action)="expandDescription()" iconAction="fas-arrow-right"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" [iconAction]="'far-newspaper'" | ||||
|             (action)="gotoBlog()"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" | ||||
|             (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600" (action)="doRefresh(null, $event, true)" | ||||
|             [content]="'core.settings.synchronizenow' | translate" [iconAction]="syncIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" | ||||
|             [iconAction]="prefetchStatusIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}" | ||||
|             iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|     </core-context-menu> | ||||
|     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> | ||||
|         <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
| @ -210,7 +190,7 @@ | ||||
| 
 | ||||
|             <!-- Button to open in browser if it cannot be attempted in the app. --> | ||||
|             <ion-button class="ion-text-wrap ion-margin" *ngIf="!buttonText && ((!hasSupportedQuestions && unsupportedQuestions.length) || | ||||
|                 unsupportedRules.length || behaviourSupported === false)" expand="block" [href]="externalUrl" core-link | ||||
|                 unsupportedRules.length || behaviourSupported === false)" expand="block" [href]="module.url" core-link | ||||
|                 [showBrowserWarning]="false"> | ||||
|                 {{ 'core.openinbrowser' | translate }} | ||||
|                 <ion-icon name="fas-external-link-alt" slot="end" aria-hidden="true"></ion-icon> | ||||
|  | ||||
| @ -180,82 +180,73 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the quiz data. | ||||
|      * | ||||
|      * @param refresh If it's refreshing content. | ||||
|      * @param sync If it should try to sync. | ||||
|      * @param showErrors If show errors to the user of hide them. | ||||
|      * @return Promise resolved when done. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> { | ||||
|         try { | ||||
|             // First get the quiz instance.
 | ||||
|             const quiz = await AddonModQuiz.getQuiz(this.courseId, this.module.id); | ||||
|     protected async fetchContent(refresh?: boolean, sync = false, showErrors = false): Promise<void> { | ||||
|         // First get the quiz instance.
 | ||||
|         const quiz = await AddonModQuiz.getQuiz(this.courseId, this.module.id); | ||||
| 
 | ||||
|             this.gradeMethodReadable = AddonModQuiz.getQuizGradeMethod(quiz.grademethod); | ||||
|             this.now = Date.now(); | ||||
|             this.dataRetrieved.emit(quiz); | ||||
|             this.description = quiz.intro || this.description; | ||||
|             this.candidateQuiz = quiz; | ||||
|         this.gradeMethodReadable = AddonModQuiz.getQuizGradeMethod(quiz.grademethod); | ||||
|         this.now = Date.now(); | ||||
|         this.dataRetrieved.emit(quiz); | ||||
|         this.description = quiz.intro || this.description; | ||||
|         this.candidateQuiz = quiz; | ||||
| 
 | ||||
|             // Try to get warnings from automatic sync.
 | ||||
|             const warnings = await AddonModQuizSync.getSyncWarnings(quiz.id); | ||||
|         // Try to get warnings from automatic sync.
 | ||||
|         const warnings = await AddonModQuizSync.getSyncWarnings(quiz.id); | ||||
| 
 | ||||
|             if (warnings?.length) { | ||||
|                 // Show warnings and delete them so they aren't shown again.
 | ||||
|                 CoreDomUtils.showErrorModal(CoreTextUtils.buildMessage(warnings)); | ||||
|         if (warnings?.length) { | ||||
|             // Show warnings and delete them so they aren't shown again.
 | ||||
|             CoreDomUtils.showErrorModal(CoreTextUtils.buildMessage(warnings)); | ||||
| 
 | ||||
|                 await AddonModQuizSync.setSyncWarnings(quiz.id, []); | ||||
|             } | ||||
| 
 | ||||
|             if (AddonModQuiz.isQuizOffline(quiz)) { | ||||
|                 if (sync) { | ||||
|                     // Try to sync the quiz.
 | ||||
|                     try { | ||||
|                         await this.syncActivity(showErrors); | ||||
|                     } catch { | ||||
|                         // Ignore errors, keep getting data even if sync fails.
 | ||||
|                         this.autoReview = undefined; | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|                 this.autoReview = undefined; | ||||
|                 this.showStatusSpinner = false; | ||||
|             } | ||||
| 
 | ||||
|             if (AddonModQuiz.isQuizOffline(quiz)) { | ||||
|                 // Handle status.
 | ||||
|                 this.setStatusListener(); | ||||
| 
 | ||||
|                 // Get last synchronization time and check if sync button should be seen.
 | ||||
|                 this.syncTime = await AddonModQuizSync.getReadableSyncTime(quiz.id); | ||||
|                 this.hasOffline = await AddonModQuizSync.hasDataToSync(quiz.id); | ||||
|             } | ||||
| 
 | ||||
|             // Get quiz access info.
 | ||||
|             this.quizAccessInfo = await AddonModQuiz.getQuizAccessInformation(quiz.id, { cmId: this.module.id }); | ||||
| 
 | ||||
|             this.showReviewColumn = this.quizAccessInfo.canreviewmyattempts; | ||||
|             this.accessRules = this.quizAccessInfo.accessrules; | ||||
|             this.unsupportedRules = AddonModQuiz.getUnsupportedRules(this.quizAccessInfo.activerulenames); | ||||
| 
 | ||||
|             if (quiz.preferredbehaviour) { | ||||
|                 this.behaviourSupported = CoreQuestionBehaviourDelegate.isBehaviourSupported(quiz.preferredbehaviour); | ||||
|             } | ||||
| 
 | ||||
|             // Get question types in the quiz.
 | ||||
|             const types = await AddonModQuiz.getQuizRequiredQtypes(quiz.id, { cmId: this.module.id }); | ||||
| 
 | ||||
|             this.unsupportedQuestions = AddonModQuiz.getUnsupportedQuestions(types); | ||||
|             this.hasSupportedQuestions = !!types.find((type) => type != 'random' && this.unsupportedQuestions.indexOf(type) == -1); | ||||
| 
 | ||||
|             await this.getAttempts(quiz); | ||||
| 
 | ||||
|             // Quiz is ready to be shown, move it to the variable that is displayed.
 | ||||
|             this.quiz = quiz; | ||||
|         } finally { | ||||
|             this.fillContextMenu(refresh); | ||||
|             await AddonModQuizSync.setSyncWarnings(quiz.id, []); | ||||
|         } | ||||
| 
 | ||||
|         if (AddonModQuiz.isQuizOffline(quiz)) { | ||||
|             if (sync) { | ||||
|                 // Try to sync the quiz.
 | ||||
|                 try { | ||||
|                     await this.syncActivity(showErrors); | ||||
|                 } catch { | ||||
|                     // Ignore errors, keep getting data even if sync fails.
 | ||||
|                     this.autoReview = undefined; | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             this.autoReview = undefined; | ||||
|             this.showStatusSpinner = false; | ||||
|         } | ||||
| 
 | ||||
|         if (AddonModQuiz.isQuizOffline(quiz)) { | ||||
|             // Handle status.
 | ||||
|             this.setStatusListener(); | ||||
| 
 | ||||
|             // Get last synchronization time and check if sync button should be seen.
 | ||||
|             this.syncTime = await AddonModQuizSync.getReadableSyncTime(quiz.id); | ||||
|             this.hasOffline = await AddonModQuizSync.hasDataToSync(quiz.id); | ||||
|         } | ||||
| 
 | ||||
|         // Get quiz access info.
 | ||||
|         this.quizAccessInfo = await AddonModQuiz.getQuizAccessInformation(quiz.id, { cmId: this.module.id }); | ||||
| 
 | ||||
|         this.showReviewColumn = this.quizAccessInfo.canreviewmyattempts; | ||||
|         this.accessRules = this.quizAccessInfo.accessrules; | ||||
|         this.unsupportedRules = AddonModQuiz.getUnsupportedRules(this.quizAccessInfo.activerulenames); | ||||
| 
 | ||||
|         if (quiz.preferredbehaviour) { | ||||
|             this.behaviourSupported = CoreQuestionBehaviourDelegate.isBehaviourSupported(quiz.preferredbehaviour); | ||||
|         } | ||||
| 
 | ||||
|         // Get question types in the quiz.
 | ||||
|         const types = await AddonModQuiz.getQuizRequiredQtypes(quiz.id, { cmId: this.module.id }); | ||||
| 
 | ||||
|         this.unsupportedQuestions = AddonModQuiz.getUnsupportedQuestions(types); | ||||
|         this.hasSupportedQuestions = !!types.find((type) => type != 'random' && this.unsupportedQuestions.indexOf(type) == -1); | ||||
| 
 | ||||
|         await this.getAttempts(quiz); | ||||
| 
 | ||||
|         // Quiz is ready to be shown, move it to the variable that is displayed.
 | ||||
|         this.quiz = quiz; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -465,16 +456,12 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp | ||||
| 
 | ||||
|         // Refresh data.
 | ||||
|         this.loaded = false; | ||||
|         this.refreshIcon = CoreConstants.ICON_LOADING; | ||||
|         this.syncIcon = CoreConstants.ICON_LOADING; | ||||
|         this.content?.scrollToTop(); | ||||
| 
 | ||||
|         await promise; | ||||
|         await CoreUtils.ignoreErrors(this.refreshContent(true)); | ||||
| 
 | ||||
|         this.loaded = true; | ||||
|         this.refreshIcon = CoreConstants.ICON_REFRESH; | ||||
|         this.syncIcon = CoreConstants.ICON_SYNC; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -1,29 +1,16 @@ | ||||
| <!-- Buttons to add to the header. --> | ||||
| <core-navbar-buttons slot="end"> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" | ||||
|             iconAction="fas-external-link-alt" [showBrowserWarning]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" | ||||
|             (action)="expandDescription()" iconAction="fas-arrow-right"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper" | ||||
|             (action)="gotoBlog()"></core-context-menu-item> | ||||
|         <core-context-menu-item [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" | ||||
|             [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="600" [content]="prefetchText" (action)="prefetch($event)" | ||||
|             [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="500" [content]="'core.clearstoreddata' | translate:{$a: size}" | ||||
|             iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|     </core-context-menu> | ||||
|     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> | ||||
|         <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
| <core-loading [hideUntil]="loaded" class="safe-area-padding"> | ||||
| 
 | ||||
|     <!-- Activity info. --> | ||||
|     <core-course-module-info [module]="module" [courseId]="courseId" | ||||
|         [description]="mode != 'iframe' && (mode != 'embedded' || displayDescription) && description" [component]="component" | ||||
|         [componentId]="componentId"> | ||||
|     <core-course-module-info [module]="module" [courseId]="courseId" [description]="displayDescription && description" | ||||
|         [component]="component" [componentId]="componentId"> | ||||
|     </core-course-module-info> | ||||
| 
 | ||||
|     <ion-card class="core-warning-card" *ngIf="warning"> | ||||
| @ -76,7 +63,7 @@ | ||||
|                     <ion-label> | ||||
|                         <h3>{{ 'core.lastdownloaded' | translate }}</h3> | ||||
|                         <p>{{ downloadTimeReadable }}</p> | ||||
|                         <ion-grid *ngIf="prefetchStatus === outdatedStatus" class="addon-mod_resource-outdated"> | ||||
|                         <ion-grid *ngIf="currentStatus === outdatedStatus" class="addon-mod_resource-outdated"> | ||||
|                             <ion-row class="ion-align-items-center"> | ||||
|                                 <ion-col size="auto"> | ||||
|                                     <ion-icon color="warning" name="fas-exclamation-triangle" aria-hidden="true"></ion-icon> | ||||
|  | ||||
| @ -116,76 +116,76 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource | ||||
|             throw new CoreError(Translate.instant('core.filenotfound')); | ||||
|         } | ||||
| 
 | ||||
|         let hasCalledDownloadResource = false; | ||||
| 
 | ||||
|         // Get the resource instance to get the latest name/description and to know if it's embedded.
 | ||||
|         const resource = await AddonModResource.getResourceData(this.courseId, this.module.id); | ||||
|         this.description = resource.intro || ''; | ||||
|         const options: AddonModResourceCustomData = | ||||
|             resource.displayoptions ? CoreTextUtils.unserialize(resource.displayoptions) : {}; | ||||
| 
 | ||||
|         try { | ||||
|             this.displayDescription = options.printintro === undefined || !!options.printintro; | ||||
|             this.dataRetrieved.emit(resource); | ||||
|         this.displayDescription = options.printintro === undefined || !!options.printintro; | ||||
|         this.dataRetrieved.emit(resource); | ||||
| 
 | ||||
|             if (AddonModResourceHelper.isDisplayedInIframe(this.module)) { | ||||
|                 hasCalledDownloadResource = true; | ||||
|         this.setStatusListener(); | ||||
| 
 | ||||
|                 const downloadResult = await this.downloadResourceIfNeeded(refresh, true); | ||||
|                 const src = await AddonModResourceHelper.getIframeSrc(this.module); | ||||
|                 this.mode = 'iframe'; | ||||
|         if (AddonModResourceHelper.isDisplayedInIframe(this.module)) { | ||||
| 
 | ||||
|                 if (this.src && src.toString() == this.src.toString()) { | ||||
|                     // Re-loading same page.
 | ||||
|                     // Set it to empty and then re-set the src in the next digest so it detects it has changed.
 | ||||
|                     this.src = ''; | ||||
|                     setTimeout(() => { | ||||
|                         this.src = src; | ||||
|                     }); | ||||
|                 } else { | ||||
|             const downloadResult = await this.downloadResourceIfNeeded(refresh, true); | ||||
|             const src = await AddonModResourceHelper.getIframeSrc(this.module); | ||||
|             this.mode = 'iframe'; | ||||
| 
 | ||||
|             if (this.src && src.toString() == this.src.toString()) { | ||||
|                 // Re-loading same page.
 | ||||
|                 // Set it to empty and then re-set the src in the next digest so it detects it has changed.
 | ||||
|                 this.src = ''; | ||||
|                 setTimeout(() => { | ||||
|                     this.src = src; | ||||
|                 } | ||||
| 
 | ||||
|                 this.warning = downloadResult.failed | ||||
|                     ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) | ||||
|                     : ''; | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             if (resource && 'display' in resource && AddonModResourceHelper.isDisplayedEmbedded(this.module, resource.display)) { | ||||
|                 this.mode = 'embedded'; | ||||
|                 this.warning = ''; | ||||
| 
 | ||||
|                 this.contentText = await AddonModResourceHelper.getEmbeddedHtml(this.module); | ||||
|                 this.mode = this.contentText.length > 0 ? 'embedded' : 'external'; | ||||
|                 }); | ||||
|             } else { | ||||
|                 this.mode = 'external'; | ||||
|                 this.warning = ''; | ||||
|                 let mimetype: string; | ||||
| 
 | ||||
|                 if (this.isIOS) { | ||||
|                     this.shouldOpenInBrowser = CoreFileHelper.shouldOpenInBrowser(contents[0]); | ||||
|                 } | ||||
| 
 | ||||
|                 if ('contentsinfo' in this.module && this.module.contentsinfo) { | ||||
|                     mimetype = this.module.contentsinfo.mimetypes[0]; | ||||
|                     this.readableSize = CoreTextUtils.bytesToSize(this.module.contentsinfo.filessize, 1); | ||||
|                     this.timemodified = this.module.contentsinfo.lastmodified * 1000; | ||||
|                 } else { | ||||
|                     mimetype = await CoreUtils.getMimeTypeFromUrl(CoreFileHelper.getFileUrl(contents[0])); | ||||
|                     this.readableSize = CoreTextUtils.bytesToSize(contents[0].filesize, 1); | ||||
|                     this.timemodified = contents[0].timemodified * 1000; | ||||
|                 } | ||||
| 
 | ||||
|                 this.timecreated = contents[0].timecreated * 1000; | ||||
|                 this.isExternalFile = !!contents[0].isexternalfile; | ||||
|                 this.type = CoreMimetypeUtils.getMimetypeDescription(mimetype); | ||||
|                 this.isStreamedFile = CoreMimetypeUtils.isStreamedMimetype(mimetype); | ||||
|                 this.src = src; | ||||
|             } | ||||
|         } finally { | ||||
|             // Pass false in some cases because downloadResourceIfNeeded already invalidates and refresh data if refresh=true.
 | ||||
|             this.fillContextMenu(hasCalledDownloadResource ? false : refresh); | ||||
| 
 | ||||
|             // Never show description on iframe.
 | ||||
|             this.displayDescription = false; | ||||
| 
 | ||||
|             this.warning = downloadResult.failed | ||||
|                 ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) | ||||
|                 : ''; | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (resource && 'display' in resource && AddonModResourceHelper.isDisplayedEmbedded(this.module, resource.display)) { | ||||
|             this.mode = 'embedded'; | ||||
|             this.warning = ''; | ||||
| 
 | ||||
|             this.contentText = await AddonModResourceHelper.getEmbeddedHtml(this.module); | ||||
|             this.mode = this.contentText.length > 0 ? 'embedded' : 'external'; | ||||
|         } else { | ||||
|             this.mode = 'external'; | ||||
|             this.warning = ''; | ||||
|             let mimetype: string; | ||||
| 
 | ||||
|             // Always show description on external.
 | ||||
|             this.displayDescription = true; | ||||
| 
 | ||||
|             if (this.isIOS) { | ||||
|                 this.shouldOpenInBrowser = CoreFileHelper.shouldOpenInBrowser(contents[0]); | ||||
|             } | ||||
| 
 | ||||
|             if ('contentsinfo' in this.module && this.module.contentsinfo) { | ||||
|                 mimetype = this.module.contentsinfo.mimetypes[0]; | ||||
|                 this.readableSize = CoreTextUtils.bytesToSize(this.module.contentsinfo.filessize, 1); | ||||
|                 this.timemodified = this.module.contentsinfo.lastmodified * 1000; | ||||
|             } else { | ||||
|                 mimetype = await CoreUtils.getMimeTypeFromUrl(CoreFileHelper.getFileUrl(contents[0])); | ||||
|                 this.readableSize = CoreTextUtils.bytesToSize(contents[0].filesize, 1); | ||||
|                 this.timemodified = contents[0].timemodified * 1000; | ||||
|             } | ||||
| 
 | ||||
|             this.timecreated = contents[0].timecreated * 1000; | ||||
|             this.isExternalFile = !!contents[0].isexternalfile; | ||||
|             this.type = CoreMimetypeUtils.getMimetypeDescription(mimetype); | ||||
|             this.isStreamedFile = CoreMimetypeUtils.isStreamedMimetype(mimetype); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -204,7 +204,7 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource | ||||
|             downloadable = await AddonModResourceHelper.isMainFileDownloadable(this.module); | ||||
| 
 | ||||
|             if (downloadable) { | ||||
|                 if (this.prefetchStatus === CoreConstants.OUTDATED && !this.isOnline) { | ||||
|                 if (this.currentStatus === CoreConstants.OUTDATED && !this.isOnline) { | ||||
|                     // Warn the user that the file isn't updated.
 | ||||
|                     const alert = await CoreDomUtils.showAlert( | ||||
|                         undefined, | ||||
|  | ||||
| @ -1,28 +1,8 @@ | ||||
| <!-- Buttons to add to the header. --> | ||||
| <core-navbar-buttons slot="end"> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" | ||||
|             iconAction="fas-external-link-alt" [showBrowserWarning]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" | ||||
|             (action)="expandDescription()" iconAction="fas-arrow-right"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper" | ||||
|             (action)="gotoBlog()"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" | ||||
|             (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600" (action)="doRefresh(null, $event, true)" | ||||
|             [content]="'core.settings.synchronizenow' | translate" [iconAction]="syncIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" | ||||
|             [iconAction]="prefetchStatusIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}" | ||||
|             iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|     </core-context-menu> | ||||
|     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> | ||||
|         <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
| @ -172,7 +152,7 @@ | ||||
|                     <p class="text-danger">{{ errorMessage | translate }}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ion-button class="ion-margin ion-text-wrap" expand="block" [href]="externalUrl" core-link [showBrowserWarning]="false"> | ||||
|             <ion-button class="ion-margin ion-text-wrap" expand="block" [href]="module.url" core-link [showBrowserWarning]="false"> | ||||
|                 {{ 'core.openinbrowser' | translate }} | ||||
|                 <ion-icon name="fas-external-link-alt" slot="end" aria-hidden="true"></ion-icon> | ||||
|             </ion-button> | ||||
|  | ||||
| @ -175,43 +175,39 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> { | ||||
|         try { | ||||
|             // Get the SCORM instance.
 | ||||
|             this.scorm = await AddonModScorm.getScorm(this.courseId, this.module.id, { moduleUrl: this.module.url }); | ||||
|     protected async fetchContent(refresh?: boolean, sync = false, showErrors = false): Promise<void> { | ||||
|         // Get the SCORM instance.
 | ||||
|         this.scorm = await AddonModScorm.getScorm(this.courseId, this.module.id, { moduleUrl: this.module.url }); | ||||
| 
 | ||||
|             this.dataRetrieved.emit(this.scorm); | ||||
|             this.description = this.scorm.intro || this.description; | ||||
|             this.errorMessage = AddonModScorm.isScormUnsupported(this.scorm); | ||||
|         this.dataRetrieved.emit(this.scorm); | ||||
|         this.description = this.scorm.intro || this.description; | ||||
|         this.errorMessage = AddonModScorm.isScormUnsupported(this.scorm); | ||||
| 
 | ||||
|             if (this.scorm.warningMessage) { | ||||
|                 return; // SCORM is closed or not open yet, we can't get more data.
 | ||||
|             } | ||||
|         if (this.scorm.warningMessage) { | ||||
|             return; // SCORM is closed or not open yet, we can't get more data.
 | ||||
|         } | ||||
| 
 | ||||
|             if (sync) { | ||||
|                 // Try to synchronize the SCORM.
 | ||||
|                 await CoreUtils.ignoreErrors(this.syncActivity(showErrors)); | ||||
|             } | ||||
|         if (sync) { | ||||
|             // Try to synchronize the SCORM.
 | ||||
|             await CoreUtils.ignoreErrors(this.syncActivity(showErrors)); | ||||
|         } | ||||
| 
 | ||||
|             const [syncTime, accessInfo] = await Promise.all([ | ||||
|                 AddonModScormSync.getReadableSyncTime(this.scorm.id), | ||||
|                 AddonModScorm.getAccessInformation(this.scorm.id, { cmId: this.module.id }), | ||||
|                 this.fetchAttemptData(this.scorm), | ||||
|             ]); | ||||
|         const [syncTime, accessInfo] = await Promise.all([ | ||||
|             AddonModScormSync.getReadableSyncTime(this.scorm.id), | ||||
|             AddonModScorm.getAccessInformation(this.scorm.id, { cmId: this.module.id }), | ||||
|             this.fetchAttemptData(this.scorm), | ||||
|         ]); | ||||
| 
 | ||||
|             this.syncTime = syncTime; | ||||
|             this.accessInfo = accessInfo; | ||||
|         this.syncTime = syncTime; | ||||
|         this.accessInfo = accessInfo; | ||||
| 
 | ||||
|             // Check whether to launch the SCORM immediately.
 | ||||
|             if (this.skip === undefined) { | ||||
|                 this.skip = !this.hasOffline && !this.errorMessage && | ||||
|         // Check whether to launch the SCORM immediately.
 | ||||
|         if (this.skip === undefined) { | ||||
|             this.skip = !this.hasOffline && !this.errorMessage && | ||||
|                     (!this.scorm.lastattemptlock || this.attemptsLeft > 0) && | ||||
|                     this.accessInfo.canskipview && !this.accessInfo.canviewreport && | ||||
|                     this.scorm.skipview! >= AddonModScormProvider.SKIPVIEW_FIRST && | ||||
|                     (this.scorm.skipview == AddonModScormProvider.SKIPVIEW_ALWAYS || this.lastAttempt == 0); | ||||
|             } | ||||
|         } finally { | ||||
|             this.fillContextMenu(refresh); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -1,29 +1,8 @@ | ||||
| <!-- Buttons to add to the header. --> | ||||
| <core-navbar-buttons slot="end"> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" | ||||
|             iconAction="fas-external-link-alt" [showBrowserWarning]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" | ||||
|             (action)="expandDescription()" iconAction="fas-arrow-right"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper" | ||||
|             (action)="gotoBlog()"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" | ||||
|             (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600" | ||||
|             [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" | ||||
|             [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" | ||||
|             [iconAction]="prefetchStatusIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}" | ||||
|             iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|     </core-context-menu> | ||||
|     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> | ||||
|         <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
| @ -37,7 +16,7 @@ | ||||
|     <!-- Survey already done --> | ||||
|     <ion-card class="ion-padding" *ngIf="survey && survey.surveydone"> | ||||
|         <p class="ion-padding">{{ 'addon.mod_survey.surveycompletednograph' | translate }}</p> | ||||
|         <ion-button expand="block" [href]="externalUrl" core-link> | ||||
|         <ion-button expand="block" [href]="module.url" core-link> | ||||
|             <ion-icon name="fas-external-link-alt" slot="start" aria-hidden="true"></ion-icon> | ||||
|             {{ 'addon.mod_survey.results' | translate }} | ||||
|         </ion-button> | ||||
|  | ||||
| @ -115,39 +115,30 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Download survey contents. | ||||
|      * | ||||
|      * @param refresh If it's refreshing content. | ||||
|      * @param sync If it should try to sync. | ||||
|      * @param showErrors If show errors to the user of hide them. | ||||
|      * @return Promise resolved when done. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> { | ||||
|         try { | ||||
|             this.survey = await AddonModSurvey.getSurvey(this.courseId, this.module.id); | ||||
|     protected async fetchContent(refresh?: boolean, sync = false, showErrors = false): Promise<void> { | ||||
|         this.survey = await AddonModSurvey.getSurvey(this.courseId, this.module.id); | ||||
| 
 | ||||
|             this.description = this.survey.intro; | ||||
|             this.dataRetrieved.emit(this.survey); | ||||
|         this.description = this.survey.intro; | ||||
|         this.dataRetrieved.emit(this.survey); | ||||
| 
 | ||||
|             if (sync) { | ||||
|                 // Try to synchronize the survey.
 | ||||
|                 const answersSent = await this.syncActivity(showErrors); | ||||
|                 if (answersSent) { | ||||
|                     // Answers were sent, update the survey.
 | ||||
|                     this.survey = await AddonModSurvey.getSurvey(this.courseId, this.module.id); | ||||
|                 } | ||||
|         if (sync) { | ||||
|             // Try to synchronize the survey.
 | ||||
|             const answersSent = await this.syncActivity(showErrors); | ||||
|             if (answersSent) { | ||||
|                 // Answers were sent, update the survey.
 | ||||
|                 this.survey = await AddonModSurvey.getSurvey(this.courseId, this.module.id); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|             // Check if there are answers stored in offline.
 | ||||
|             this.hasOffline = this.survey.surveydone | ||||
|                 ? false | ||||
|                 : await AddonModSurveyOffline.hasAnswers(this.survey.id); | ||||
|         // Check if there are answers stored in offline.
 | ||||
|         this.hasOffline = this.survey.surveydone | ||||
|             ? false | ||||
|             : await AddonModSurveyOffline.hasAnswers(this.survey.id); | ||||
| 
 | ||||
|             if (!this.survey.surveydone && !this.hasOffline) { | ||||
|                 await this.fetchQuestions(); | ||||
|             } | ||||
|         } finally { | ||||
|             this.fillContextMenu(refresh); | ||||
|         if (!this.survey.surveydone && !this.hasOffline) { | ||||
|             await this.fetchQuestions(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -1,15 +1,8 @@ | ||||
| <!-- Buttons to add to the header. --> | ||||
| <core-navbar-buttons slot="end"> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" | ||||
|             iconAction="fas-external-link-alt" [showBrowserWarning]="false"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" | ||||
|             (action)="expandDescription()" iconAction="fas-arrow-right"></core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper" | ||||
|             (action)="gotoBlog()"></core-context-menu-item> | ||||
|         <core-context-menu-item [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" | ||||
|             [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item> | ||||
|     </core-context-menu> | ||||
|     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> | ||||
|         <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
|  | ||||
| @ -13,35 +13,17 @@ | ||||
|     </ion-button> | ||||
| 
 | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" | ||||
|             iconAction="fas-external-link-alt" [showBrowserWarning]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" | ||||
|             (action)="expandDescription()" iconAction="fas-arrow-right"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper" | ||||
|             (action)="gotoBlog()"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && !hasOffline && isOnline && !pageIsOffline" [priority]="700" | ||||
|             [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && isOnline && (hasOffline || pageIsOffline)" [priority]="600" | ||||
|             [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" | ||||
|             [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="canEdit && (isOnline || pageIsOffline)" [priority]="590" [content]="'core.edit' | translate" | ||||
|             iconAction="fas-edit" (action)="goToEditPage()"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="canEdit" [priority]="580" [content]="'addon.mod_wiki.createpage' | translate" iconAction="fas-plus" | ||||
|             (action)="goToNewPage()"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" | ||||
|             [iconAction]="prefetchStatusIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}" | ||||
|             iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|     </core-context-menu> | ||||
| 
 | ||||
|     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> | ||||
|         <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
|  | ||||
| @ -21,14 +21,16 @@ import { CoreCourse } from '@features/course/services/course'; | ||||
| import { CoreTag, CoreTagItem } from '@features/tag/services/tag'; | ||||
| import { CoreUser } from '@features/user/services/user'; | ||||
| import { IonContent } from '@ionic/angular'; | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreGroup, CoreGroups } from '@services/groups'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { Translate } from '@singletons'; | ||||
| import { Network, Translate, NgZone } from '@singletons'; | ||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| import { Md5 } from 'ts-md5'; | ||||
| import { AddonModWikiPageDBRecord } from '../../services/database/wiki'; | ||||
| import { AddonModWikiModuleHandlerService } from '../../services/handlers/module'; | ||||
| @ -76,6 +78,8 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp | ||||
|     moduleName = 'wiki'; | ||||
|     groupWiki = false; | ||||
| 
 | ||||
|     isOnline = false; | ||||
| 
 | ||||
|     wiki?: AddonModWikiWiki; // The wiki instance.
 | ||||
|     isMainPage = false; // Whether the user is viewing wiki's main page (just entered the wiki).
 | ||||
|     canEdit = false; // Whether user can edit the page.
 | ||||
| @ -104,12 +108,23 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp | ||||
|     protected ignoreManualSyncEvent = false; // Whether manual sync event should be ignored.
 | ||||
|     protected currentUserId?: number; // Current user ID.
 | ||||
|     protected currentPath!: string; | ||||
|     protected onlineSubscription: Subscription; // It will observe the status of the network connection.
 | ||||
| 
 | ||||
|     constructor( | ||||
|         protected content?: IonContent, | ||||
|         @Optional() courseContentsPage?: CoreCourseContentsPage, | ||||
|     ) { | ||||
|         super('AddonModLessonIndexComponent', content, courseContentsPage); | ||||
| 
 | ||||
|         this.isOnline = CoreApp.isOnline(); | ||||
| 
 | ||||
|         // Refresh online status when changes.
 | ||||
|         this.onlineSubscription = Network.onChange().subscribe(() => { | ||||
|             // Execute the callback in the Angular zone, so change detection doesn't stop working.
 | ||||
|             NgZone.run(() => { | ||||
|                 this.isOnline = CoreApp.isOnline(); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -212,7 +227,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async fetchContent(refresh = false, sync = false, showErrors = false): Promise<void> { | ||||
|     protected async fetchContent(refresh?: boolean, sync = false, showErrors = false): Promise<void> { | ||||
|         try { | ||||
|             // Get the wiki instance.
 | ||||
|             this.wiki = await AddonModWiki.getWiki(this.courseId, this.module.id); | ||||
| @ -240,7 +255,6 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp | ||||
|             } | ||||
| 
 | ||||
|             this.description = this.wiki.intro || this.module.description; | ||||
|             this.externalUrl = this.module.url; | ||||
|             this.componentId = this.module.id; | ||||
| 
 | ||||
|             await this.fetchSubwikis(this.wiki.id); | ||||
| @ -278,8 +292,6 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp | ||||
|             } | ||||
| 
 | ||||
|             throw error; | ||||
|         } finally { | ||||
|             this.fillContextMenu(refresh); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -835,6 +847,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp | ||||
| 
 | ||||
|         this.manualSyncObserver?.off(); | ||||
|         this.newPageObserver?.off(); | ||||
|         this.onlineSubscription.unsubscribe(); | ||||
|         if (this.wiki) { | ||||
|             AddonModWiki.wikiPageClosed(this.wiki.id, this.currentPath); | ||||
|         } | ||||
|  | ||||
| @ -1,29 +1,8 @@ | ||||
| <!-- Buttons to add to the header. --> | ||||
| <core-navbar-buttons slot="end"> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" | ||||
|             iconAction="fas-external-link-alt" [showBrowserWarning]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" | ||||
|             (action)="expandDescription()" iconAction="fas-arrow-right"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper" | ||||
|             (action)="gotoBlog()"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" | ||||
|             (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600" | ||||
|             [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" | ||||
|             [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" | ||||
|             [iconAction]="prefetchStatusIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item *ngIf="size" [priority]="200" [content]="'core.clearstoreddata' | translate:{$a: size}" | ||||
|             iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|     </core-context-menu> | ||||
|     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> | ||||
|         <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <!-- Content. --> | ||||
|  | ||||
| @ -216,55 +216,45 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Download feedback contents. | ||||
|      * | ||||
|      * @param refresh If it's refreshing content. | ||||
|      * @param sync If it should try to sync. | ||||
|      * @param showErrors If show errors to the user of hide them. | ||||
|      * @return Promise resolved when done. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async fetchContent(refresh = false, sync = false, showErrors = false): Promise<void> { | ||||
|         try { | ||||
|             this.workshop = await AddonModWorkshop.getWorkshop(this.courseId, this.module.id); | ||||
|     protected async fetchContent(refresh?: boolean, sync = false, showErrors = false): Promise<void> { | ||||
|         this.workshop = await AddonModWorkshop.getWorkshop(this.courseId, this.module.id); | ||||
| 
 | ||||
|             this.description = this.workshop.intro; | ||||
|             this.dataRetrieved.emit(this.workshop); | ||||
|         this.description = this.workshop.intro; | ||||
|         this.dataRetrieved.emit(this.workshop); | ||||
| 
 | ||||
|             if (sync) { | ||||
|                 // Try to synchronize the feedback.
 | ||||
|                 await this.syncActivity(showErrors); | ||||
|             } | ||||
| 
 | ||||
|             // Check if there are answers stored in offline.
 | ||||
|             this.access = await AddonModWorkshop.getWorkshopAccessInformation(this.workshop.id, { cmId: this.module.id }); | ||||
| 
 | ||||
|             if (this.access.canviewallsubmissions) { | ||||
|                 this.groupInfo = await CoreGroups.getActivityGroupInfo(this.workshop.coursemodule); | ||||
|                 this.group = CoreGroups.validateGroupId(this.group, this.groupInfo); | ||||
|             } | ||||
| 
 | ||||
|             this.phases = await AddonModWorkshop.getUserPlanPhases(this.workshop.id, { cmId: this.module.id }); | ||||
| 
 | ||||
|             this.phases[this.workshop.phase].tasks.forEach((task) => { | ||||
|                 if (!task.link && (task.code == 'examples' || task.code == 'prepareexamples')) { | ||||
|                     // Add links to manage examples.
 | ||||
|                     task.link = this.externalUrl!; | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             // Check if there are info stored in offline.
 | ||||
|             this.hasOffline = await AddonModWorkshopOffline.hasWorkshopOfflineData(this.workshop.id); | ||||
|             if (this.hasOffline) { | ||||
|                 this.offlineSubmissions = await AddonModWorkshopOffline.getSubmissions(this.workshop.id); | ||||
|             } else { | ||||
|                 this.offlineSubmissions = []; | ||||
|             } | ||||
| 
 | ||||
|             await this.setPhaseInfo(); | ||||
| 
 | ||||
|         } finally { | ||||
|             this.fillContextMenu(refresh); | ||||
|         if (sync) { | ||||
|             // Try to synchronize the feedback.
 | ||||
|             await this.syncActivity(showErrors); | ||||
|         } | ||||
| 
 | ||||
|         // Check if there are answers stored in offline.
 | ||||
|         this.access = await AddonModWorkshop.getWorkshopAccessInformation(this.workshop.id, { cmId: this.module.id }); | ||||
| 
 | ||||
|         if (this.access.canviewallsubmissions) { | ||||
|             this.groupInfo = await CoreGroups.getActivityGroupInfo(this.workshop.coursemodule); | ||||
|             this.group = CoreGroups.validateGroupId(this.group, this.groupInfo); | ||||
|         } | ||||
| 
 | ||||
|         this.phases = await AddonModWorkshop.getUserPlanPhases(this.workshop.id, { cmId: this.module.id }); | ||||
| 
 | ||||
|         this.phases[this.workshop.phase].tasks.forEach((task) => { | ||||
|             if (!task.link && (task.code == 'examples' || task.code == 'prepareexamples')) { | ||||
|                 // Add links to manage examples.
 | ||||
|                 task.link = this.module.url || ''; | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         // Check if there are info stored in offline.
 | ||||
|         this.hasOffline = await AddonModWorkshopOffline.hasWorkshopOfflineData(this.workshop.id); | ||||
|         if (this.hasOffline) { | ||||
|             this.offlineSubmissions = await AddonModWorkshopOffline.getSubmissions(this.workshop.id); | ||||
|         } else { | ||||
|             this.offlineSubmissions = []; | ||||
|         } | ||||
| 
 | ||||
|         await this.setPhaseInfo(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -394,7 +384,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity | ||||
|                 componentProps: { | ||||
|                     phases: CoreUtils.objectToArray(this.phases), | ||||
|                     workshopPhase: this.workshop!.phase, | ||||
|                     externalUrl: this.externalUrl, | ||||
|                     externalUrl: this.module.url, | ||||
|                     showSubmit: this.showSubmit, | ||||
|                 }, | ||||
|             }); | ||||
|  | ||||
| @ -11,6 +11,7 @@ | ||||
|     "deletedata": "Delete downloaded data", | ||||
|     "deletedatafrom": "Delete all downloaded data from '{{name}}'", | ||||
|     "downloadedcourses": "Downloaded courses", | ||||
|     "downloads": "Downloads", | ||||
|     "errordeletedownloadeddata": "Error deleting downloaded data.", | ||||
|     "managedownloads": "Manage downloads", | ||||
|     "totaldownloads": "Total downloads", | ||||
|  | ||||
| @ -13,6 +13,7 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, Input, Output, OnInit, OnDestroy, EventEmitter, OnChanges, SimpleChange } from '@angular/core'; | ||||
| import { CoreLogger } from '@singletons/logger'; | ||||
| import { CoreContextMenuComponent } from '../context-menu/context-menu'; | ||||
| 
 | ||||
| /** | ||||
| @ -35,7 +36,6 @@ import { CoreContextMenuComponent } from '../context-menu/context-menu'; | ||||
| export class CoreContextMenuItemComponent implements OnInit, OnDestroy, OnChanges { | ||||
| 
 | ||||
|     @Input() content?: string; // Content of the item.
 | ||||
|     @Input() iconDescription?: string; // Name of the icon to be shown on the left side of the item.
 | ||||
|     @Input() iconAction?: string; // Name of the icon to show on the right side of the item. Represents the action to do on click.
 | ||||
|     // If is "spinner" an spinner will be shown.
 | ||||
|     // If is "toggle" a toggle switch will be shown.
 | ||||
| @ -58,6 +58,11 @@ export class CoreContextMenuItemComponent implements OnInit, OnDestroy, OnChange | ||||
|     @Output() onClosed?: EventEmitter<() => void>; // Will emit an event when the popover is closed because the item was clicked.
 | ||||
|     @Output() toggleChange = new EventEmitter<boolean>();// Will emit an event when toggle changes to enable 2-way data binding.
 | ||||
| 
 | ||||
|     /** | ||||
|      * @deprecated since 4.0. | ||||
|      */ | ||||
|     @Input() iconDescription?: string; // Name of the icon to be shown on the left side of the item. Not used anymore.
 | ||||
| 
 | ||||
|     protected hasAction = false; | ||||
|     protected destroyed = false; | ||||
| 
 | ||||
| @ -88,6 +93,11 @@ export class CoreContextMenuItemComponent implements OnInit, OnDestroy, OnChange | ||||
|         if (!this.destroyed) { | ||||
|             this.ctxtMenu.addItem(this); | ||||
|         } | ||||
| 
 | ||||
|         if (this.iconDescription !== undefined) { | ||||
|             CoreLogger.getInstance('CoreContextMenuItemComponent') | ||||
|                 .warn('iconDescription Input is deprecated and should not be used'); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -13,7 +13,7 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, Input, OnInit, OnDestroy, ElementRef, ChangeDetectorRef } from '@angular/core'; | ||||
| import { Subject } from 'rxjs'; | ||||
| import { Subject, Subscription } from 'rxjs'; | ||||
| import { auditTime } from 'rxjs/operators'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| @ -42,11 +42,12 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy { | ||||
|     protected itemsChangedStream: Subject<void>; // Stream to update the hideMenu boolean when items change.
 | ||||
|     protected parentContextMenu?: CoreContextMenuComponent; | ||||
|     protected expanded = false; | ||||
|     protected itemsSubscription: Subscription; | ||||
| 
 | ||||
|     constructor(elementRef: ElementRef, changeDetector: ChangeDetectorRef) { | ||||
|         // Create the stream and subscribe to it. We ignore successive changes during 250ms.
 | ||||
|         this.itemsChangedStream = new Subject<void>(); | ||||
|         this.itemsChangedStream.pipe(auditTime(250)).subscribe(() => { | ||||
|         this.itemsSubscription = this.itemsChangedStream.pipe(auditTime(250)).subscribe(() => { | ||||
|             // Hide the menu if all items are hidden.
 | ||||
|             this.hideMenu = !this.items.some((item) => !item.hidden); | ||||
| 
 | ||||
| @ -63,7 +64,7 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         this.icon = this.icon || 'ellipsis-vertical'; | ||||
| @ -197,10 +198,11 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component destroyed. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.removeMergedItems(); | ||||
|         this.itemsSubscription.unsubscribe(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -6,8 +6,6 @@ | ||||
|         [href]="item.href" (click)="itemClicked($event, item)" [attr.aria-label]="item.ariaAction" [hidden]="item.hidden" | ||||
|         [detail]="(item.href && !item.iconAction) || null" role="menuitem" [button]="(item.href && !item.iconAction)" | ||||
|         [showBrowserWarning]="item.showBrowserWarning"> | ||||
|         <ion-icon *ngIf="item.iconDescription" [name]="item.iconDescription" aria-hidden="true" slot="start"> | ||||
|         </ion-icon> | ||||
|         <ion-label> | ||||
|             <p class="item-heading"> | ||||
|                 <core-format-text [clean]="true" [text]="item.content" [filter]="false"></core-format-text> | ||||
|  | ||||
| @ -1,8 +1,5 @@ | ||||
| <ion-header> | ||||
| <ion-header class="no-title"> | ||||
|     <ion-toolbar> | ||||
|         <ion-title> | ||||
|             <h2>{{ 'core.block.blocks' | translate }}</h2> | ||||
|         </ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|             <ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||
|                 <ion-icon name="fas-times" slot="icon-only" aria-hidden=true></ion-icon> | ||||
| @ -10,7 +7,7 @@ | ||||
|         </ion-buttons> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
| <ion-content [fullscreen]="true"> | ||||
|     <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)"> | ||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|     </ion-refresher> | ||||
|  | ||||
| @ -17,15 +17,11 @@ import { IonContent } from '@ionic/angular'; | ||||
| 
 | ||||
| import { CoreCourseModuleMainResourceComponent } from './main-resource-component'; | ||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||
| import { Network, NgZone } from '@singletons'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreCourse } from '../services/course'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreWSExternalWarning } from '@services/ws'; | ||||
| import { CoreCourseContentsPage } from '../pages/contents/contents'; | ||||
| import { CoreConstants } from '@/core/constants'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| 
 | ||||
| /** | ||||
| @ -40,13 +36,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR | ||||
| 
 | ||||
|     moduleName?: string; // Raw module name to be translated. It will be translated on init.
 | ||||
| 
 | ||||
|     // Data for context menu.
 | ||||
|     syncIcon?: string; // Sync icon.
 | ||||
|     hasOffline?: boolean; // If it has offline data to be synced.
 | ||||
|     isOnline?: boolean; // If the app is online or not.
 | ||||
| 
 | ||||
|     protected syncObserver?: CoreEventObserver; // It will observe the sync auto event.
 | ||||
|     protected onlineSubscription: Subscription; // It will observe the status of the network connection.
 | ||||
|     protected syncEventName?: string; // Auto sync event name.
 | ||||
| 
 | ||||
|     constructor( | ||||
| @ -55,14 +45,6 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR | ||||
|         courseContentsPage?: CoreCourseContentsPage, | ||||
|     ) { | ||||
|         super(loggerName, courseContentsPage); | ||||
| 
 | ||||
|         // Refresh online status when changes.
 | ||||
|         this.onlineSubscription = Network.onChange().subscribe(() => { | ||||
|             // Execute the callback in the Angular zone, so change detection doesn't stop working.
 | ||||
|             NgZone.run(() => { | ||||
|                 this.isOnline = CoreApp.isOnline(); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -72,7 +54,6 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR | ||||
|         await super.ngOnInit(); | ||||
| 
 | ||||
|         this.hasOffline = false; | ||||
|         this.syncIcon = CoreConstants.ICON_LOADING; | ||||
|         this.moduleName = CoreCourse.translateModuleName(this.moduleName || ''); | ||||
| 
 | ||||
|         if (this.syncEventName) { | ||||
| @ -119,20 +100,12 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.refreshIcon = CoreConstants.ICON_LOADING; | ||||
|         this.syncIcon = CoreConstants.ICON_LOADING; | ||||
|         await CoreUtils.ignoreErrors(Promise.all([ | ||||
|             this.invalidateContent(), | ||||
|             this.showCompletion ? CoreCourse.invalidateModule(this.module.id) : undefined, | ||||
|         ])); | ||||
| 
 | ||||
|         try { | ||||
|             await CoreUtils.ignoreErrors(Promise.all([ | ||||
|                 this.invalidateContent(), | ||||
|                 this.showCompletion ? CoreCourse.invalidateModule(this.module.id) : undefined, | ||||
|             ])); | ||||
| 
 | ||||
|             await this.loadContent(true, sync, showErrors); | ||||
|         } finally { | ||||
|             this.refreshIcon = CoreConstants.ICON_REFRESH; | ||||
|             this.syncIcon = CoreConstants.ICON_SYNC; | ||||
|         } | ||||
|         await this.loadContent(true, sync, showErrors); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -143,17 +116,10 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR | ||||
|      * @return Resolved when done. | ||||
|      */ | ||||
|     protected async showLoadingAndFetch(sync: boolean = false, showErrors: boolean = false): Promise<void> { | ||||
|         this.refreshIcon = CoreConstants.ICON_LOADING; | ||||
|         this.syncIcon = CoreConstants.ICON_LOADING; | ||||
|         this.loaded = false; | ||||
|         this.content?.scrollToTop(); | ||||
| 
 | ||||
|         try { | ||||
|             await this.loadContent(false, sync, showErrors); | ||||
|         } finally { | ||||
|             this.refreshIcon = CoreConstants.ICON_REFRESH; | ||||
|             this.syncIcon = CoreConstants.ICON_REFRESH; | ||||
|         } | ||||
|         await this.loadContent(false, sync, showErrors); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -164,8 +130,6 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR | ||||
|      * @return Resolved when done. | ||||
|      */ | ||||
|     protected showLoadingAndRefresh(sync: boolean = false, showErrors: boolean = false): Promise<void> { | ||||
|         this.refreshIcon = CoreConstants.ICON_LOADING; | ||||
|         this.syncIcon = CoreConstants.ICON_LOADING; | ||||
|         this.loaded = false; | ||||
|         this.content?.scrollToTop(); | ||||
| 
 | ||||
| @ -194,8 +158,6 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async loadContent(refresh?: boolean, sync: boolean = false, showErrors: boolean = false): Promise<void> { | ||||
|         this.isOnline = CoreApp.isOnline(); | ||||
| 
 | ||||
|         if (!this.module) { | ||||
|             // This can happen if course format changes from single activity to weekly/topics.
 | ||||
|             return; | ||||
| @ -216,8 +178,6 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR | ||||
|             CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError, true); | ||||
|         } finally { | ||||
|             this.loaded = true; | ||||
|             this.refreshIcon = CoreConstants.ICON_REFRESH; | ||||
|             this.syncIcon = CoreConstants.ICON_REFRESH; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -270,8 +230,6 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         super.ngOnDestroy(); | ||||
| 
 | ||||
|         this.onlineSubscription?.unsubscribe(); | ||||
|         this.syncObserver?.off(); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -13,14 +13,10 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreConstants } from '@/core/constants'; | ||||
| import { AddonBlog } from '@addons/blog/services/blog'; | ||||
| import { AddonBlogMainMenuHandlerService } from '@addons/blog/services/handlers/mainmenu'; | ||||
| import { OnInit, OnDestroy, Input, Output, EventEmitter, Component, Optional, Inject } from '@angular/core'; | ||||
| import { Params } from '@angular/router'; | ||||
| import { CoreAnyError } from '@classes/errors/error'; | ||||
| import { IonRefresher } from '@ionic/angular'; | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| 
 | ||||
| @ -29,6 +25,7 @@ import { CoreUtils } from '@services/utils/utils'; | ||||
| import { Translate } from '@singletons'; | ||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||
| import { CoreLogger } from '@singletons/logger'; | ||||
| import { CoreCourseModuleSummaryComponent, CoreCourseModuleSummaryResult } from '../components/module-summary/module-summary'; | ||||
| import { CoreCourseContentsPage } from '../pages/contents/contents'; | ||||
| import { CoreCourse } from '../services/course'; | ||||
| import { CoreCourseHelper, CoreCourseModuleData } from '../services/course-helper'; | ||||
| @ -58,30 +55,23 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, | ||||
|     loaded = false; // If the component has been loaded.
 | ||||
|     component?: string; // Component name.
 | ||||
|     componentId?: number; // Component ID.
 | ||||
|     blog?: boolean; // If blog is available.
 | ||||
|     hasOffline = false; // Resources don't have any data to sync.
 | ||||
| 
 | ||||
|     // Data for context menu.
 | ||||
|     externalUrl?: string; // External URL to open in browser.
 | ||||
|     description?: string; // Module description.
 | ||||
|     refreshIcon = CoreConstants.ICON_LOADING; // Refresh icon, normally spinner or refresh.
 | ||||
|     prefetchStatusIcon?: string; // Used when calling fillContextMenu.
 | ||||
|     prefetchStatus?: string; // Used when calling fillContextMenu.
 | ||||
|     prefetchText?: string; // Used when calling fillContextMenu.
 | ||||
|     size?: string; // Used when calling fillContextMenu.
 | ||||
|     downloadTimeReadable?: string; // Last download time in a readable format. Used when calling fillContextMenu.
 | ||||
|     isDestroyed = false; // Whether the component is destroyed, used when calling fillContextMenu.
 | ||||
|     contextMenuStatusObserver?: CoreEventObserver; // Observer of package status, used when calling fillContextMenu.
 | ||||
|     contextFileStatusObserver?: CoreEventObserver; // Observer of file status, used when calling fillContextMenu.
 | ||||
| 
 | ||||
|     protected fetchContentDefaultError = 'core.course.errorgetmodule'; // Default error to show when loading contents.
 | ||||
|     protected isCurrentView = false; // Whether the component is in the current view.
 | ||||
|     protected siteId?: string; // Current Site ID.
 | ||||
|     protected statusObserver?: CoreEventObserver; // Observer of package status. Only if setStatusListener is called.
 | ||||
|     protected currentStatus?: string; // The current status of the module. Only if setStatusListener is called.
 | ||||
|     currentStatus?: string; // The current status of the module. Only if setStatusListener is called.
 | ||||
|     downloadTimeReadable?: string; // Last download time in a readable format. Only if setStatusListener is called.
 | ||||
| 
 | ||||
|     protected completionObserver?: CoreEventObserver; | ||||
|     protected logger: CoreLogger; | ||||
|     protected debouncedUpdateModule?: () => void; // Update the module after a certain time.
 | ||||
|     protected showCompletion = false; // Whether to show completion inside the activity.
 | ||||
|     protected displayDescription = true; // Wether to show Module description on module page, and not on summary or the contrary.
 | ||||
|     protected isDestroyed = false; // Whether the component is destroyed.
 | ||||
| 
 | ||||
|     constructor( | ||||
|         @Optional() @Inject('') loggerName: string = 'CoreCourseModuleMainResourceComponent', | ||||
| @ -91,13 +81,12 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         this.siteId = CoreSites.getCurrentSiteId(); | ||||
|         this.description = this.module.description; | ||||
|         this.componentId = this.module.id; | ||||
|         this.externalUrl = this.module.url; | ||||
|         this.courseId = this.courseId || this.module.course; | ||||
|         this.showCompletion = !!CoreSites.getRequiredCurrentSite().isVersionGreaterEqualThan('3.11'); | ||||
| 
 | ||||
| @ -116,20 +105,17 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, | ||||
|                 this.fetchModule(); | ||||
|             }, 10000); | ||||
|         } | ||||
| 
 | ||||
|         this.blog = await AddonBlog.isPluginEnabled(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Refresh the data. | ||||
|      * | ||||
|      * @param refresher Refresher. | ||||
|      * @param done Function to call when done. | ||||
|      * @param showErrors If show errors to the user of hide them. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async doRefresh(refresher?: IonRefresher | null, done?: () => void, showErrors: boolean = false): Promise<void> { | ||||
|         if (!this.loaded || !this.module) { | ||||
|     async doRefresh(refresher?: IonRefresher | null, showErrors = false): Promise<void> { | ||||
|         if (!this.module) { | ||||
|             // Module can be undefined if course format changes from single activity to weekly/topics.
 | ||||
|             return; | ||||
|         } | ||||
| @ -143,7 +129,6 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, | ||||
|         await CoreUtils.ignoreErrors(this.refreshContent(true, showErrors)); | ||||
| 
 | ||||
|         refresher?.complete(); | ||||
|         done && done(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -160,22 +145,16 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.refreshIcon = CoreConstants.ICON_LOADING; | ||||
|         await CoreUtils.ignoreErrors(Promise.all([ | ||||
|             this.invalidateContent(), | ||||
|             this.showCompletion ? CoreCourse.invalidateModule(this.module.id) : undefined, | ||||
|         ])); | ||||
| 
 | ||||
|         try { | ||||
|             await CoreUtils.ignoreErrors(Promise.all([ | ||||
|                 this.invalidateContent(), | ||||
|                 this.showCompletion ? CoreCourse.invalidateModule(this.module.id) : undefined, | ||||
|             ])); | ||||
| 
 | ||||
|             if (this.showCompletion) { | ||||
|                 this.fetchModule(); | ||||
|             } | ||||
| 
 | ||||
|             await this.loadContent(true); | ||||
|         } finally { | ||||
|             this.refreshIcon = CoreConstants.ICON_REFRESH; | ||||
|         if (this.showCompletion) { | ||||
|             this.fetchModule(); | ||||
|         } | ||||
| 
 | ||||
|         await this.loadContent(true); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -221,7 +200,6 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, | ||||
|             CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError, true); | ||||
|         } finally { | ||||
|             this.loaded = true; | ||||
|             this.refreshIcon = CoreConstants.ICON_REFRESH; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -236,66 +214,25 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fill the context menu options | ||||
|      * Updage package last downloaded. | ||||
|      */ | ||||
|     protected fillContextMenu(refresh: boolean = false): void { | ||||
|         // All data obtained, now fill the context menu.
 | ||||
|         CoreCourseHelper.fillContextMenu(this, this.module, this.courseId, refresh, this.component); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the module is prefetched or being prefetched. To make it faster, just use the data calculated by fillContextMenu. | ||||
|      * This means that you need to call fillContextMenu to make this work. | ||||
|      */ | ||||
|     protected isPrefetched(): boolean { | ||||
|         return this.prefetchStatus != CoreConstants.NOT_DOWNLOADABLE && this.prefetchStatus != CoreConstants.NOT_DOWNLOADED; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Expand the description. | ||||
|      */ | ||||
|     expandDescription(): void { | ||||
|         CoreTextUtils.viewText(Translate.instant('core.description'), this.description!, { | ||||
|             component: this.component, | ||||
|             componentId: this.module.id, | ||||
|             filter: true, | ||||
|             contextLevel: 'module', | ||||
|             instanceId: this.module.id, | ||||
|             courseId: this.courseId, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Go to blog posts. | ||||
|      */ | ||||
|     async gotoBlog(): Promise<void> { | ||||
|         const params: Params = { cmId: this.module.id }; | ||||
| 
 | ||||
|         await CoreNavigator.navigateToSitePath(AddonBlogMainMenuHandlerService.PAGE_NAME, { params }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prefetch the module. | ||||
|      * | ||||
|      * @param done Function to call when done. | ||||
|      */ | ||||
|     prefetch(done?: () => void): void { | ||||
|         CoreCourseHelper.contextMenuPrefetch(this, this.module, this.courseId, done); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Confirm and remove downloaded files. | ||||
|      * | ||||
|      * @param done Function to call when done. | ||||
|      */ | ||||
|     removeFiles(done?: () => void): void { | ||||
|         if (this.prefetchStatus == CoreConstants.DOWNLOADING) { | ||||
|             CoreDomUtils.showAlertTranslated(undefined, 'core.course.cannotdeletewhiledownloading'); | ||||
| 
 | ||||
|     protected async getPackageLastDownloaded(): Promise<void> { | ||||
|         if (!this.module) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         CoreCourseHelper.confirmAndRemoveFiles(this.module, this.courseId, done); | ||||
|         const lastDownloaded = | ||||
|                 await CoreCourseHelper.getModulePackageLastDownloaded(this.module, this.component); | ||||
| 
 | ||||
|         this.downloadTimeReadable = CoreTextUtils.ucFirst(lastDownloaded.downloadTimeReadable); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the module is prefetched or being prefetched. | ||||
|      * To make it faster, just use the data calculated by setStatusListener. | ||||
|      */ | ||||
|     protected isPrefetched(): boolean { | ||||
|         return this.currentStatus != CoreConstants.NOT_DOWNLOADABLE && this.currentStatus != CoreConstants.NOT_DOWNLOADED; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -355,6 +292,8 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, | ||||
|                 const previousStatus = this.currentStatus; | ||||
|                 this.currentStatus = data.status; | ||||
| 
 | ||||
|                 this.getPackageLastDownloaded(); | ||||
| 
 | ||||
|                 this.showStatus(this.currentStatus, previousStatus); | ||||
|             }, this.siteId); | ||||
|         } else if (!refresh) { | ||||
| @ -369,6 +308,9 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, | ||||
|         const status = await CoreCourseModulePrefetchDelegate.getModuleStatus(this.module, this.courseId, undefined, refresh); | ||||
| 
 | ||||
|         this.currentStatus = status; | ||||
| 
 | ||||
|         this.getPackageLastDownloaded(); | ||||
| 
 | ||||
|         this.showStatus(status); | ||||
|     } | ||||
| 
 | ||||
| @ -449,13 +391,47 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, | ||||
|         this.module = module; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Opens a module summary page. | ||||
|      */ | ||||
|     async openModuleSummary(): Promise<void> { | ||||
|         if (!this.module) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const data = await CoreDomUtils.openSideModal<CoreCourseModuleSummaryResult>({ | ||||
|             component: CoreCourseModuleSummaryComponent, | ||||
|             componentProps: { | ||||
|                 moduleId: this.module.id, | ||||
|                 module: this.module, | ||||
|                 description: this.description, | ||||
|                 component: this.component, | ||||
|                 courseId: this.courseId, | ||||
|                 hasOffline: this.hasOffline, | ||||
|                 displayOptions: { | ||||
|                     // Show description on summary if not shown on the page.
 | ||||
|                     displayDescription: !this.displayDescription, | ||||
|                 }, | ||||
|             }, | ||||
|         }); | ||||
| 
 | ||||
|         if (data) { | ||||
|             if (this.loaded && (data.action == 'refresh' || data.action == 'sync')) { | ||||
|                 this.loaded = false; | ||||
|                 try { | ||||
|                     await this.doRefresh(undefined, data.action == 'sync'); | ||||
|                 } finally { | ||||
|                     this.loaded = true; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.isDestroyed = true; | ||||
|         this.contextMenuStatusObserver?.off(); | ||||
|         this.contextFileStatusObserver?.off(); | ||||
|         this.statusObserver?.off(); | ||||
|         this.completionObserver?.off(); | ||||
|     } | ||||
|  | ||||
| @ -16,7 +16,7 @@ import { NgModule } from '@angular/core'; | ||||
| 
 | ||||
| import { CoreSharedModule } from '@/core/shared.module'; | ||||
| import { CoreBlockComponentsModule } from '@features/block/components/components.module'; | ||||
| import { CoreCourseFormatComponent } from './format/format'; | ||||
| import { CoreCourseFormatComponent } from './course-format/course-format'; | ||||
| import { CoreCourseModuleComponent } from './module/module'; | ||||
| import { CoreCourseModuleCompletionComponent } from './module-completion/module-completion'; | ||||
| import { CoreCourseModuleDescriptionComponent } from './module-description/module-description'; | ||||
| @ -27,6 +27,7 @@ import { CoreCourseModuleCompletionLegacyComponent } from './module-completion-l | ||||
| import { CoreCourseModuleInfoComponent } from './module-info/module-info'; | ||||
| import { CoreCourseModuleManualCompletionComponent } from './module-manual-completion/module-manual-completion'; | ||||
| import { CoreCourseModuleNavigationComponent } from './module-navigation/module-navigation'; | ||||
| import { CoreCourseModuleSummaryComponent } from './module-summary/module-summary'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
| @ -41,6 +42,7 @@ import { CoreCourseModuleNavigationComponent } from './module-navigation/module- | ||||
|         CoreCourseTagAreaComponent, | ||||
|         CoreCourseUnsupportedModuleComponent, | ||||
|         CoreCourseModuleNavigationComponent, | ||||
|         CoreCourseModuleSummaryComponent, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreBlockComponentsModule, | ||||
| @ -58,6 +60,7 @@ import { CoreCourseModuleNavigationComponent } from './module-navigation/module- | ||||
|         CoreCourseTagAreaComponent, | ||||
|         CoreCourseUnsupportedModuleComponent, | ||||
|         CoreCourseModuleNavigationComponent, | ||||
|         CoreCourseModuleSummaryComponent, | ||||
|     ], | ||||
| }) | ||||
| export class CoreCourseComponentsModule {} | ||||
|  | ||||
| @ -55,8 +55,8 @@ import { CoreCourseModuleDelegate } from '@features/course/services/module-deleg | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'core-course-format', | ||||
|     templateUrl: 'core-course-format.html', | ||||
|     styleUrls: ['format.scss'], | ||||
|     templateUrl: 'course-format.html', | ||||
|     styleUrls: ['course-format.scss'], | ||||
| }) | ||||
| export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
| 
 | ||||
| @ -110,7 +110,7 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy { | ||||
|             return; | ||||
|         } | ||||
|         // Set a minimum height value.
 | ||||
|         this.initialHeight = this.initialHeight || 56; | ||||
|         this.initialHeight = this.initialHeight || 48; | ||||
|         this.previousHeight = this.initialHeight; | ||||
| 
 | ||||
|         this.content = this.element.closest('ion-content'); | ||||
|  | ||||
| @ -0,0 +1,213 @@ | ||||
| <ion-header class="no-title"> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="end"> | ||||
|             <ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||
|                 <ion-icon slot="icon-only" name="fas-times" aria-hidden="true"></ion-icon> | ||||
|             </ion-button> | ||||
|         </ion-buttons> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
| <ion-content [fullscreen]="true"> | ||||
|     <!-- Content. --> | ||||
|     <core-loading [hideUntil]="loaded"> | ||||
|         <!-- Activity info. --> | ||||
|         <ion-item class="ion-text-wrap" *ngIf="module" lines="full"> | ||||
|             <ion-label> | ||||
|                 <p *ngIf="moduleNameTranslated" class="core-modulename"> | ||||
|                     <core-mod-icon slot="start" [modicon]="modicon" [modname]="module.modname" [componentId]="module.instance"> | ||||
|                     </core-mod-icon> | ||||
|                     {{moduleNameTranslated}} | ||||
|                 </p> | ||||
|                 <h1> | ||||
|                     <core-format-text [text]="module.name" contextLevel="module" [component]="component" [componentId]="componentId" | ||||
|                         [contextInstanceId]="module.id" [courseId]="courseId"> | ||||
|                     </core-format-text> | ||||
|                 </h1> | ||||
|             </ion-label> | ||||
|             <ion-button fill="clear" *ngIf="displayOptions.displayOpenInBrowser" [href]="externalUrl" core-link [showBrowserWarning]="false" | ||||
|                 color="dark" [attr.aria-label]="'core.openinbrowser' | translate" slot="end"> | ||||
|                 <ion-icon name="fas-external-link-alt" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|             </ion-button> | ||||
|         </ion-item> | ||||
| 
 | ||||
|         <ion-item class="ion-text-wrap" *ngIf="course" (click)="openCourse()" button [detail]="true" lines="full"> | ||||
|             <ion-label> | ||||
|                 <p class="item-heading"> | ||||
|                     <ion-icon name="fas-graduation-cap" aria-hidden="true"></ion-icon> | ||||
|                     {{ 'core.course' | translate}} | ||||
|                 </p> | ||||
|                 <p> | ||||
|                     <core-format-text [text]="course.displayname || course.fullname" contextLevel="course" [contextInstanceId]="courseId"> | ||||
|                     </core-format-text> | ||||
|                 </p> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
| 
 | ||||
|         <ion-item class="ion-text-wrap" *ngIf="module && description && displayOptions.displayDescription" lines="full"> | ||||
|             <ion-label> | ||||
|                 <p class="item-heading"> | ||||
|                     {{ 'core.description' | translate}} | ||||
|                 </p> | ||||
|                 <core-format-text [text]="description" [component]="component" [componentId]="componentId" contextLevel="module" | ||||
|                     [contextInstanceId]="module.id" [courseId]="courseId" [maxHeight]="120"> | ||||
|                 </core-format-text> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
| 
 | ||||
|         <ion-card *ngIf="(canPrefetch && displayOptions.displayPrefetch) || (sizeReadable && displayOptions.displaySize)"> | ||||
|             <ion-item lines="full" class="ion-text-wrap"> | ||||
|                 <ion-label> | ||||
|                     <h2> | ||||
|                         <ion-icon name="cloud-done" aria-hidden="true"></ion-icon> | ||||
|                         {{ 'addon.storagemanager.downloads' | translate }} | ||||
|                     </h2> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ion-item *ngIf="sizeReadable && displayOptions.displaySize" class="ion-text-wrap"> | ||||
|                 <ion-label> | ||||
|                     <p class="item-heading ion-text-wrap">{{ 'addon.storagemanager.totalspaceusage' | translate }}</p> | ||||
|                     <ion-badge color="light">{{ sizeReadable | coreBytesToSize }}</ion-badge> | ||||
|                 </ion-label> | ||||
|                 <ion-button *ngIf="!removeFilesLoading" [disabled]="prefetchLoading" (click)="removeFiles()" color="danger" fill="clear" | ||||
|                     [attr.aria-label]="'core.clearstoreddata' | translate:{$a: sizeReadable}" slot="end"> | ||||
|                     <ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|                 </ion-button> | ||||
|                 <ion-spinner *ngIf="removeFilesLoading" slot="end" aria-hidden="true"></ion-spinner> | ||||
|             </ion-item> | ||||
|             <ion-item *ngIf="downloadTimeReadable" class="ion-text-wrap"> | ||||
|                 <ion-label> | ||||
|                     <p class="ion-text-wrap">{{ 'core.lastdownloaded' | translate }} {{ downloadTimeReadable }}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ion-button fill="outline" expand="block" *ngIf="canPrefetch && displayOptions.displayPrefetch" class="ion-text-wrap" | ||||
|                 (click)="prefetch()" color="primary" [disabled]="prefetchDisabled"> | ||||
|                 <ion-icon *ngIf="!prefetchLoading" name="cloud-done" slot="start" aria-hidden="true"></ion-icon> | ||||
|                 <ion-spinner *ngIf="prefetchLoading" slot="start" aria-hidden="true"></ion-spinner> | ||||
|                 <ion-label> | ||||
|                     {{ 'core.download' | translate }} | ||||
|                 </ion-label> | ||||
|             </ion-button> | ||||
|         </ion-card> | ||||
| 
 | ||||
|         <ion-card *ngIf="displayOptions.displayGrades && grades?.length > 0"> | ||||
|             <ion-list> | ||||
|                 <ion-item lines="full" class="ion-text-wrap"> | ||||
|                     <ion-label> | ||||
|                         <h2> | ||||
|                             <ion-icon name="fas-chart-bar" slot="end" aria-hidden="true"></ion-icon>{{ 'core.grades.gradebook' | translate | ||||
|                             }} | ||||
|                         </h2> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ng-container *ngFor="let grade of grades"> | ||||
|                     <ion-item button *ngIf="grade.gradeitem" class="ion-text-wrap divider" (click)="toggleGrade(grade)" | ||||
|                         [attr.aria-label]="(grade.expanded ? 'core.collapse' : 'core.expand') | translate" | ||||
|                         [attr.aria-expanded]="grade.expanded" [attr.aria-controls]="'grade-'+grade.id" role="heading" detail="false"> | ||||
|                         <ion-icon name="fas-chevron-right" flip-rtl slot="start" aria-hidden="true" class="expandable-status-icon" | ||||
|                             [class.expandable-status-icon-expanded]="grade.expanded"> | ||||
|                         </ion-icon> | ||||
|                         <ion-label> | ||||
|                             <p class="item-heading" *ngIf="!grade.itemmodule"> | ||||
|                                 <core-format-text [text]="grade.gradeitem" contextLevel="course" [contextInstanceId]="courseId"> | ||||
|                                 </core-format-text> | ||||
|                             </p> | ||||
|                             <p class="item-heading" *ngIf="grade.itemmodule"> | ||||
|                                 {{ 'core.grades.grade' | translate}} | ||||
|                             </p> | ||||
|                             <p *ngIf="grade.grade && grade.grade != '-'" [innerHTML]="grade.grade"></p> | ||||
|                             <ion-badge *ngIf="!grade.grade || grade.grade == '-'" color="light">Not graded</ion-badge> | ||||
|                         </ion-label> | ||||
|                         <ion-icon *ngIf="grade.icon" name="{{grade.icon}}" slot="end" [attr.aria-label]="grade.iconAlt"> | ||||
|                         </ion-icon> | ||||
|                         <img *ngIf="grade.image && !grade.itemmodule" [src]="grade.image" slot="end" [alt]="grade.iconAlt" /> | ||||
|                         <ion-icon *ngIf="grade.image && grade.itemmodule" name="fas-chart-bar" slot="end" [attr.aria-label]="grade.iconAlt"> | ||||
|                         </ion-icon> | ||||
|                     </ion-item> | ||||
|                     <div *ngIf="grade.expanded" [id]="'grade-'+grade.id"> | ||||
|                         <ion-item class="ion-text-wrap" *ngIf="grade.weight?.length > 0 && grade.weight != '-'"> | ||||
|                             <ion-label> | ||||
|                                 <p class="item-heading">{{ 'core.grades.weight' | translate}}</p> | ||||
|                                 <p [innerHTML]="grade.weight"></p> | ||||
|                             </ion-label> | ||||
|                         </ion-item> | ||||
|                         <ion-item class="ion-text-wrap" *ngIf="grade.range?.length > 0 && grade.range != '-'"> | ||||
|                             <ion-label> | ||||
|                                 <p class="item-heading">{{ 'core.grades.range' | translate}}</p> | ||||
|                                 <p [innerHTML]="grade.range"></p> | ||||
|                             </ion-label> | ||||
|                         </ion-item> | ||||
| 
 | ||||
|                         <ion-item class="ion-text-wrap" *ngIf="grade.percentage?.length > 0 && grade.percentage != '-'"> | ||||
|                             <ion-label> | ||||
|                                 <p class="item-heading">{{ 'core.grades.percentage' | translate}}</p> | ||||
|                                 <p [innerHTML]="grade.percentage"></p> | ||||
|                             </ion-label> | ||||
|                         </ion-item> | ||||
| 
 | ||||
|                         <ion-item class="ion-text-wrap" *ngIf="grade.lettergrade?.length > 0 && grade.lettergrade != '-'"> | ||||
|                             <ion-label> | ||||
|                                 <p class="item-heading">{{ 'core.grades.lettergrade' | translate}}</p> | ||||
|                                 <p [innerHTML]="grade.lettergrade"></p> | ||||
|                             </ion-label> | ||||
|                         </ion-item> | ||||
| 
 | ||||
|                         <ion-item class="ion-text-wrap" *ngIf="grade.rank?.length > 0 && grade.rank != '-'"> | ||||
|                             <ion-label> | ||||
|                                 <p class="item-heading">{{ 'core.grades.rank' | translate}}</p> | ||||
|                                 <p [innerHTML]="grade.rank"></p> | ||||
|                             </ion-label> | ||||
|                         </ion-item> | ||||
| 
 | ||||
|                         <ion-item class="ion-text-wrap" *ngIf="grade.average?.length > 0 && grade.average != '-'"> | ||||
|                             <ion-label> | ||||
|                                 <p class="item-heading">{{ 'core.grades.average' | translate}}</p> | ||||
|                                 <p [innerHTML]="grade.average"></p> | ||||
|                             </ion-label> | ||||
|                         </ion-item> | ||||
| 
 | ||||
|                         <ion-item class="ion-text-wrap" *ngIf="grade.feedback?.length > 0 && grade.feedback != '-'"> | ||||
|                             <ion-label> | ||||
|                                 <p class="item-heading">{{ 'core.grades.feedback' | translate}}</p> | ||||
|                                 <p> | ||||
|                                     <core-format-text [maxHeight]="120" [text]="grade.feedback" contextLevel="course" | ||||
|                                         [contextInstanceId]="courseId"> | ||||
|                                     </core-format-text> | ||||
|                                 </p> | ||||
|                             </ion-label> | ||||
|                         </ion-item> | ||||
| 
 | ||||
|                         <ion-item class="ion-text-wrap" | ||||
|                             *ngIf="grade.contributiontocoursetotal?.length > 0 && grade.contributiontocoursetotal != '-'"> | ||||
|                             <ion-label> | ||||
|                                 <p class="item-heading">{{ 'core.grades.contributiontocoursetotal' | translate}}</p> | ||||
|                                 <p [innerHTML]="grade.contributiontocoursetotal"></p> | ||||
|                             </ion-label> | ||||
|                         </ion-item> | ||||
|                     </div> | ||||
|                 </ng-container> | ||||
|             </ion-list> | ||||
|         </ion-card> | ||||
| 
 | ||||
|         <ion-item button *ngIf="blog && displayOptions.displayBlog" (click)="gotoBlog()" [detail]="true"> | ||||
|             <ion-icon name="far-newspaper" slot="start" aria-hidden="true"></ion-icon> | ||||
|             <ion-label> | ||||
|                 {{ 'addon.blog.blog' | translate }} | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
| <ion-footer *ngIf="loaded && isOnline && displayOptions.displayRefresh"> | ||||
|     <ion-button class="ion-margin" *ngIf="!hasOffline" (click)="refresh()" expand="block"> | ||||
|         <ion-icon name="fas-redo-alt" slot="start" aria-hidden="true"></ion-icon> | ||||
|         <ion-label> | ||||
|             {{ 'core.refresh' | translate }} | ||||
|         </ion-label> | ||||
|     </ion-button> | ||||
| 
 | ||||
|     <ion-button class="ion-margin" *ngIf="hasOffline" (click)="sync()" expand="block"> | ||||
|         <ion-icon name="fas-sync-alt" slot="start" aria-hidden="true"></ion-icon> | ||||
|         <ion-label> | ||||
|             {{ 'core.settings.synchronizenow' | translate }} | ||||
|         </ion-label> | ||||
|     </ion-button> | ||||
| </ion-footer> | ||||
| @ -0,0 +1,24 @@ | ||||
| @import "~theme/globals"; | ||||
| 
 | ||||
| :host ::ng-deep .collapsible-title ion-label { | ||||
|     margin-top: 12px; | ||||
| } | ||||
| 
 | ||||
| h1 { | ||||
|     font-size: 20px; | ||||
| } | ||||
| 
 | ||||
| .core-modulename { | ||||
|     text-transform: uppercase; | ||||
|     core-mod-icon { | ||||
|         padding: 3px; | ||||
|         --size: 10px; | ||||
|         margin: 0; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| ion-item ion-label ion-icon { | ||||
|     @include margin-horizontal(0, 4px); | ||||
|     vertical-align: text-top; | ||||
| } | ||||
| @ -0,0 +1,385 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreConstants } from '@/core/constants'; | ||||
| import { AddonBlog } from '@addons/blog/services/blog'; | ||||
| import { AddonBlogMainMenuHandlerService } from '@addons/blog/services/handlers/mainmenu'; | ||||
| import { Component, Input, OnDestroy, OnInit } from '@angular/core'; | ||||
| import { Params } from '@angular/router'; | ||||
| import { CoreCourse } from '@features/course/services/course'; | ||||
| import { CoreCourseHelper, CoreCourseModuleData } from '@features/course/services/course-helper'; | ||||
| import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; | ||||
| import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; | ||||
| import { CoreCourses, CoreEnrolledCourseData } from '@features/courses/services/courses'; | ||||
| import { CoreGradesFormattedRow, CoreGradesFormattedTableRow, CoreGradesHelper } from '@features/grades/services/grades-helper'; | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreFilepool } from '@services/filepool'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { ModalController, Network, NgZone } from '@singletons'; | ||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| 
 | ||||
| /** | ||||
|  * Component to display a module summary modal. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'core-course-module-summary', | ||||
|     templateUrl: 'module-summary.html', | ||||
|     styleUrls: ['module-summary.scss'], | ||||
| }) | ||||
| export class CoreCourseModuleSummaryComponent implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     @Input() module?: CoreCourseModuleData; // The module of the component.
 | ||||
|     @Input() courseId = 0; // Course ID the component belongs to.
 | ||||
|     @Input() moduleId = 0; // Module ID the component belongs to.
 | ||||
|     @Input() component = ''; // Component name.
 | ||||
|     @Input() description = ''; // Module description.
 | ||||
|     @Input() hasOffline = false; // If it has offline data to be synced.
 | ||||
|     @Input() displayOptions: CoreCourseModuleSummaryDisplayOptions = {}; | ||||
| 
 | ||||
|     loaded = false; // If the component has been loaded.
 | ||||
|     componentId?: number; // Component ID.
 | ||||
| 
 | ||||
|     // Data for context menu.
 | ||||
|     externalUrl?: string; // External URL to open in browser.
 | ||||
| 
 | ||||
|     removeFilesLoading = false; | ||||
|     prefetchLoading = false; | ||||
|     canPrefetch = false;; | ||||
|     prefetchDisabled = false; | ||||
|     sizeReadable = ''; | ||||
|     downloadTimeReadable = ''; // Last download time in a readable format.
 | ||||
|     grades?: CoreGradesFormattedRow[]; | ||||
|     blog = false; // If blog is available.
 | ||||
|     isOnline = false; // If the app is online or not.
 | ||||
|     course?: CoreEnrolledCourseData; | ||||
|     modicon = ''; | ||||
|     moduleNameTranslated = ''; | ||||
| 
 | ||||
|     protected onlineSubscription: Subscription; // It will observe the status of the network connection.
 | ||||
|     protected packageStatusObserver?: CoreEventObserver; // Observer of package status.
 | ||||
|     protected fileStatusObserver?: CoreEventObserver; // Observer of file status.
 | ||||
|     protected siteId: string; | ||||
|     protected isDestroyed = false; | ||||
| 
 | ||||
|     constructor() { | ||||
|         this.siteId = CoreSites.getCurrentSiteId(); | ||||
|         this.isOnline = CoreApp.isOnline(); | ||||
| 
 | ||||
|         // Refresh online status when changes.
 | ||||
|         this.onlineSubscription = Network.onChange().subscribe(() => { | ||||
|             // Execute the callback in the Angular zone, so change detection doesn't stop working.
 | ||||
|             NgZone.run(() => { | ||||
|                 this.isOnline = CoreApp.isOnline(); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         if (!this.module) { | ||||
|             this.closeModal(); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.displayOptions = Object.assign({ | ||||
|             displayOpenInBrowser: true, | ||||
|             displayDescription: true, | ||||
|             displayRefresh: true, | ||||
|             displayPrefetch: true, | ||||
|             displaySize: true, | ||||
|             displayBlog: true, | ||||
|             displayGrades: true, | ||||
|         }, this.displayOptions); | ||||
| 
 | ||||
|         this.displayOptions.displayGrades = this.displayOptions.displayGrades && | ||||
|             CoreCourseModuleDelegate.supportsFeature(this.module.modname, CoreConstants.FEATURE_GRADE_HAS_GRADE, true); | ||||
| 
 | ||||
|         this.displayOptions.displayDescription = this.displayOptions.displayDescription && | ||||
|             CoreCourseModuleDelegate.supportsFeature(this.module.modname, CoreConstants.FEATURE_SHOW_DESCRIPTION, true); | ||||
| 
 | ||||
|         this.fetchContent(); | ||||
| 
 | ||||
|         if (this.component) { | ||||
|             this.packageStatusObserver = CoreEvents.on( | ||||
|                 CoreEvents.PACKAGE_STATUS_CHANGED, | ||||
|                 (data) => { | ||||
|                     if (data.componentId == module.id && data.component == this.component) { | ||||
|                         this.getPackageStatus(); | ||||
|                     } | ||||
|                 }, | ||||
|                 this.siteId, | ||||
|             ); | ||||
| 
 | ||||
|             // Debounce the update size function to prevent too many calls when downloading or deleting a whole activity.
 | ||||
|             const debouncedUpdateSize = CoreUtils.debounce(async () => { | ||||
|                 if (!this.module) { | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 const moduleSize = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(this.module, this.courseId); | ||||
| 
 | ||||
|                 this.sizeReadable = moduleSize > 0 ? CoreTextUtils.bytesToSize(moduleSize, 2) : ''; | ||||
|             }, 1000); | ||||
| 
 | ||||
|             this.fileStatusObserver = CoreEvents.on( | ||||
|                 CoreEvents.COMPONENT_FILE_ACTION, | ||||
|                 (data) => { | ||||
|                     if (data.component != this.component || data.componentId != module.id) { | ||||
|                         // The event doesn't belong to this component, ignore.
 | ||||
|                         return; | ||||
|                     } | ||||
| 
 | ||||
|                     if (!CoreFilepool.isFileEventDownloadedOrDeleted(data)) { | ||||
|                         return; | ||||
|                     } | ||||
| 
 | ||||
|                     // Update the module size.
 | ||||
|                     debouncedUpdateSize(); | ||||
|                 }, | ||||
|                 this.siteId, | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch content to populate the page. | ||||
|      */ | ||||
|     protected async fetchContent(): Promise<void> { | ||||
|         if (!this.module) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.componentId = this.module.id; | ||||
|         this.externalUrl = this.module.url; | ||||
|         this.courseId = this.courseId || this.module.course; | ||||
| 
 | ||||
|         this.modicon = await CoreCourseModuleDelegate.getModuleIconSrc(this.module.modname, this.module.modicon, this.module); | ||||
|         this.moduleNameTranslated = CoreCourse.translateModuleName(this.module.modname || ''); | ||||
| 
 | ||||
|         this.blog = await AddonBlog.isPluginEnabled(); | ||||
| 
 | ||||
|         await Promise.all([ | ||||
|             this.getPackageStatus(), | ||||
|             this.fetchGrades(), | ||||
|             this.fetchCourse(), | ||||
|         ]); | ||||
| 
 | ||||
|         this.loaded = true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Updage package status. | ||||
|      * | ||||
|      * @param refresh If prefetch info has to be refreshed. | ||||
|      */ | ||||
|     protected async getPackageStatus(refresh = false): Promise<void> { | ||||
|         if (!this.module) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const moduleInfo = | ||||
|             await CoreCourseHelper.getModulePrefetchInfo(this.module, this.courseId, refresh, this.component); | ||||
| 
 | ||||
|         this.canPrefetch = moduleInfo.status != CoreConstants.NOT_DOWNLOADABLE; | ||||
|         this.downloadTimeReadable = ''; | ||||
| 
 | ||||
|         if (this.canPrefetch) { | ||||
|             if (moduleInfo.downloadTime && moduleInfo.downloadTime > 0) { | ||||
|                 this.downloadTimeReadable = CoreTextUtils.ucFirst(moduleInfo.downloadTimeReadable); | ||||
|             } | ||||
|             this.prefetchLoading = moduleInfo.status == CoreConstants.DOWNLOADING; | ||||
|             this.prefetchDisabled = moduleInfo.status == CoreConstants.DOWNLOADED; | ||||
|         } | ||||
| 
 | ||||
|         this.sizeReadable = moduleInfo.size && moduleInfo.size > 0 | ||||
|             ? moduleInfo.sizeReadable | ||||
|             : ''; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Go to blog posts. | ||||
|      */ | ||||
|     async gotoBlog(): Promise<void> { | ||||
|         const params: Params = { cmId: this.moduleId }; | ||||
| 
 | ||||
|         await CoreNavigator.navigateToSitePath(AddonBlogMainMenuHandlerService.PAGE_NAME, { params }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch grade module info. | ||||
|      */ | ||||
|     protected async fetchGrades(): Promise<void> { | ||||
|         if (!this.displayOptions.displayGrades) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.grades = await CoreGradesHelper.getModuleGrades(this.courseId, this.moduleId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Toggle grades expand. | ||||
|      * | ||||
|      * @param grade Row to expand. | ||||
|      */ | ||||
|     toggleGrade(grade: CoreGradesFormattedTableRow): void { | ||||
|         grade.expanded = !grade.expanded; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch course. | ||||
|      */ | ||||
|     protected async fetchCourse(): Promise<void> { | ||||
|         this.course = await CoreCourses.getUserCourse(this.courseId, true); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Open course. | ||||
|      */ | ||||
|     openCourse(): void { | ||||
|         if (!this.course) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         CoreCourse.openCourse( | ||||
|             this.course, | ||||
|             { | ||||
|                 replace: true, | ||||
|                 animationDirection: 'back', | ||||
|             }, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prefetch the module. | ||||
|      */ | ||||
|     async prefetch(): Promise<void> { | ||||
|         if (!this.module) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.prefetchLoading = true; // Show spinner since this operation might take a while.
 | ||||
| 
 | ||||
|         try { | ||||
|             // We need to call getDownloadSize, the package might have been updated.
 | ||||
|             const size = await CoreCourseModulePrefetchDelegate.getModuleDownloadSize(this.module, this.courseId, true); | ||||
| 
 | ||||
|             await CoreDomUtils.confirmDownloadSize(size); | ||||
| 
 | ||||
|             await CoreCourseModulePrefetchDelegate.prefetchModule(this.module, this.courseId, true); | ||||
| 
 | ||||
|             await this.getPackageStatus(true); | ||||
|         } catch (error) { | ||||
|             this.prefetchLoading = false; | ||||
| 
 | ||||
|             if (!this.isDestroyed) { | ||||
|                 CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Confirm and remove downloaded files. | ||||
|      */ | ||||
|     async removeFiles(): Promise<void> { | ||||
|         if (!this.module) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (this.prefetchLoading) { | ||||
|             CoreDomUtils.showAlertTranslated(undefined, 'core.course.cannotdeletewhiledownloading'); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             await CoreDomUtils.showDeleteConfirm('addon.storagemanager.confirmdeletedatafrom', { name: this.module.name }); | ||||
| 
 | ||||
|             this.removeFilesLoading = true; | ||||
| 
 | ||||
|             await CoreCourseHelper.removeModuleStoredData(this.module, this.courseId); | ||||
| 
 | ||||
|         } catch (error) { | ||||
|             if (!this.isDestroyed &&error) { | ||||
|                 CoreDomUtils.showErrorModal(error); | ||||
|             } | ||||
|         } finally { | ||||
|             this.removeFilesLoading = false; | ||||
|         } | ||||
| 
 | ||||
|         await this.getPackageStatus(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Refresh the data. | ||||
|      */ | ||||
|     async refresh(): Promise<void> { | ||||
|         if (!this.module) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         ModalController.dismiss({ action: 'refresh' }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sync the data. | ||||
|      */ | ||||
|     async sync(): Promise<void> { | ||||
|         if (!this.module) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         ModalController.dismiss({ action: 'sync' }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Close the modal. | ||||
|      */ | ||||
|     closeModal(): void { | ||||
|         ModalController.dismiss(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.isDestroyed = true; | ||||
|         this.packageStatusObserver?.off(); | ||||
|         this.fileStatusObserver?.off(); | ||||
|         this.onlineSubscription.unsubscribe(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export type CoreCourseModuleSummaryResult = { | ||||
|     action: 'sync'|'refresh'; | ||||
| }; | ||||
| 
 | ||||
| export type CoreCourseModuleSummaryDisplayOptions = { | ||||
|     displayOpenInBrowser?: boolean; | ||||
|     displayDescription?: boolean; | ||||
|     displayRefresh?: boolean; | ||||
|     displayPrefetch?: boolean; | ||||
|     displaySize?: boolean; | ||||
|     displayBlog?: boolean; | ||||
|     displayGrades?: boolean; | ||||
| }; | ||||
| @ -31,7 +31,7 @@ import { CoreCourseFormatDelegate } from '@features/course/services/format-deleg | ||||
| import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; | ||||
| import { CoreCourseOptionsMenuHandlerToDisplay } from '@features/course/services/course-options-delegate'; | ||||
| import { CoreCourseSync, CoreCourseSyncProvider } from '@features/course/services/sync'; | ||||
| import { CoreCourseFormatComponent } from '../../components/format/format'; | ||||
| import { CoreCourseFormatComponent } from '../../components/course-format/course-format'; | ||||
| import { | ||||
|     CoreEvents, | ||||
|     CoreEventObserver, | ||||
|  | ||||
| @ -11,11 +11,10 @@ | ||||
|         </ion-title> | ||||
| 
 | ||||
|         <ion-buttons slot="end"> | ||||
|             <core-context-menu> | ||||
|                 <core-context-menu-item [priority]="900" *ngIf="module.url" [href]="module!.url" | ||||
|                     [content]="'core.openinbrowser' | translate" iconAction="fas-external-link-alt"> | ||||
|                 </core-context-menu-item> | ||||
|             </core-context-menu> | ||||
|             <ion-button fill="clear" *ngIf="module.url" [href]="module.url" core-link [showBrowserWarning]="false" color="dark" | ||||
|                 [attr.aria-label]="'core.openinbrowser' | translate"> | ||||
|                 <ion-icon name="fas-external-link-alt" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|             </ion-button> | ||||
|         </ion-buttons> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
|  | ||||
| @ -64,7 +64,6 @@ import { CoreFile } from '@services/file'; | ||||
| import { CoreUrlUtils } from '@services/utils/url'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreTimeUtils } from '@services/utils/time'; | ||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||
| import { CoreFilterHelper } from '@features/filter/services/filter-helper'; | ||||
| import { CoreNetworkError } from '@classes/errors/network-error'; | ||||
| import { CoreSiteHome } from '@features/sitehome/services/sitehome'; | ||||
| @ -75,36 +74,19 @@ import { CoreStatusWithWarningsWSResponse } from '@services/ws'; | ||||
| /** | ||||
|  * Prefetch info of a module. | ||||
|  */ | ||||
| export type CoreCourseModulePrefetchInfo = { | ||||
|     /** | ||||
|      * Downloaded size. | ||||
|      */ | ||||
|     size: number; | ||||
| export type CoreCourseModulePrefetchInfo = CoreCourseModulePackageLastDownloaded & { | ||||
|     size: number; // Downloaded size.
 | ||||
|     sizeReadable: string; // Downloadable size in a readable format.
 | ||||
|     status: string; // Module status.
 | ||||
|     statusIcon?: string; // Icon's name of the module status.
 | ||||
| }; | ||||
| 
 | ||||
|     /** | ||||
|      * Downloadable size in a readable format. | ||||
|      */ | ||||
|     sizeReadable: string; | ||||
| 
 | ||||
|     /** | ||||
|      * Module status. | ||||
|      */ | ||||
|     status: string; | ||||
| 
 | ||||
|     /** | ||||
|      * Icon's name of the module status. | ||||
|      */ | ||||
|     statusIcon?: string; | ||||
| 
 | ||||
|     /** | ||||
|      * Time when the module was last downloaded. | ||||
|      */ | ||||
|     downloadTime: number; | ||||
| 
 | ||||
|     /** | ||||
|      * Download time in a readable format. | ||||
|      */ | ||||
|     downloadTimeReadable: string; | ||||
| /** | ||||
|  * Prefetch info of a module. | ||||
|  */ | ||||
| export type CoreCourseModulePackageLastDownloaded = { | ||||
|     downloadTime: number; // Time when the module was last downloaded.
 | ||||
|     downloadTimeReadable: string; // Download time in a readable format.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
| @ -491,22 +473,18 @@ export class CoreCourseHelperProvider { | ||||
|      * | ||||
|      * @param module Module to remove the files. | ||||
|      * @param courseId Course ID the module belongs to. | ||||
|      * @param done Function to call when done. It will close the context menu. | ||||
|      * @return Promise resolved when done. | ||||
|      * @deprecated since 4.0 | ||||
|      */ | ||||
|     async confirmAndRemoveFiles(module: CoreCourseModuleData, courseId: number, done?: () => void): Promise<void> { | ||||
|     async confirmAndRemoveFiles(module: CoreCourseModuleData, courseId: number): Promise<void> { | ||||
|         let modal: CoreIonLoadingElement | undefined; | ||||
| 
 | ||||
|         try { | ||||
| 
 | ||||
|             await CoreDomUtils.showDeleteConfirm('addon.storagemanager.confirmdeletedatafrom', { name: module.name }); | ||||
| 
 | ||||
|             modal = await CoreDomUtils.showModalLoading(); | ||||
| 
 | ||||
|             await this.removeModuleStoredData(module, courseId); | ||||
| 
 | ||||
|             done && done(); | ||||
| 
 | ||||
|         } catch (error) { | ||||
|             if (error) { | ||||
|                 CoreDomUtils.showErrorModal(error); | ||||
| @ -571,44 +549,6 @@ export class CoreCourseHelperProvider { | ||||
|         await CoreDomUtils.confirmDownloadSize(sizeSum, undefined, undefined, undefined, undefined, alwaysConfirm); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Helper function to prefetch a module, showing a confirmation modal if the size is big. | ||||
|      * This function is meant to be called from a context menu option. It will also modify some data like the prefetch icon. | ||||
|      * | ||||
|      * @param instance The component instance that has the context menu. | ||||
|      * @param module Module to be prefetched | ||||
|      * @param courseId Course ID the module belongs to. | ||||
|      * @param done Function to call when done. It will close the context menu. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async contextMenuPrefetch( | ||||
|         instance: ComponentWithContextMenu, | ||||
|         module: CoreCourseModuleData, | ||||
|         courseId: number, | ||||
|         done?: () => void, | ||||
|     ): Promise<void> { | ||||
|         const initialIcon = instance.prefetchStatusIcon; | ||||
|         instance.prefetchStatusIcon = CoreConstants.ICON_DOWNLOADING; // Show spinner since this operation might take a while.
 | ||||
| 
 | ||||
|         try { | ||||
|             // We need to call getDownloadSize, the package might have been updated.
 | ||||
|             const size = await CoreCourseModulePrefetchDelegate.getModuleDownloadSize(module, courseId, true); | ||||
| 
 | ||||
|             await CoreDomUtils.confirmDownloadSize(size); | ||||
| 
 | ||||
|             await CoreCourseModulePrefetchDelegate.prefetchModule(module, courseId, true); | ||||
| 
 | ||||
|             // Success, close menu.
 | ||||
|             done && done(); | ||||
|         } catch (error) { | ||||
|             instance.prefetchStatusIcon = initialIcon; | ||||
| 
 | ||||
|             if (!instance.isDestroyed) { | ||||
|                 CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check whether a course is accessed using guest access. | ||||
|      * | ||||
| @ -1045,87 +985,6 @@ export class CoreCourseHelperProvider { | ||||
|         await CoreFilepool.downloadOrPrefetchFiles(siteId, files, false, false, component, componentId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fill the Context Menu for a certain module. | ||||
|      * | ||||
|      * @param instance The component instance that has the context menu. | ||||
|      * @param module Module to be prefetched | ||||
|      * @param courseId Course ID the module belongs to. | ||||
|      * @param invalidateCache Invalidates the cache first. | ||||
|      * @param component Component of the module. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async fillContextMenu( | ||||
|         instance: ComponentWithContextMenu, | ||||
|         module: CoreCourseModuleData, | ||||
|         courseId: number, | ||||
|         invalidateCache?: boolean, | ||||
|         component?: string, | ||||
|     ): Promise<void> { | ||||
|         const siteId = CoreSites.getCurrentSiteId(); | ||||
| 
 | ||||
|         const moduleInfo = await this.getModulePrefetchInfo(module, courseId, invalidateCache, component); | ||||
| 
 | ||||
|         instance.size = moduleInfo.sizeReadable; | ||||
|         instance.prefetchStatusIcon = moduleInfo.statusIcon; | ||||
|         instance.prefetchStatus = moduleInfo.status; | ||||
|         instance.downloadTimeReadable = CoreTextUtils.ucFirst(moduleInfo.downloadTimeReadable); | ||||
| 
 | ||||
|         if (moduleInfo.status != CoreConstants.NOT_DOWNLOADABLE) { | ||||
|             // Module is downloadable, get the text to display to prefetch.
 | ||||
|             if (moduleInfo.downloadTime && moduleInfo.downloadTime > 0) { | ||||
|                 instance.prefetchText = Translate.instant('core.lastdownloaded') + ': ' + moduleInfo.downloadTimeReadable; | ||||
|             } else { | ||||
|                 // Module not downloaded, show a default text.
 | ||||
|                 instance.prefetchText = Translate.instant('core.download'); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (moduleInfo.status == CoreConstants.DOWNLOADING) { | ||||
|             // Set this to empty to prevent "remove file" option showing up while downloading.
 | ||||
|             instance.size = ''; | ||||
|         } | ||||
| 
 | ||||
|         if (!instance.contextMenuStatusObserver && component) { | ||||
|             instance.contextMenuStatusObserver = CoreEvents.on( | ||||
|                 CoreEvents.PACKAGE_STATUS_CHANGED, | ||||
|                 (data) => { | ||||
|                     if (data.componentId == module.id && data.component == component) { | ||||
|                         this.fillContextMenu(instance, module, courseId, false, component); | ||||
|                     } | ||||
|                 }, | ||||
|                 siteId, | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         if (!instance.contextFileStatusObserver && component) { | ||||
|             // Debounce the update size function to prevent too many calls when downloading or deleting a whole activity.
 | ||||
|             const debouncedUpdateSize = CoreUtils.debounce(async () => { | ||||
|                 const moduleSize = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, courseId); | ||||
| 
 | ||||
|                 instance.size = moduleSize > 0 ? CoreTextUtils.bytesToSize(moduleSize, 2) : ''; | ||||
|             }, 1000); | ||||
| 
 | ||||
|             instance.contextFileStatusObserver = CoreEvents.on( | ||||
|                 CoreEvents.COMPONENT_FILE_ACTION, | ||||
|                 (data) => { | ||||
|                     if (data.component != component || data.componentId != module.id) { | ||||
|                         // The event doesn't belong to this component, ignore.
 | ||||
|                         return; | ||||
|                     } | ||||
| 
 | ||||
|                     if (!CoreFilepool.isFileEventDownloadedOrDeleted(data)) { | ||||
|                         return; | ||||
|                     } | ||||
| 
 | ||||
|                     // Update the module size.
 | ||||
|                     debouncedUpdateSize(); | ||||
|                 }, | ||||
|                 siteId, | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a course. It will first check the user courses, and fallback to another WS if not enrolled. | ||||
|      * | ||||
| @ -1482,12 +1341,9 @@ export class CoreCourseHelperProvider { | ||||
|     async getModulePrefetchInfo( | ||||
|         module: CoreCourseModuleData, | ||||
|         courseId: number, | ||||
|         invalidateCache?: boolean, | ||||
|         component?: string, | ||||
|         invalidateCache = false, | ||||
|         component = '', | ||||
|     ): Promise<CoreCourseModulePrefetchInfo> { | ||||
| 
 | ||||
|         const siteId = CoreSites.getCurrentSiteId(); | ||||
| 
 | ||||
|         if (invalidateCache) { | ||||
|             // Currently, some modules pass invalidateCache=false because they already invalidate data in downloadResourceIfNeeded.
 | ||||
|             // If this function is changed to do more actions if invalidateCache=true, please review those modules.
 | ||||
| @ -1499,7 +1355,7 @@ export class CoreCourseHelperProvider { | ||||
|         const results = await Promise.all([ | ||||
|             CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, courseId), | ||||
|             CoreCourseModulePrefetchDelegate.getModuleStatus(module, courseId), | ||||
|             CoreUtils.ignoreErrors(CoreFilepool.getPackageData(siteId, component || '', module.id)), | ||||
|             this.getModulePackageLastDownloaded(module, component), | ||||
|         ]); | ||||
| 
 | ||||
|         // Treat stored size.
 | ||||
| @ -1526,33 +1382,51 @@ export class CoreCourseHelperProvider { | ||||
|                 break; | ||||
|         } | ||||
| 
 | ||||
|         // Treat download time.
 | ||||
|         if (!results[2] || !results[2].downloadTime || !CoreFileHelper.isStateDownloaded(results[2].status || '')) { | ||||
|             // Not downloaded.
 | ||||
|             return { | ||||
|                 size, | ||||
|                 sizeReadable, | ||||
|                 status, | ||||
|                 statusIcon, | ||||
|                 downloadTime: 0, | ||||
|                 downloadTimeReadable: '', | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         const now = CoreTimeUtils.timestamp(); | ||||
|         const downloadTime = results[2].downloadTime; | ||||
|         let downloadTimeReadable = ''; | ||||
|         if (now - results[2].downloadTime < 7 * 86400) { | ||||
|             downloadTimeReadable = moment(results[2].downloadTime * 1000).fromNow(); | ||||
|         } else { | ||||
|             downloadTimeReadable = moment(results[2].downloadTime * 1000).calendar(); | ||||
|         } | ||||
|         const packageData = results[2]; | ||||
| 
 | ||||
|         return { | ||||
|             size, | ||||
|             sizeReadable, | ||||
|             status, | ||||
|             statusIcon, | ||||
|             downloadTime: packageData.downloadTime, | ||||
|             downloadTimeReadable: packageData.downloadTimeReadable, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get prefetch info for a module. | ||||
|      * | ||||
|      * @param module Module to get the info from. | ||||
|      * @param component Component of the module. | ||||
|      * @return Promise resolved with the info. | ||||
|      */ | ||||
|     async getModulePackageLastDownloaded( | ||||
|         module: CoreCourseModuleData, | ||||
|         component = '', | ||||
|     ): Promise<CoreCourseModulePackageLastDownloaded> { | ||||
|         const siteId = CoreSites.getCurrentSiteId(); | ||||
|         const packageData = await CoreUtils.ignoreErrors(CoreFilepool.getPackageData(siteId, component, module.id)); | ||||
| 
 | ||||
|         // Treat download time.
 | ||||
|         if (!packageData || !packageData.downloadTime || !CoreFileHelper.isStateDownloaded(packageData.status || '')) { | ||||
|             // Not downloaded.
 | ||||
|             return { | ||||
|                 downloadTime: 0, | ||||
|                 downloadTimeReadable: '', | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         const now = CoreTimeUtils.timestamp(); | ||||
|         const downloadTime = packageData.downloadTime; | ||||
|         let downloadTimeReadable = ''; | ||||
|         if (now - downloadTime < 7 * 86400) { | ||||
|             downloadTimeReadable = moment(downloadTime * 1000).fromNow(); | ||||
|         } else { | ||||
|             downloadTimeReadable = moment(downloadTime * 1000).calendar(); | ||||
|         } | ||||
| 
 | ||||
|         return { | ||||
|             downloadTime, | ||||
|             downloadTimeReadable, | ||||
|         }; | ||||
| @ -2223,14 +2097,3 @@ export type CoreCourseOpenModuleOptions = { | ||||
|     sectionId?: number; // Section the module belongs to.
 | ||||
|     modNavOptions?: CoreNavigationOptions; // Navigation options to open the module, including params to pass to the module.
 | ||||
| }; | ||||
| 
 | ||||
| type ComponentWithContextMenu = { | ||||
|     prefetchStatusIcon?: string; | ||||
|     isDestroyed?: boolean; | ||||
|     size?: string; | ||||
|     prefetchStatus?: string; | ||||
|     prefetchText?: string; | ||||
|     downloadTimeReadable?: string; | ||||
|     contextMenuStatusObserver?: CoreEventObserver; | ||||
|     contextFileStatusObserver?: CoreEventObserver; | ||||
| }; | ||||
|  | ||||
| @ -130,9 +130,13 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler { | ||||
|         navOptions.params = navOptions.params || {}; | ||||
|         Object.assign(navOptions.params, { course: course }); | ||||
| 
 | ||||
|         // Don't return the .push promise, we don't want to display a loading modal during the page transition.
 | ||||
|         const currentTab = CoreNavigator.getCurrentMainMenuTab(); | ||||
|         const routeDepth = CoreNavigator.getRouteDepth(`/main/${currentTab}/course/${course.id}`); | ||||
|         // When replace is true, disable route depth.
 | ||||
|         let routeDepth = 0; | ||||
|         if (!navOptions.replace) { | ||||
|             // Don't return the .push promise, we don't want to display a loading modal during the page transition.
 | ||||
|             const currentTab = CoreNavigator.getCurrentMainMenuTab(); | ||||
|             routeDepth = CoreNavigator.getRouteDepth(`/main/${currentTab}/course/${course.id}`); | ||||
|         } | ||||
|         const deepPath = '/deep'.repeat(routeDepth); | ||||
| 
 | ||||
|         CoreNavigator.navigateToSitePath(`course${deepPath}/${course.id}`, navOptions); | ||||
|  | ||||
| @ -202,10 +202,10 @@ export interface CoreCourseModuleMainComponent { | ||||
|      * Refresh the data. | ||||
|      * | ||||
|      * @param refresher Refresher. | ||||
|      * @param done Function to call when done. | ||||
|      * @param showErrors If show errors to the user of hide them. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     doRefresh(refresher?: IonRefresher, done?: () => void): Promise<void>; | ||||
|     doRefresh(refresher?: IonRefresher | null, showErrors?: boolean): Promise<void>; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  | ||||
| @ -4,7 +4,7 @@ | ||||
|     <div *ngIf="layout == 'card' || layout == 'summarycard'" (click)="openCourse()" class="core-course-thumb" | ||||
|         [class.core-course-color-img]="course.courseImage"> | ||||
|         <img *ngIf="course.courseImage" [src]="course.courseImage" core-external-content alt="" /> | ||||
|         <ion-icon *ngIf="!course.courseImage" name="fas-graduation-cap" class="course-icon"> | ||||
|         <ion-icon *ngIf="!course.courseImage" name="fas-graduation-cap" class="course-icon" aria-hidden="true"> | ||||
|         </ion-icon> | ||||
|     </div> | ||||
| 
 | ||||
| @ -31,7 +31,8 @@ | ||||
|         [class.item-disabled]="course.visible == 0"> | ||||
| 
 | ||||
|         <ng-container *ngIf="layout == 'list' || layout == 'listwithenrol'"> | ||||
|             <ion-icon *ngIf="!course.courseImage" name="fas-graduation-cap" slot="start" class="course-icon core-course-thumb"> | ||||
|             <ion-icon *ngIf="!course.courseImage" name="fas-graduation-cap" slot="start" class="course-icon core-course-thumb" | ||||
|                 aria-hidden="true"> | ||||
|             </ion-icon> | ||||
|             <ion-avatar *ngIf="course.courseImage" slot="start" class="core-course-thumb"> | ||||
|                 <img [src]="course.courseImage" core-external-content alt="" /> | ||||
|  | ||||
| @ -22,7 +22,7 @@ import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-ro | ||||
| import { CoreUserDelegate } from '@features/user/services/user-delegate'; | ||||
| import { PARTICIPANTS_PAGE_NAME } from '@features/user/user.module'; | ||||
| import { CoreGradesProvider } from './services/grades'; | ||||
| import { CoreGradesHelperProvider } from './services/grades-helper'; | ||||
| import { CoreGradesHelperProvider, GRADES_PAGE_NAME } from './services/grades-helper'; | ||||
| import { CoreGradesCourseOptionHandler } from './services/handlers/course-option'; | ||||
| import { CoreGradesOverviewLinkHandler } from './services/handlers/overview-link'; | ||||
| import { CoreGradesUserHandler } from './services/handlers/user'; | ||||
| @ -33,8 +33,6 @@ export const CORE_GRADES_SERVICES: Type<unknown>[] = [ | ||||
|     CoreGradesHelperProvider, | ||||
| ]; | ||||
| 
 | ||||
| export const GRADES_PAGE_NAME = 'grades'; | ||||
| 
 | ||||
| const mainMenuChildrenRoutes: Routes = [ | ||||
|     { | ||||
|         path: GRADES_PAGE_NAME, | ||||
|  | ||||
| @ -9,6 +9,7 @@ | ||||
|     "fail": "Fail", | ||||
|     "feedback": "Feedback", | ||||
|     "grade": "Grade", | ||||
|     "gradebook": "Gradebook", | ||||
|     "gradeitem": "Grade item", | ||||
|     "gradepass": "Grade to pass", | ||||
|     "grades": "Grades", | ||||
|  | ||||
| @ -31,7 +31,7 @@ | ||||
|                             [attr.tabindex]="row.expandable && showSummary && 0" [attr.aria-expanded]="row.expanded" | ||||
|                             [attr.aria-label]="rowAriaLabel(row)" [attr.aria-controls]="row.detailsid" | ||||
|                             (ariaButtonClick)="row.expandable && showSummary && toggleRow(row)" [class]="row.rowclass" | ||||
|                             [class.core-grades-grade-clickable]="row.expandable && showSummary"> | ||||
|                             [class.core-grades-grade-clickable]="row.expandable && showSummary" [id]="'grade-'+row.id"> | ||||
|                             <ng-container *ngIf="row.itemtype"> | ||||
|                                 <td *ngIf="row.itemtype == 'category'" class="core-grades-table-category" [attr.rowspan]="row.rowspan"> | ||||
|                                 </td> | ||||
| @ -71,40 +71,6 @@ | ||||
|                         <tr *ngIf="row.expandable" [id]="row.detailsid" [class]="row.rowclass" [hidden]="!row.expanded"> | ||||
|                             <td [attr.colspan]="totalColumnsSpan"> | ||||
|                                 <ion-list> | ||||
|                                     <ion-item *ngIf="row.itemname && row.link" class="ion-text-wrap" detail="true" [href]="row.link" | ||||
|                                         core-link capture="true"> | ||||
|                                         <ion-icon *ngIf="row.icon" name="{{row.icon}}" slot="start" [attr.aria-label]="row.iconAlt"> | ||||
|                                         </ion-icon> | ||||
|                                         <img *ngIf="row.image && !row.itemmodule" [src]="row.image && row.itemmodule" slot="start" | ||||
|                                             [alt]="row.iconAlt" /> | ||||
|                                         <core-mod-icon *ngIf="row.image && row.itemmodule" [modicon]="row.image" slot="start" | ||||
|                                             [modname]="row.itemmodule"> | ||||
|                                         </core-mod-icon> | ||||
|                                         <ion-label> | ||||
|                                             <h2> | ||||
|                                                 <core-format-text [text]="row.itemname" contextLevel="course" | ||||
|                                                     [contextInstanceId]="courseId"> | ||||
|                                                 </core-format-text> | ||||
|                                             </h2> | ||||
|                                         </ion-label> | ||||
|                                     </ion-item> | ||||
| 
 | ||||
|                                     <ion-item *ngIf="row.itemname && !row.link" class="ion-text-wrap"> | ||||
|                                         <ion-icon *ngIf="row.icon" name="{{row.icon}}" slot="start" [attr.aria-label]="row.iconAlt"> | ||||
|                                         </ion-icon> | ||||
|                                         <img *ngIf="row.image && !row.itemmodule" [src]="row.image" slot="start" [alt]="row.iconAlt" /> | ||||
|                                         <core-mod-icon *ngIf="row.image && row.itemmodule" [modicon]="row.image" slot="start" | ||||
|                                             [modname]="row.itemmodule"> | ||||
|                                         </core-mod-icon> | ||||
|                                         <ion-label> | ||||
|                                             <h2> | ||||
|                                                 <core-format-text [text]="row.itemname" contextLevel="course" | ||||
|                                                     [contextInstanceId]="courseId"> | ||||
|                                                 </core-format-text> | ||||
|                                             </h2> | ||||
|                                         </ion-label> | ||||
|                                     </ion-item> | ||||
| 
 | ||||
|                                     <ion-item class="ion-text-wrap" *ngIf="row.weight"> | ||||
|                                         <ion-label> | ||||
|                                             <h2>{{ 'core.grades.weight' | translate}}</h2> | ||||
|  | ||||
| @ -13,8 +13,8 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { ActivatedRoute } from '@angular/router'; | ||||
| import { AfterViewInit, Component, ElementRef, OnDestroy } from '@angular/core'; | ||||
| import { IonRefresher } from '@ionic/angular'; | ||||
| import { AfterViewInit, Component, ElementRef, OnDestroy, Optional } from '@angular/core'; | ||||
| import { IonContent, IonRefresher } from '@ionic/angular'; | ||||
| 
 | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreGrades } from '@features/grades/services/grades'; | ||||
| @ -44,6 +44,7 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy { | ||||
| 
 | ||||
|     courseId!: number; | ||||
|     userId!: number; | ||||
|     gradeId?: number; | ||||
|     expandLabel!: string; | ||||
|     collapseLabel!: string; | ||||
|     title?: string; | ||||
| @ -53,10 +54,16 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy { | ||||
|     totalColumnsSpan?: number; | ||||
|     withinSplitView?: boolean; | ||||
| 
 | ||||
|     constructor(protected route: ActivatedRoute, protected element: ElementRef<HTMLElement>) { | ||||
|     constructor( | ||||
|         protected route: ActivatedRoute, | ||||
|         protected element: ElementRef<HTMLElement>, | ||||
|         @Optional() protected content?: IonContent, | ||||
|     ) { | ||||
|         try { | ||||
|             this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId', { route }); | ||||
|             this.userId = CoreNavigator.getRouteNumberParam('userId', { route }) ?? CoreSites.getCurrentSiteUserId(); | ||||
|             this.gradeId = CoreNavigator.getRouteNumberParam('gradeId', { route }); | ||||
| 
 | ||||
|             this.expandLabel = Translate.instant('core.expand'); | ||||
|             this.collapseLabel = Translate.instant('core.collapse'); | ||||
| 
 | ||||
| @ -116,13 +123,14 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy { | ||||
|      * Toggle whether a row is expanded or collapsed. | ||||
|      * | ||||
|      * @param row Row. | ||||
|      * @param expand If defined, force expand or collapse. | ||||
|      */ | ||||
|     toggleRow(row: CoreGradesFormattedTableRow): void { | ||||
|     toggleRow(row: CoreGradesFormattedTableRow, expand?: boolean): void { | ||||
|         if (!this.rows || !this.columns) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         row.expanded = !row.expanded; | ||||
|         row.expanded = expand ?? !row.expanded; | ||||
| 
 | ||||
|         let colspan: number = this.columns.length + (row.colspan ?? 0) - 1; | ||||
|         for (let i = this.rows.indexOf(row) - 1; i >= 0; i--) { | ||||
| @ -155,6 +163,22 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy { | ||||
|     private async fetchInitialGrades(): Promise<void> { | ||||
|         try { | ||||
|             await this.fetchGrades(); | ||||
| 
 | ||||
|             if (this.gradeId && this.rows) { | ||||
|                 const row = this.rows.find((row) => row.id == this.gradeId); | ||||
| 
 | ||||
|                 if (row) { | ||||
|                     this.toggleRow(row, true); | ||||
|                     await CoreUtils.nextTick(); | ||||
| 
 | ||||
|                     CoreDomUtils.scrollToElementBySelector( | ||||
|                         this.element.nativeElement, | ||||
|                         this.content, | ||||
|                         '#grade-' + row.id, | ||||
|                     ); | ||||
|                     this.gradeId = undefined; | ||||
|                 } | ||||
|             } | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.showErrorModalDefault(error, 'Error loading course'); | ||||
| 
 | ||||
|  | ||||
| @ -80,8 +80,8 @@ | ||||
|         } | ||||
| 
 | ||||
|         core-mod-icon { | ||||
|             padding: 0.1rem; | ||||
|             --size: 16px; | ||||
|             padding: 3px; | ||||
|             --size: 10px; | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
| @ -35,7 +35,8 @@ import { CoreNavigator } from '@services/navigator'; | ||||
| import { makeSingleton, Translate } from '@singletons'; | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| import { CoreCourseHelper } from '@features/course/services/course-helper'; | ||||
| import { GRADES_PAGE_NAME } from '../grades.module'; | ||||
| 
 | ||||
| export const GRADES_PAGE_NAME = 'grades'; | ||||
| 
 | ||||
| /** | ||||
|  * Service that provides some features regarding grades information. | ||||
| @ -456,6 +457,38 @@ export class CoreGradesHelperProvider { | ||||
|         }).map((row) => this.formatGradeRow(row))); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get module grades to display. | ||||
|      * | ||||
|      * @param courseId Course Id. | ||||
|      * @param moduleId Module Id. | ||||
|      * @return Formatted table rows. | ||||
|      */ | ||||
|     async getModuleGrades(courseId: number, moduleId: number): Promise<CoreGradesFormattedTableRow[] > { | ||||
|         const table = await CoreGrades.getCourseGradesTable(courseId); | ||||
| 
 | ||||
|         if (!table.tabledata) { | ||||
|             return []; | ||||
|         } | ||||
| 
 | ||||
|         // Find href containing "/mod/xxx/xxx.php".
 | ||||
|         const regex = /href="([^"]*\/mod\/[^"|^/]*\/[^"|^.]*\.php[^"]*)/; | ||||
| 
 | ||||
|         return await Promise.all(table.tabledata.filter((row) => { | ||||
|             if (row.itemname && row.itemname.content) { | ||||
|                 const matches = row.itemname.content.match(regex); | ||||
| 
 | ||||
|                 if (matches && matches.length) { | ||||
|                     const hrefParams = CoreUrlUtils.extractUrlParams(matches[1]); | ||||
| 
 | ||||
|                     return hrefParams && parseInt(hrefParams.id) === moduleId; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return false; | ||||
|         }).map((row) => this.formatGradeRowForTable(row))); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Go to view grades. | ||||
|      * | ||||
| @ -497,9 +530,12 @@ export class CoreGradesHelperProvider { | ||||
|             const gradeId = item.id; | ||||
| 
 | ||||
|             await CoreUtils.ignoreErrors( | ||||
|                 CoreNavigator.navigateToSitePath(`/${GRADES_PAGE_NAME}/${courseId}/${gradeId}`, { siteId }), | ||||
|                 CoreNavigator.navigateToSitePath( | ||||
|                     `/${GRADES_PAGE_NAME}/${courseId}`, | ||||
|                     { params: { gradeId }, siteId }, | ||||
|                 ), | ||||
|             ); | ||||
|         } catch (error) { | ||||
|         } catch { | ||||
|             try { | ||||
|                 // Cannot get grade items or there's no need to.
 | ||||
|                 if (userId && userId != currentUserId) { | ||||
| @ -519,7 +555,7 @@ export class CoreGradesHelperProvider { | ||||
| 
 | ||||
|                 // Open the course with the grades tab selected.
 | ||||
|                 await CoreCourseHelper.getAndOpenCourse(courseId, { selectedTab: 'CoreGrades' }, siteId); | ||||
|             } catch (error) { | ||||
|             } catch { | ||||
|                 // Cannot get course for some reason, just open the grades page.
 | ||||
|                 await CoreNavigator.navigateToSitePath(`/${GRADES_PAGE_NAME}/${courseId}`, { siteId }); | ||||
|             } | ||||
| @ -565,7 +601,7 @@ export class CoreGradesHelperProvider { | ||||
|             row.iconAlt = Translate.instant('core.grades.aggregatesum'); | ||||
|         } else if (text.indexOf('/outcomes') > -1 || text.indexOf('fa-tasks') > -1) { | ||||
|             row.itemtype = 'outcome'; | ||||
|             row.icon = 'fas-chart-pie'; | ||||
|             row.icon = 'fas-tasks'; | ||||
|             row.iconAlt = Translate.instant('core.grades.outcome'); | ||||
|         } else if (text.indexOf('i/folder') > -1 || text.indexOf('fa-folder') > -1) { | ||||
|             row.itemtype = 'category'; | ||||
|  | ||||
| @ -199,7 +199,7 @@ export class CoreGradesProvider { | ||||
|         const table = await site.read<CoreGradesGetUserGradesTableWSResponse>('gradereport_user_get_grades_table', params, preSets); | ||||
| 
 | ||||
|         if (!table?.tables?.[0]) { | ||||
|             throw new CoreError('Coudln\'t get course grades table'); | ||||
|             throw new CoreError('Couldn\'t get course grades table'); | ||||
|         } | ||||
| 
 | ||||
|         return table.tables[0]; | ||||
|  | ||||
| @ -15,10 +15,10 @@ | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; | ||||
| import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; | ||||
| import { GRADES_PAGE_NAME } from '@features/grades/grades.module'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { CoreGrades } from '../grades'; | ||||
| import { GRADES_PAGE_NAME } from '../grades-helper'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to treat links to overview courses grades. | ||||
|  | ||||
| @ -14,7 +14,6 @@ | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { COURSE_PAGE_NAME } from '@features/course/course.module'; | ||||
| import { GRADES_PAGE_NAME } from '@features/grades/grades.module'; | ||||
| 
 | ||||
| import { CoreGrades } from '@features/grades/services/grades'; | ||||
| import { CoreUserProfile } from '@features/user/services/user'; | ||||
| @ -29,6 +28,7 @@ import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { GRADES_PAGE_NAME } from '../grades-helper'; | ||||
| 
 | ||||
| /** | ||||
|  * Profile grades handler. | ||||
|  | ||||
| @ -73,11 +73,11 @@ | ||||
|                     <h3 class="item-heading">{{ 'core.login.potentialidps' | translate }}</h3> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ion-item button *ngFor="let provider of identityProviders" class="ion-text-wrap core-oauth-icon" | ||||
|                 (click)="oauthClicked(provider)" [attr.aria-label]="provider.name" detail="false"> | ||||
|             <ion-button fill="outline" *ngFor="let provider of identityProviders" class="ion-text-wrap core-oauth-provider" | ||||
|                 (click)="oauthClicked(provider)" [attr.aria-label]="provider.name" expand="block"> | ||||
|                 <img [src]="provider.iconurl" alt="" width="32" height="32" slot="start"> | ||||
|                 <ion-label>{{provider.name}}</ion-label> | ||||
|             </ion-item> | ||||
|             </ion-button> | ||||
|         </ion-list> | ||||
| 
 | ||||
|         <ion-list *ngIf="canSignup" class="ion-padding-top core-login-sign-up"> | ||||
|  | ||||
| @ -87,11 +87,11 @@ | ||||
|                 <h3 class="item-heading">{{ 'core.login.potentialidps' | translate }}</h3> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
|         <ion-item button *ngFor="let provider of identityProviders" class="ion-text-wrap core-oauth-icon" (click)="oauthClicked(provider)" | ||||
|             detail="false"> | ||||
|             <img [src]="provider.iconurl" alt="" role="presentation" width="32" height="32" slot="start"> | ||||
|         <ion-button fill="outline" *ngFor="let provider of identityProviders" class="ion-text-wrap core-oauth-provider" | ||||
|             (click)="oauthClicked(provider)" [attr.aria-label]="provider.name" expand="block"> | ||||
|             <img [src]="provider.iconurl" alt="" width="32" height="32" slot="start"> | ||||
|             <ion-label>{{provider.name}}</ion-label> | ||||
|         </ion-item> | ||||
|         </ion-button> | ||||
|     </ion-list> | ||||
| 
 | ||||
|     <!-- If OAuth, display cancel button since the form isn't displayed. --> | ||||
|  | ||||
| @ -15,7 +15,7 @@ | ||||
| import { Component, OnChanges, Input, ViewChild, Output, EventEmitter } from '@angular/core'; | ||||
| import { IonRefresher } from '@ionic/angular'; | ||||
| 
 | ||||
| import { CoreCourseFormatComponent } from '@features/course/components/format/format'; | ||||
| import { CoreCourseFormatComponent } from '@features/course/components/course-format/course-format'; | ||||
| import { CoreCourseModuleCompletionData, CoreCourseSection } from '@features/course/services/course-helper'; | ||||
| import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate'; | ||||
| import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; | ||||
|  | ||||
| @ -1,28 +1,8 @@ | ||||
| <!-- Buttons to add to the header. --> | ||||
| <core-navbar-buttons slot="end"> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item [hidden]="!displayOpenInBrowser || !externalUrl || ( | ||||
|             content?.compileComponent?.componentInstance?.displayOpenInBrowser === false)" [priority]="900" | ||||
|             [content]="'core.openinbrowser' | translate" [href]="externalUrl" iconAction="fas-external-link-alt"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item [hidden]="!displayDescription || !description || ( | ||||
|             content?.compileComponent?.componentInstance?.displayDescription === false)" [priority]="800" | ||||
|             [content]="'core.moduleintro' | translate" (action)="expandDescription()" iconAction="fas-arrow-right"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item [hidden]="!displayRefresh || ( | ||||
|             content?.compileComponent?.componentInstance?.displayRefresh === false)" [priority]="700" | ||||
|             [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item [hidden]="!displayPrefetch || !prefetchStatusIcon || ( | ||||
|             content?.compileComponent?.componentInstance?.displayPrefetch === false)" [priority]="600" [content]="prefetchText" | ||||
|             (action)="prefetch()" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|         <core-context-menu-item [hidden]="!displaySize || !size || ( | ||||
|             content?.compileComponent?.componentInstance?.displaySize === false)" [priority]="500" | ||||
|             [content]="'core.clearstoreddata' | translate:{$a: size}" [iconDescription]="'fas-archive'" (action)="removeFiles()" | ||||
|             iconAction="fas-trash" [closeOnClick]="false"> | ||||
|         </core-context-menu-item> | ||||
|     </core-context-menu> | ||||
|     <ion-button fill="clear" (click)="openModuleSummary()" [attr.aria-label]="'core.info' | translate"> | ||||
|         <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|     </ion-button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <core-site-plugins-plugin-content *ngIf="component && method" [component]="component" [method]="method" [args]="args" | ||||
|  | ||||
| @ -14,8 +14,13 @@ | ||||
| 
 | ||||
| import { CoreConstants } from '@/core/constants'; | ||||
| import { Component, OnInit, OnDestroy, Input, ViewChild } from '@angular/core'; | ||||
| import { CoreIonLoadingElement } from '@classes/ion-loading'; | ||||
| 
 | ||||
| import { CoreSiteWSPreSets } from '@classes/site'; | ||||
| import { | ||||
|     CoreCourseModuleSummaryResult, | ||||
|     CoreCourseModuleSummaryComponent, | ||||
| } from '@features/course/components/module-summary/module-summary'; | ||||
| import { CoreCourseHelper, CoreCourseModuleData } from '@features/course/services/course-helper'; | ||||
| import { | ||||
|     CoreCourseModuleDelegate, | ||||
| @ -28,10 +33,8 @@ import { | ||||
|     CoreSitePluginsCourseModuleHandlerData, | ||||
| } from '@features/siteplugins/services/siteplugins'; | ||||
| import { IonRefresher } from '@ionic/angular'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { Translate } from '@singletons'; | ||||
| import { CoreEventObserver } from '@singletons/events'; | ||||
| import { CoreSitePluginsPluginContentComponent } from '../plugin-content/plugin-content'; | ||||
| 
 | ||||
| /** | ||||
| @ -55,22 +58,42 @@ export class CoreSitePluginsModuleIndexComponent implements OnInit, OnDestroy, C | ||||
|     args?: Record<string, unknown>; | ||||
|     initResult?: CoreSitePluginsContent | null; | ||||
|     preSets?: CoreSiteWSPreSets; | ||||
| 
 | ||||
|     // Data for context menu.
 | ||||
|     externalUrl?: string; | ||||
|     description?: string; | ||||
|     refreshIcon?: string; | ||||
| 
 | ||||
|     /** | ||||
|      * @deprecated since 4.0, use module.url instead. | ||||
|      */ | ||||
|     externalUrl?: string; | ||||
|     /** | ||||
|      * @deprecated since 4.0. It won't be populated anymore. | ||||
|      */ | ||||
|     refreshIcon = CoreConstants.ICON_REFRESH; | ||||
|     /** | ||||
|      * @deprecated since 4.0.. It won't be populated anymore. | ||||
|      */ | ||||
|     prefetchStatus?: string; | ||||
|     /** | ||||
|      * @deprecated since 4.0. It won't be populated anymore. | ||||
|      */ | ||||
|     prefetchStatusIcon?: string; | ||||
|     /** | ||||
|      * @deprecated since 4.0. It won't be populated anymore. | ||||
|      */ | ||||
|     prefetchText?: string; | ||||
|     /** | ||||
|      * @deprecated since 4.0. It won't be populated anymore. | ||||
|      */ | ||||
|     size?: string; | ||||
|     contextMenuStatusObserver?: CoreEventObserver; | ||||
|     contextFileStatusObserver?: CoreEventObserver; | ||||
| 
 | ||||
|     displayOpenInBrowser = true; | ||||
|     displayDescription = true; | ||||
|     displayRefresh = true; | ||||
|     displayPrefetch = true; | ||||
|     displaySize = true; | ||||
|     displayGrades = false; | ||||
|     // @TODO:  // Currently display blogs is not an option since it may change soon adding new summary handlers.
 | ||||
|     displayBlog = false; | ||||
| 
 | ||||
|     ptrEnabled = true; | ||||
|     isDestroyed = false; | ||||
| 
 | ||||
| @ -80,8 +103,6 @@ export class CoreSitePluginsModuleIndexComponent implements OnInit, OnDestroy, C | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         this.refreshIcon = CoreConstants.ICON_LOADING; | ||||
| 
 | ||||
|         if (!this.module) { | ||||
|             return; | ||||
|         } | ||||
| @ -110,6 +131,7 @@ export class CoreSitePluginsModuleIndexComponent implements OnInit, OnDestroy, C | ||||
|             this.displayRefresh = !CoreUtils.isFalseOrZero(handlerSchema.displayrefresh); | ||||
|             this.displayPrefetch = !CoreUtils.isFalseOrZero(handlerSchema.displayprefetch); | ||||
|             this.displaySize = !CoreUtils.isFalseOrZero(handlerSchema.displaysize); | ||||
|             this.displayGrades = CoreUtils.isTrueOrOne(handlerSchema.displaygrades); // False by default.
 | ||||
|             this.ptrEnabled = !CoreUtils.isFalseOrZero(handlerSchema.ptrenabled); | ||||
|         } | ||||
| 
 | ||||
| @ -122,71 +144,114 @@ export class CoreSitePluginsModuleIndexComponent implements OnInit, OnDestroy, C | ||||
|      * Refresh the data. | ||||
|      * | ||||
|      * @param refresher Refresher. | ||||
|      * @param done Function to call when done. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async doRefresh(refresher?: IonRefresher | null, done?: () => void): Promise<void> { | ||||
|         if (this.content) { | ||||
|             this.refreshIcon = CoreConstants.ICON_LOADING; | ||||
|         } | ||||
| 
 | ||||
|     async doRefresh(refresher?: IonRefresher | null): Promise<void> { | ||||
|         try { | ||||
|             await this.content?.refreshContent(false); | ||||
|         } finally { | ||||
|             refresher?.complete(); | ||||
|             done && done(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Function called when the data of the site plugin content is loaded. | ||||
|      */ | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||
|     contentLoaded(refresh: boolean): void { | ||||
|         this.refreshIcon = CoreConstants.ICON_REFRESH; | ||||
| 
 | ||||
|         // Check if there is a prefetch handler for this type of module.
 | ||||
|         if (CoreCourseModulePrefetchDelegate.getPrefetchHandlerFor(this.module.modname)) { | ||||
|             CoreCourseHelper.fillContextMenu(this, this.module, this.courseId, refresh, this.component); | ||||
|         } | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Function called when starting to load the data of the site plugin content. | ||||
|      */ | ||||
|     contentLoading(): void { | ||||
|         this.refreshIcon = CoreConstants.ICON_LOADING; | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Expand the description. | ||||
|      * | ||||
|      * @deprecated since 4.0 | ||||
|      */ | ||||
|     expandDescription(): void { | ||||
|         if (!this.description) { | ||||
|         this.openModuleSummary(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Opens a module summary page. | ||||
|      */ | ||||
|     async openModuleSummary(): Promise<void> { | ||||
|         if (!this.module) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         CoreTextUtils.viewText(Translate.instant('core.description'), this.description, { | ||||
|             component: this.component, | ||||
|             componentId: this.module.id, | ||||
|             filter: true, | ||||
|             contextLevel: 'module', | ||||
|             instanceId: this.module.id, | ||||
|             courseId: this.courseId, | ||||
|         const data = await CoreDomUtils.openSideModal<CoreCourseModuleSummaryResult>({ | ||||
|             component: CoreCourseModuleSummaryComponent, | ||||
|             componentProps: { | ||||
|                 moduleId: this.module.id, | ||||
|                 module: this.module, | ||||
|                 description: this.description, | ||||
|                 component: this.component, | ||||
|                 courseId: this.courseId, | ||||
|                 displayOptions: { | ||||
|                     displayOpenInBrowser: this.displayOpenInBrowser, | ||||
|                     displayDescription: this.displayDescription, | ||||
|                     displayRefresh: this.displayRefresh, | ||||
|                     displayPrefetch: this.displayPrefetch, | ||||
|                     displaySize: this.displaySize, | ||||
|                     displayBlog: this.displayBlog, | ||||
|                     displayGrades: this.displayGrades, | ||||
|                 }, | ||||
|             }, | ||||
|         }); | ||||
| 
 | ||||
|         if (data && data.action == 'refresh' && this.content?.dataLoaded) { | ||||
|             this.content?.refreshContent(true); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prefetch the module. | ||||
|      * | ||||
|      * @deprecated since 4.0 | ||||
|      */ | ||||
|     prefetch(): void { | ||||
|         CoreCourseHelper.contextMenuPrefetch(this, this.module, this.courseId); | ||||
|     async prefetch(): Promise<void> { | ||||
|         try { | ||||
|             // We need to call getDownloadSize, the package might have been updated.
 | ||||
|             const size = await CoreCourseModulePrefetchDelegate.getModuleDownloadSize(this.module, this.courseId, true); | ||||
| 
 | ||||
|             await CoreDomUtils.confirmDownloadSize(size); | ||||
| 
 | ||||
|             await CoreCourseModulePrefetchDelegate.prefetchModule(this.module, this.courseId, true); | ||||
|         } catch (error) { | ||||
|             if (!this.isDestroyed) { | ||||
|                 CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Confirm and remove downloaded files. | ||||
|      * | ||||
|      * @deprecated since 4.0 | ||||
|      */ | ||||
|     removeFiles(): void { | ||||
|         CoreCourseHelper.confirmAndRemoveFiles(this.module, this.courseId); | ||||
|     async removeFiles(): Promise<void> { | ||||
|         let modal: CoreIonLoadingElement | undefined; | ||||
| 
 | ||||
|         try { | ||||
|             await CoreDomUtils.showDeleteConfirm('addon.storagemanager.confirmdeletedatafrom', { name: this.module.name }); | ||||
| 
 | ||||
|             modal = await CoreDomUtils.showModalLoading(); | ||||
| 
 | ||||
|             await CoreCourseHelper.removeModuleStoredData(this.module, this.courseId); | ||||
|         } catch (error) { | ||||
|             if (error) { | ||||
|                 CoreDomUtils.showErrorModal(error); | ||||
|             } | ||||
|         } finally { | ||||
|             modal?.dismiss(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -871,6 +871,7 @@ export type CoreSitePluginsCourseModuleHandlerData = CoreSitePluginsHandlerCommo | ||||
|     displayrefresh?: boolean; | ||||
|     displayprefetch?: boolean; | ||||
|     displaysize?: boolean; | ||||
|     displaygrades?: boolean; | ||||
|     coursepagemethod?: string; | ||||
|     ptrenabled?: boolean; | ||||
|     supportedfeatures?: Record<string, unknown>; | ||||
|  | ||||
| @ -297,6 +297,21 @@ button, | ||||
| 
 | ||||
| ion-button { | ||||
|     margin: 4px 8px; | ||||
| 
 | ||||
|     ion-spinner[slot=start], | ||||
|     img[slot=start] { | ||||
|         @include margin-horizontal(-0.3em, 0.3em); | ||||
|     } | ||||
| 
 | ||||
|     ion-spinner[slot=end], | ||||
|     img[slot=end] { | ||||
|         @include margin-horizontal(-0.3em, 0.3em); | ||||
|     } | ||||
| 
 | ||||
|     ion-spinner[slot] { | ||||
|         width: 20px; | ||||
|         color: inherit; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| ion-button.button-outline { | ||||
| @ -460,6 +475,14 @@ ion-alert { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| ion-loading { | ||||
|     --border-radius: var(--huge-radius); | ||||
| 
 | ||||
|     .loading-wrapper { | ||||
|         border-radius: var(--border-radius) !important; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // Ionic list. | ||||
| ion-list { | ||||
|     padding: 0 !important; | ||||
| @ -1465,6 +1488,16 @@ ion-grid.core-no-grid > ion-row { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| ion-header.no-title { | ||||
|     --core-header-toolbar-border-width: 0; | ||||
|     --core-header-toolbar-background: transparent; | ||||
| 
 | ||||
|     ion-toolbar .button.button-clear, | ||||
|     ion-toolbar .button.button-solid { | ||||
|         --background: var(--ion-background-color); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| ion-header[collapsible] { | ||||
|     @include core-transition(all, 500ms); | ||||
| 
 | ||||
|  | ||||
| @ -233,6 +233,9 @@ | ||||
|     } | ||||
| 
 | ||||
|     --core-loading-spinner: var(--brand); | ||||
|     ion-loading { | ||||
|         --spinner-color: var(--core-loading-spinner); | ||||
|     } | ||||
|     ion-spinner, ion-refresher { | ||||
|         --ion-color-base: var(--core-loading-spinner); | ||||
|         --ion-color-primary: var(--core-loading-spinner); | ||||
|  | ||||
| @ -20,6 +20,10 @@ information provided here is intended especially for developers. | ||||
| - Most of the functions or callbacks that handle redirects/deeplinks have been modified to accept an object instead of just path + options. E.g.: CoreLoginHelper.isSiteLoggedOut, CoreLoginHelper.openBrowserForSSOLogin, CoreLoginHelper.openBrowserForOAuthLogin, CoreLoginHelper.prepareForSSOLogin, CoreApp.storeRedirect, CoreSites.loadSite. | ||||
| - Course preview page route has changed from course/:courseId/preview to course/:courseId/summary to match with the page name and characteristics. | ||||
| - The parameters of the following functions in CoreCourseHelper have changed: navigateToModuleByInstance, navigateToModule, openModule. | ||||
| - fillContextMenu, expandDescription, gotoBlog, prefetch and removeFiles functions have been removed from CoreCourseModuleMainResourceComponent. | ||||
| - contextMenuPrefetch and fillContextMenu have been removed from CoreCourseHelper. | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| === 3.9.5 === | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user