forked from EVOgeek/Vmeda.Online
		
	
						commit
						692a31e29b
					
				| @ -331,14 +331,18 @@ | |||||||
|   "addon.mod_assign.allowsubmissionsfromdatesummary": "assign", |   "addon.mod_assign.allowsubmissionsfromdatesummary": "assign", | ||||||
|   "addon.mod_assign.applytoteam": "assign", |   "addon.mod_assign.applytoteam": "assign", | ||||||
|   "addon.mod_assign.assignmentisdue": "assign", |   "addon.mod_assign.assignmentisdue": "assign", | ||||||
|  |   "addon.mod_assign.assigntimeleft": "assign", | ||||||
|   "addon.mod_assign.attemptnumber": "assign", |   "addon.mod_assign.attemptnumber": "assign", | ||||||
|   "addon.mod_assign.attemptreopenmethod": "assign", |   "addon.mod_assign.attemptreopenmethod": "assign", | ||||||
|   "addon.mod_assign.attemptreopenmethod_manual": "assign", |   "addon.mod_assign.attemptreopenmethod_manual": "assign", | ||||||
|   "addon.mod_assign.attemptreopenmethod_untilpass": "assign", |   "addon.mod_assign.attemptreopenmethod_untilpass": "assign", | ||||||
|   "addon.mod_assign.attemptsettings": "assign", |   "addon.mod_assign.attemptsettings": "assign", | ||||||
|  |   "addon.mod_assign.beginassignment": "assign", | ||||||
|  |   "addon.mod_assign.caneditsubmission": "assign", | ||||||
|   "addon.mod_assign.cannoteditduetostatementsubmission": "local_moodlemobileapp", |   "addon.mod_assign.cannoteditduetostatementsubmission": "local_moodlemobileapp", | ||||||
|   "addon.mod_assign.cannotgradefromapp": "local_moodlemobileapp", |   "addon.mod_assign.cannotgradefromapp": "local_moodlemobileapp", | ||||||
|   "addon.mod_assign.cannotsubmitduetostatementsubmission": "local_moodlemobileapp", |   "addon.mod_assign.cannotsubmitduetostatementsubmission": "local_moodlemobileapp", | ||||||
|  |   "addon.mod_assign.confirmstart": "assign", | ||||||
|   "addon.mod_assign.confirmsubmission": "assign", |   "addon.mod_assign.confirmsubmission": "assign", | ||||||
|   "addon.mod_assign.currentattempt": "assign", |   "addon.mod_assign.currentattempt": "assign", | ||||||
|   "addon.mod_assign.currentattemptof": "assign", |   "addon.mod_assign.currentattemptof": "assign", | ||||||
| @ -416,7 +420,10 @@ | |||||||
|   "addon.mod_assign.submitassignment_help": "assign", |   "addon.mod_assign.submitassignment_help": "assign", | ||||||
|   "addon.mod_assign.submittedearly": "assign", |   "addon.mod_assign.submittedearly": "assign", | ||||||
|   "addon.mod_assign.submittedlate": "assign", |   "addon.mod_assign.submittedlate": "assign", | ||||||
|  |   "addon.mod_assign.submittedovertime": "assign", | ||||||
|  |   "addon.mod_assign.submittedundertime": "assign", | ||||||
|   "addon.mod_assign.syncblockedusercomponent": "local_moodlemobileapp", |   "addon.mod_assign.syncblockedusercomponent": "local_moodlemobileapp", | ||||||
|  |   "addon.mod_assign.timelimit": "assign", | ||||||
|   "addon.mod_assign.timemodified": "assign", |   "addon.mod_assign.timemodified": "assign", | ||||||
|   "addon.mod_assign.timeremaining": "assign", |   "addon.mod_assign.timeremaining": "assign", | ||||||
|   "addon.mod_assign.ungroupedusers": "assign", |   "addon.mod_assign.ungroupedusers": "assign", | ||||||
|  | |||||||
| @ -11,7 +11,7 @@ | |||||||
|     <!-- Activity info. --> |     <!-- Activity info. --> | ||||||
|     <core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId" |     <core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId" | ||||||
|         [courseId]="courseId" [hasDataToSync]="hasOffline" (completionChanged)="onCompletionChange()"> |         [courseId]="courseId" [hasDataToSync]="hasOffline" (completionChanged)="onCompletionChange()"> | ||||||
|         <ion-list inset="true" description *ngIf="assign && assign.introattachments && assign.introattachments.length"> |         <ion-list inset="true" description *ngIf="assign && assign.introattachments?.length && !assign.submissionattachments"> | ||||||
|             <core-file *ngFor="let file of assign.introattachments" [file]="file" [component]="component" [componentId]="componentId"> |             <core-file *ngFor="let file of assign.introattachments" [file]="file" [component]="component" [componentId]="componentId"> | ||||||
|             </core-file> |             </core-file> | ||||||
|         </ion-list> |         </ion-list> | ||||||
| @ -33,6 +33,14 @@ | |||||||
|                 </ion-select> |                 </ion-select> | ||||||
|             </ion-item> |             </ion-item> | ||||||
| 
 | 
 | ||||||
|  |             <ion-item class="ion-text-wrap"> | ||||||
|  |                 <ion-label> | ||||||
|  |                     <h2>{{ 'core.course.hiddenfromstudents' | translate }}</h2> | ||||||
|  |                     <p *ngIf="module.visible">{{ 'core.no' | translate }}</p> | ||||||
|  |                     <p *ngIf="!module.visible">{{ 'core.yes' | translate }}</p> | ||||||
|  |                 </ion-label> | ||||||
|  |             </ion-item> | ||||||
|  | 
 | ||||||
|             <ion-item class="ion-text-wrap" *ngIf="timeRemaining"> |             <ion-item class="ion-text-wrap" *ngIf="timeRemaining"> | ||||||
|                 <ion-label> |                 <ion-label> | ||||||
|                     <h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2> |                     <h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2> | ||||||
|  | |||||||
| @ -26,6 +26,7 @@ import { CoreTimeUtils } from '@services/utils/time'; | |||||||
| import { CoreUtils } from '@services/utils/utils'; | import { CoreUtils } from '@services/utils/utils'; | ||||||
| import { Translate } from '@singletons'; | import { Translate } from '@singletons'; | ||||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||||
|  | import { CoreTime } from '@singletons/time'; | ||||||
| import { AddonModAssignListFilterName } from '../../classes/submissions-source'; | import { AddonModAssignListFilterName } from '../../classes/submissions-source'; | ||||||
| import { | import { | ||||||
|     AddonModAssign, |     AddonModAssign, | ||||||
| @ -86,6 +87,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo | |||||||
|     protected savedObserver?: CoreEventObserver; |     protected savedObserver?: CoreEventObserver; | ||||||
|     protected submittedObserver?: CoreEventObserver; |     protected submittedObserver?: CoreEventObserver; | ||||||
|     protected gradedObserver?: CoreEventObserver; |     protected gradedObserver?: CoreEventObserver; | ||||||
|  |     protected startedObserver?: CoreEventObserver; | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         protected content?: IonContent, |         protected content?: IonContent, | ||||||
| @ -136,6 +138,13 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo | |||||||
|             } |             } | ||||||
|         }, this.siteId); |         }, this.siteId); | ||||||
| 
 | 
 | ||||||
|  |         this.startedObserver = CoreEvents.on(AddonModAssignProvider.STARTED_EVENT, (data) => { | ||||||
|  |             if (this.assign && data.assignmentId == this.assign.id) { | ||||||
|  |                 // Assignment submission started, refresh data.
 | ||||||
|  |                 this.showLoadingAndRefresh(false, false); | ||||||
|  |             } | ||||||
|  |         }, this.siteId); | ||||||
|  | 
 | ||||||
|         await this.loadContent(false, true); |         await this.loadContent(false, true); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -167,12 +176,17 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo | |||||||
|         if (submissions.canviewsubmissions) { |         if (submissions.canviewsubmissions) { | ||||||
| 
 | 
 | ||||||
|             // Calculate the messages to display about time remaining and late submissions.
 |             // Calculate the messages to display about time remaining and late submissions.
 | ||||||
|  |             this.timeRemaining = ''; | ||||||
|  |             this.lateSubmissions = ''; | ||||||
|  | 
 | ||||||
|             if (this.assign.duedate > 0) { |             if (this.assign.duedate > 0) { | ||||||
|                 if (this.assign.duedate - time <= 0) { |                 if (this.assign.duedate - time <= 0) { | ||||||
|                     this.timeRemaining = Translate.instant('addon.mod_assign.assignmentisdue'); |                     this.timeRemaining = Translate.instant('addon.mod_assign.assignmentisdue'); | ||||||
|                 } else { |                 } else { | ||||||
|                     this.timeRemaining = CoreTimeUtils.formatDuration(this.assign.duedate - time, 3); |                     this.timeRemaining = CoreTime.formatTime(this.assign.duedate - time); | ||||||
|  |                 } | ||||||
| 
 | 
 | ||||||
|  |                 if (this.assign.duedate < time) { | ||||||
|                     if (this.assign.cutoffdate) { |                     if (this.assign.cutoffdate) { | ||||||
|                         if (this.assign.cutoffdate > time) { |                         if (this.assign.cutoffdate > time) { | ||||||
|                             this.lateSubmissions = Translate.instant( |                             this.lateSubmissions = Translate.instant( | ||||||
| @ -182,13 +196,8 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo | |||||||
|                         } else { |                         } else { | ||||||
|                             this.lateSubmissions = Translate.instant('addon.mod_assign.nomoresubmissionsaccepted'); |                             this.lateSubmissions = Translate.instant('addon.mod_assign.nomoresubmissionsaccepted'); | ||||||
|                         } |                         } | ||||||
|                     } else { |  | ||||||
|                         this.lateSubmissions = ''; |  | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } else { |  | ||||||
|                 this.timeRemaining = ''; |  | ||||||
|                 this.lateSubmissions = ''; |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // Check if groupmode is enabled to avoid showing wrong numbers.
 |             // Check if groupmode is enabled to avoid showing wrong numbers.
 | ||||||
| @ -398,6 +407,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo | |||||||
|         this.savedObserver?.off(); |         this.savedObserver?.off(); | ||||||
|         this.submittedObserver?.off(); |         this.submittedObserver?.off(); | ||||||
|         this.gradedObserver?.off(); |         this.gradedObserver?.off(); | ||||||
|  |         this.startedObserver?.off(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,5 +1,15 @@ | |||||||
| <core-loading [hideUntil]="loaded"> | <core-loading [hideUntil]="loaded"> | ||||||
|     <div class="list-item-limited-width"> |     <div class="list-item-limited-width"> | ||||||
|  |         <!-- Time limit is over. --> | ||||||
|  |         <ion-card *ngIf="timeLimitFinished" class="core-danger-card"> | ||||||
|  |             <ion-item class="ion-text-wrap"> | ||||||
|  |                 <ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon> | ||||||
|  |                 <ion-label> | ||||||
|  |                     <p>{{ 'addon.mod_assign.caneditsubmission' | translate }}</p> | ||||||
|  |                 </ion-label> | ||||||
|  |             </ion-item> | ||||||
|  |         </ion-card> | ||||||
|  | 
 | ||||||
|         <!-- User and status of the submission. --> |         <!-- User and status of the submission. --> | ||||||
|         <ion-item class="ion-text-wrap" *ngIf="!blindMarking && user" core-user-link [userId]="submitId" [courseId]="courseId" |         <ion-item class="ion-text-wrap" *ngIf="!blindMarking && user" core-user-link [userId]="submitId" [courseId]="courseId" | ||||||
|             [attr.aria-label]="user!.fullname"> |             [attr.aria-label]="user!.fullname"> | ||||||
| @ -31,26 +41,29 @@ | |||||||
|             <!-- View the submission tab. --> |             <!-- View the submission tab. --> | ||||||
|             <core-tab [title]="'addon.mod_assign.submission' | translate" id="submission"> |             <core-tab [title]="'addon.mod_assign.submission' | translate" id="submission"> | ||||||
|                 <ng-template> |                 <ng-template> | ||||||
|                     <addon-mod-assign-submission-plugin *ngFor="let plugin of submissionPlugins" [assign]="assign" |  | ||||||
|                         [submission]="userSubmission" [plugin]="plugin"> |  | ||||||
|                     </addon-mod-assign-submission-plugin> |  | ||||||
| 
 |  | ||||||
|                     <!-- Render some data about the submission. --> |                     <!-- Render some data about the submission. --> | ||||||
|                     <ion-item class="ion-text-wrap" |                     <ion-item class="ion-text-wrap" *ngIf="currentAttempt && !isGrading"> | ||||||
|                         *ngIf="userSubmission && userSubmission!.status != statusNew && userSubmission!.timemodified"> |  | ||||||
|                         <ion-label> |                         <ion-label> | ||||||
|                             <h2>{{ 'addon.mod_assign.timemodified' | translate }}</h2> |                             <h2>{{ 'addon.mod_assign.attemptnumber' | translate }}</h2> | ||||||
|                             <p>{{ userSubmission!.timemodified * 1000 | coreFormatDate }}</p> |                             <p *ngIf="assign!.maxattempts == unlimitedAttempts"> | ||||||
|  |                                 {{ 'addon.mod_assign.outof' | translate : | ||||||
|  |                                 {'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }} | ||||||
|  |                             </p> | ||||||
|  |                             <p *ngIf="assign!.maxattempts != unlimitedAttempts"> | ||||||
|  |                                 {{ 'addon.mod_assign.outof' | translate : | ||||||
|  |                                 {'$a': {'current': currentAttempt, 'total': assign!.maxattempts} } }} | ||||||
|  |                             </p> | ||||||
|                         </ion-label> |                         </ion-label> | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
| 
 | 
 | ||||||
|                     <ion-item class="ion-text-wrap" *ngIf="timeRemaining" [ngClass]="[timeRemainingClass]"> |                     <!-- Submission is locked. --> | ||||||
|  |                     <ion-item class="ion-text-wrap" *ngIf="lastAttempt?.locked"> | ||||||
|                         <ion-label> |                         <ion-label> | ||||||
|                             <h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2> |                             <h2>{{ 'addon.mod_assign.submissionslocked' | translate }}</h2> | ||||||
|                             <p [innerHTML]="timeRemaining"></p> |  | ||||||
|                         </ion-label> |                         </ion-label> | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
| 
 | 
 | ||||||
|  |                     <!-- Dates. --> | ||||||
|                     <ion-item class="ion-text-wrap" *ngIf="showDates && fromDate && !isSubmittedForGrading"> |                     <ion-item class="ion-text-wrap" *ngIf="showDates && fromDate && !isSubmittedForGrading"> | ||||||
|                         <ion-label> |                         <ion-label> | ||||||
|                             <p *ngIf="assign!.intro" |                             <p *ngIf="assign!.intro" | ||||||
| @ -84,20 +97,48 @@ | |||||||
|                         </ion-label> |                         </ion-label> | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
| 
 | 
 | ||||||
|                     <ion-item class="ion-text-wrap" *ngIf="currentAttempt && !isGrading"> |                     <!-- Time remaining. --> | ||||||
|  |                     <ion-item class="ion-text-wrap" *ngIf="timeRemaining || timeLimitEndTime > 0" [ngClass]="[timeRemainingClass]"> | ||||||
|                         <ion-label> |                         <ion-label> | ||||||
|                             <h2>{{ 'addon.mod_assign.attemptnumber' | translate }}</h2> |                             <h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2> | ||||||
|                             <p *ngIf="assign!.maxattempts == unlimitedAttempts"> |                             <p *ngIf="!timeLimitEndTime" [innerHTML]="timeRemaining"></p> | ||||||
|                                 {{ 'addon.mod_assign.outof' | translate : |                             <core-timer *ngIf="timeLimitEndTime > 0" [endTime]="timeLimitEndTime" mode="basic" timeUpText="00:00:00" | ||||||
|                                 {'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }} |                                 [timeLeftClassThreshold]="-1" [underTimeClassThresholds]="[300, 900]" (finished)="timeUp()"> | ||||||
|                             </p> |                             </core-timer> | ||||||
|                             <p *ngIf="assign!.maxattempts != unlimitedAttempts"> |  | ||||||
|                                 {{ 'addon.mod_assign.outof' | translate : |  | ||||||
|                                 {'$a': {'current': currentAttempt, 'total': assign!.maxattempts} } }} |  | ||||||
|                             </p> |  | ||||||
|                         </ion-label> |                         </ion-label> | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
| 
 | 
 | ||||||
|  |                     <!-- Time limit. --> | ||||||
|  |                     <ion-item class="ion-text-wrap" *ngIf="assign && assign.timelimit"> | ||||||
|  |                         <ion-label> | ||||||
|  |                             <h2>{{ 'addon.mod_assign.timelimit' | translate }}</h2> | ||||||
|  |                             <p>{{ assign.timelimit | coreDuration }}</p> | ||||||
|  |                         </ion-label> | ||||||
|  |                     </ion-item> | ||||||
|  | 
 | ||||||
|  |                     <!-- Editing status. --> | ||||||
|  |                     <ion-item class="ion-text-wrap" *ngIf="lastAttempt && isSubmittedForGrading && lastAttempt!.caneditowner !== undefined" | ||||||
|  |                         [ngClass]="{submissioneditable: lastAttempt!.caneditowner, submissionnoteditable: !lastAttempt!.caneditowner}"> | ||||||
|  |                         <ion-label> | ||||||
|  |                             <h2>{{ 'addon.mod_assign.editingstatus' | translate }}</h2> | ||||||
|  |                             <p *ngIf="lastAttempt!.caneditowner">{{ 'addon.mod_assign.submissioneditable' | translate }}</p> | ||||||
|  |                             <p *ngIf="!lastAttempt!.caneditowner">{{ 'addon.mod_assign.submissionnoteditable' | translate }}</p> | ||||||
|  |                         </ion-label> | ||||||
|  |                     </ion-item> | ||||||
|  | 
 | ||||||
|  |                     <!-- Last modified. --> | ||||||
|  |                     <ion-item class="ion-text-wrap" | ||||||
|  |                         *ngIf="userSubmission && userSubmission!.status != statusNew && userSubmission!.timemodified"> | ||||||
|  |                         <ion-label> | ||||||
|  |                             <h2>{{ 'addon.mod_assign.timemodified' | translate }}</h2> | ||||||
|  |                             <p>{{ userSubmission!.timemodified * 1000 | coreFormatDate }}</p> | ||||||
|  |                         </ion-label> | ||||||
|  |                     </ion-item> | ||||||
|  | 
 | ||||||
|  |                     <addon-mod-assign-submission-plugin *ngFor="let plugin of submissionPlugins" [assign]="assign" | ||||||
|  |                         [submission]="userSubmission" [plugin]="plugin"> | ||||||
|  |                     </addon-mod-assign-submission-plugin> | ||||||
|  | 
 | ||||||
|                     <!-- Add or edit submission. --> |                     <!-- Add or edit submission. --> | ||||||
|                     <ion-item class="ion-text-wrap" *ngIf="canEdit"> |                     <ion-item class="ion-text-wrap" *ngIf="canEdit"> | ||||||
|                         <ion-label> |                         <ion-label> | ||||||
| @ -109,7 +150,12 @@ | |||||||
|                                 <!-- If no submission or is new, show add submission. --> |                                 <!-- If no submission or is new, show add submission. --> | ||||||
|                                 <ion-button expand="block" class="ion-text-wrap" (click)="goToEdit()" *ngIf="!hasOffline && |                                 <ion-button expand="block" class="ion-text-wrap" (click)="goToEdit()" *ngIf="!hasOffline && | ||||||
|                                     (!userSubmission || !userSubmission!.status || userSubmission!.status == statusNew)"> |                                     (!userSubmission || !userSubmission!.status || userSubmission!.status == statusNew)"> | ||||||
|  |                                     <ng-container *ngIf="!assign?.timelimit || userSubmission?.timestarted"> | ||||||
|                                         {{ 'addon.mod_assign.addsubmission' | translate }} |                                         {{ 'addon.mod_assign.addsubmission' | translate }} | ||||||
|  |                                     </ng-container> | ||||||
|  |                                     <ng-container *ngIf="assign?.timelimit && (!userSubmission || !userSubmission.timestarted)"> | ||||||
|  |                                         {{ 'addon.mod_assign.beginassignment' | translate }} | ||||||
|  |                                     </ng-container> | ||||||
|                                 </ion-button> |                                 </ion-button> | ||||||
|                                 <!-- If reopened, show addfromprevious and addnewattempt. --> |                                 <!-- If reopened, show addfromprevious and addnewattempt. --> | ||||||
|                                 <ng-container *ngIf="!hasOffline && userSubmission?.status == statusReopened"> |                                 <ng-container *ngIf="!hasOffline && userSubmission?.status == statusReopened"> | ||||||
| @ -122,9 +168,8 @@ | |||||||
|                                     </ion-button> |                                     </ion-button> | ||||||
|                                 </ng-container> |                                 </ng-container> | ||||||
|                                 <!-- Else show editsubmission. --> |                                 <!-- Else show editsubmission. --> | ||||||
|                                 <ion-button expand="block" class="ion-text-wrap" *ngIf="!hasOffline && |                                 <ion-button expand="block" class="ion-text-wrap" *ngIf="!hasOffline && userSubmission && | ||||||
|                                     userSubmission && userSubmission!.status && |                                     userSubmission!.status && userSubmission!.status != statusNew && | ||||||
|                                     userSubmission!.status != statusNew && |  | ||||||
|                                     userSubmission!.status != statusReopened" (click)="goToEdit()"> |                                     userSubmission!.status != statusReopened" (click)="goToEdit()"> | ||||||
|                                     {{ 'addon.mod_assign.editsubmission' | translate }} |                                     {{ 'addon.mod_assign.editsubmission' | translate }} | ||||||
|                                 </ion-button> |                                 </ion-button> | ||||||
| @ -191,23 +236,6 @@ | |||||||
|                             </ion-item> |                             </ion-item> | ||||||
|                         </ng-container> |                         </ng-container> | ||||||
|                     </ng-container> |                     </ng-container> | ||||||
| 
 |  | ||||||
|                     <!-- Submission is locked. --> |  | ||||||
|                     <ion-item class="ion-text-wrap" *ngIf="lastAttempt?.locked"> |  | ||||||
|                         <ion-label> |  | ||||||
|                             <h2>{{ 'addon.mod_assign.submissionslocked' | translate }}</h2> |  | ||||||
|                         </ion-label> |  | ||||||
|                     </ion-item> |  | ||||||
| 
 |  | ||||||
|                     <!-- Editing status. --> |  | ||||||
|                     <ion-item class="ion-text-wrap" *ngIf="lastAttempt && isSubmittedForGrading && lastAttempt!.caneditowner !== undefined" |  | ||||||
|                         [ngClass]="{submissioneditable: lastAttempt!.caneditowner, submissionnoteditable: !lastAttempt!.caneditowner}"> |  | ||||||
|                         <ion-label> |  | ||||||
|                             <h2>{{ 'addon.mod_assign.editingstatus' | translate }}</h2> |  | ||||||
|                             <p *ngIf="lastAttempt!.caneditowner">{{ 'addon.mod_assign.submissioneditable' | translate }}</p> |  | ||||||
|                             <p *ngIf="!lastAttempt!.caneditowner">{{ 'addon.mod_assign.submissionnoteditable' | translate }}</p> |  | ||||||
|                         </ion-label> |  | ||||||
|                     </ion-item> |  | ||||||
|                 </ng-template> |                 </ng-template> | ||||||
|             </core-tab> |             </core-tab> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,20 +1,20 @@ | |||||||
| :host ::ng-deep { | :host ::ng-deep { | ||||||
|     div.latesubmission, |     ion-item.latesubmission, | ||||||
|     div.overdue { |     ion-item.overdue { | ||||||
|         border-bottom: 3px solid var(--danger) !important; |         border-bottom: 3px solid var(--danger) !important; | ||||||
|         ion-icon { |         ion-icon { | ||||||
|             color: var(--danger); |             color: var(--danger); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     div.earlysubmission { |     ion-item.earlysubmission { | ||||||
|         border-bottom: 3px solid var(--success) !important; |         border-bottom: 3px solid var(--success) !important; | ||||||
|         ion-icon { |         ion-icon { | ||||||
|             color: var(--success); |             color: var(--success); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     div.submissioneditable p { |     ion-item.submissioneditable p { | ||||||
|         color: var(--danger); |         color: var(--danger); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -26,10 +26,22 @@ | |||||||
|         margin-left: 2px; |         margin-left: 2px; | ||||||
|         margin-right: 2px; |         margin-right: 2px; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     core-timer .core-timer { | ||||||
|  |         &.core-timer-under-300 { | ||||||
|  |             background-color: var(--danger-tint); | ||||||
|  |             color: var(--danger-shade); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         &.core-timer-under-900 { | ||||||
|  |             background-color: var(--warning-tint); | ||||||
|  |             color: var(--warning-shade); | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| :host-context(body.dark) ::ng-deep { | :host-context(body.dark) ::ng-deep { | ||||||
|     div.submissioneditable p { |     ion-item.submissioneditable p { | ||||||
|         color: var(--danger-tint); |         color: var(--danger-tint); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -58,6 +58,7 @@ import { CoreSync } from '@services/sync'; | |||||||
| import { AddonModAssignSubmissionPluginComponent } from '../submission-plugin/submission-plugin'; | import { AddonModAssignSubmissionPluginComponent } from '../submission-plugin/submission-plugin'; | ||||||
| import { AddonModAssignModuleHandlerService } from '../../services/handlers/module'; | import { AddonModAssignModuleHandlerService } from '../../services/handlers/module'; | ||||||
| import { CanLeave } from '@guards/can-leave'; | import { CanLeave } from '@guards/can-leave'; | ||||||
|  | import { CoreTime } from '@singletons/time'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Component that displays an assignment submission. |  * Component that displays an assignment submission. | ||||||
| @ -106,6 +107,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can | |||||||
|     submissionPlugins: AddonModAssignPlugin[] = []; // List of submission plugins.
 |     submissionPlugins: AddonModAssignPlugin[] = []; // List of submission plugins.
 | ||||||
|     timeRemaining = ''; // Message about time remaining.
 |     timeRemaining = ''; // Message about time remaining.
 | ||||||
|     timeRemainingClass = ''; // Class to apply to time remaining message.
 |     timeRemainingClass = ''; // Class to apply to time remaining message.
 | ||||||
|  |     timeLimitEndTime = 0; // If time limit is enabled and submission is ongoing, the end time for the timer.
 | ||||||
|     statusTranslated?: string; // Status.
 |     statusTranslated?: string; // Status.
 | ||||||
|     statusColor = ''; // Color to apply to the status.
 |     statusColor = ''; // Color to apply to the status.
 | ||||||
|     unsupportedEditPlugins: string[] = []; // List of submission plugins that don't support edit.
 |     unsupportedEditPlugins: string[] = []; // List of submission plugins that don't support edit.
 | ||||||
| @ -126,6 +128,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can | |||||||
|     gradeUrl?: string; // URL to grade in browser.
 |     gradeUrl?: string; // URL to grade in browser.
 | ||||||
|     isPreviousAttemptEmpty = true; // Whether the previous attempt contains an empty submission.
 |     isPreviousAttemptEmpty = true; // Whether the previous attempt contains an empty submission.
 | ||||||
|     showDates = false; // Whether to show some dates.
 |     showDates = false; // Whether to show some dates.
 | ||||||
|  |     timeLimitFinished = false; // Whether there is a time limit and it finished, so the user will submit late.
 | ||||||
| 
 | 
 | ||||||
|     // Some constants.
 |     // Some constants.
 | ||||||
|     statusNew = AddonModAssignSubmissionStatusValues.NEW; |     statusNew = AddonModAssignSubmissionStatusValues.NEW; | ||||||
| @ -200,7 +203,12 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can | |||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (this.assign.duedate <= 0) { |         const submissionStarted = !!this.userSubmission?.timestarted; | ||||||
|  |         this.timeLimitEndTime = 0; | ||||||
|  |         this.timeLimitFinished = false; | ||||||
|  | 
 | ||||||
|  |         if (this.assign.duedate <= 0 && !submissionStarted) { | ||||||
|  |             // No due date and no countdown.
 | ||||||
|             this.timeRemaining = ''; |             this.timeRemaining = ''; | ||||||
|             this.timeRemainingClass = ''; |             this.timeRemainingClass = ''; | ||||||
| 
 | 
 | ||||||
| @ -208,53 +216,53 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const time = CoreTimeUtils.timestamp(); |         const time = CoreTimeUtils.timestamp(); | ||||||
|         const dueDate = response.lastattempt?.extensionduedate |         const timeLimitEnabled = this.assign.timelimit && submissionStarted; | ||||||
|             ? response.lastattempt.extensionduedate |         const dueDateReached = this.assign.duedate > 0 && this.assign.duedate - time <= 0; | ||||||
|             : this.assign.duedate; |         const timeLimitEnabledBeforeDueDate = timeLimitEnabled && !dueDateReached; | ||||||
|         const timeRemaining = dueDate - time; |  | ||||||
| 
 | 
 | ||||||
|         if (timeRemaining > 0) { |         if (this.userSubmission && this.userSubmission.status === AddonModAssignSubmissionStatusValues.SUBMITTED) { | ||||||
|             this.timeRemaining = CoreTimeUtils.formatDuration(timeRemaining, 3); |             // Submitted, display the relevant early/late message.
 | ||||||
|             this.timeRemainingClass = ''; |             const lateCalculation = this.userSubmission.timemodified - | ||||||
|  |                 (timeLimitEnabledBeforeDueDate ? this.userSubmission.timecreated : 0); | ||||||
|  |             const lateThreshold = timeLimitEnabledBeforeDueDate ? this.assign.timelimit || 0 : this.assign.duedate; | ||||||
|  |             const earlyString = timeLimitEnabledBeforeDueDate ? 'submittedundertime' : 'submittedearly'; | ||||||
|  |             const lateString = timeLimitEnabledBeforeDueDate ? 'submittedovertime' : 'submittedlate'; | ||||||
|  |             const onTime = lateCalculation <= lateThreshold; | ||||||
|  | 
 | ||||||
|  |             this.timeRemaining = Translate.instant( | ||||||
|  |                 'addon.mod_assign.' + (onTime ? earlyString : lateString), | ||||||
|  |                 { $a: CoreTime.formatTime(Math.abs(lateCalculation - lateThreshold)) }, | ||||||
|  |             ); | ||||||
|  |             this.timeRemainingClass = onTime ? 'earlysubmission' : 'latesubmission'; | ||||||
| 
 | 
 | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Not submitted.
 |         if (dueDateReached) { | ||||||
|         if (!this.userSubmission || this.userSubmission.status != AddonModAssignSubmissionStatusValues.SUBMITTED) { |             // There is no submission, due date has passed, show assignment is overdue.
 | ||||||
| 
 |             const submissionsEnabled = response.lastattempt?.submissionsenabled || response.gradingsummary?.submissionsenabled; | ||||||
|             if (response.lastattempt?.submissionsenabled || response.gradingsummary?.submissionsenabled) { |  | ||||||
|             this.timeRemaining = Translate.instant( |             this.timeRemaining = Translate.instant( | ||||||
|                     'addon.mod_assign.overdue', |                 'addon.mod_assign.' + (submissionsEnabled ? 'overdue' : 'duedatereached'), | ||||||
|                     { $a: CoreTimeUtils.formatDuration(-timeRemaining, 3) }, |                 { $a: CoreTime.formatTime(time - this.assign.duedate) }, | ||||||
|             ); |             ); | ||||||
|             this.timeRemainingClass = 'overdue'; |             this.timeRemainingClass = 'overdue'; | ||||||
|  |             this.timeLimitFinished = true; | ||||||
| 
 | 
 | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|             this.timeRemaining = Translate.instant('addon.mod_assign.duedatereached'); |         if (timeLimitEnabled && submissionStarted) { | ||||||
|             this.timeRemainingClass = ''; |             // An attempt has started and there is a time limit, display the time limit.
 | ||||||
|  |             this.timeRemaining = ''; | ||||||
|  |             this.timeRemainingClass = 'timeremaining'; | ||||||
|  |             this.timeLimitEndTime = AddonModAssignHelper.calculateEndTime(this.assign, this.userSubmission); | ||||||
| 
 | 
 | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const timeSubmittedDiff = this.userSubmission.timemodified - dueDate; |         // Assignment is not overdue, and no submission has been made. Just display the due date.
 | ||||||
|         if (timeSubmittedDiff > 0) { |         this.timeRemaining = CoreTime.formatTime(this.assign.duedate - time); | ||||||
|             this.timeRemaining = Translate.instant( |         this.timeRemainingClass = 'timeremaining'; | ||||||
|                 'addon.mod_assign.submittedlate', |  | ||||||
|                 { $a: CoreTimeUtils.formatDuration(timeSubmittedDiff, 2) }, |  | ||||||
|             ); |  | ||||||
|             this.timeRemainingClass = 'latesubmission'; |  | ||||||
| 
 |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         this.timeRemaining = Translate.instant( |  | ||||||
|             'addon.mod_assign.submittedearly', |  | ||||||
|             { $a: CoreTimeUtils.formatDuration(-timeSubmittedDiff, 2) }, |  | ||||||
|         ); |  | ||||||
|         this.timeRemainingClass = 'earlysubmission'; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -292,7 +300,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can | |||||||
| 
 | 
 | ||||||
|         if (!this.previousAttempt?.submission) { |         if (!this.previousAttempt?.submission) { | ||||||
|             // Cannot access previous attempts, just go to edit.
 |             // Cannot access previous attempts, just go to edit.
 | ||||||
|             return this.goToEdit(); |             return this.goToEdit(true); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const previousSubmission = this.previousAttempt.submission; |         const previousSubmission = this.previousAttempt.submission; | ||||||
| @ -319,7 +327,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can | |||||||
|         try { |         try { | ||||||
|             await AddonModAssignHelper.copyPreviousAttempt(this.assign, previousSubmission); |             await AddonModAssignHelper.copyPreviousAttempt(this.assign, previousSubmission); | ||||||
|             // Now go to edit.
 |             // Now go to edit.
 | ||||||
|             this.goToEdit(); |             this.goToEdit(true); | ||||||
| 
 | 
 | ||||||
|             if (!this.assign.submissiondrafts && this.userSubmission) { |             if (!this.assign.submissiondrafts && this.userSubmission) { | ||||||
|                 // No drafts allowed, so it was submitted. Trigger event.
 |                 // No drafts allowed, so it was submitted. Trigger event.
 | ||||||
| @ -352,8 +360,24 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can | |||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Go to the page to add or edit submission. |      * Go to the page to add or edit submission. | ||||||
|  |      * | ||||||
|  |      * @param afterCopyPrevious Whether the user has just copied the previous submission. | ||||||
|      */ |      */ | ||||||
|     goToEdit(): void { |     async goToEdit(afterCopyPrevious = false): Promise<void> { | ||||||
|  |         if (!afterCopyPrevious && this.assign?.timelimit && (!this.userSubmission || !this.userSubmission.timestarted)) { | ||||||
|  |             try { | ||||||
|  |                 await CoreDomUtils.showConfirm( | ||||||
|  |                     Translate.instant('addon.mod_assign.confirmstart', { | ||||||
|  |                         $a: CoreTime.formatTime(this.assign.timelimit), | ||||||
|  |                     }), | ||||||
|  |                     undefined, | ||||||
|  |                     Translate.instant('addon.mod_assign.beginassignment'), | ||||||
|  |                 ); | ||||||
|  |             } catch { | ||||||
|  |                 return; // User canceled.
 | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         CoreNavigator.navigateToSitePath( |         CoreNavigator.navigateToSitePath( | ||||||
|             AddonModAssignModuleHandlerService.PAGE_NAME + '/' + this.courseId + '/' + this.moduleId + '/edit', |             AddonModAssignModuleHandlerService.PAGE_NAME + '/' + this.courseId + '/' + this.moduleId + '/edit', | ||||||
|             { |             { | ||||||
| @ -1175,6 +1199,13 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can | |||||||
|         this.setGradeSyncBlocked(tab.id === 'grade'); |         this.setGradeSyncBlocked(tab.id === 'grade'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Function called when the time is up. | ||||||
|  |      */ | ||||||
|  |     timeUp(): void { | ||||||
|  |         this.timeLimitFinished = true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Component being destroyed. |      * Component being destroyed. | ||||||
|      */ |      */ | ||||||
|  | |||||||
| @ -9,14 +9,18 @@ | |||||||
|     "allowsubmissionsfromdatesummary": "This assignment will accept submissions from <strong>{{$a}}</strong>", |     "allowsubmissionsfromdatesummary": "This assignment will accept submissions from <strong>{{$a}}</strong>", | ||||||
|     "applytoteam": "Apply grades and feedback to entire group", |     "applytoteam": "Apply grades and feedback to entire group", | ||||||
|     "assignmentisdue": "Assignment is due", |     "assignmentisdue": "Assignment is due", | ||||||
|  |     "assigntimeleft": "Time left", | ||||||
|     "attemptnumber": "Attempt number", |     "attemptnumber": "Attempt number", | ||||||
|     "attemptreopenmethod": "Additional attempts", |     "attemptreopenmethod": "Additional attempts", | ||||||
|     "attemptreopenmethod_manual": "Manually", |     "attemptreopenmethod_manual": "Manually", | ||||||
|     "attemptreopenmethod_untilpass": "Automatically until pass", |     "attemptreopenmethod_untilpass": "Automatically until pass", | ||||||
|     "attemptsettings": "Attempt settings", |     "attemptsettings": "Attempt settings", | ||||||
|  |     "beginassignment": "Begin assignment", | ||||||
|  |     "caneditsubmission": "You can submit/edit submission after time limit passed, but it will be marked as late.", | ||||||
|     "cannoteditduetostatementsubmission": "You can't add or edit a submission in the app because the submission statement could not be retrieved from the site.", |     "cannoteditduetostatementsubmission": "You can't add or edit a submission in the app because the submission statement could not be retrieved from the site.", | ||||||
|     "cannotgradefromapp": "Certain grading methods are not yet supported by the app and cannot be modified.", |     "cannotgradefromapp": "Certain grading methods are not yet supported by the app and cannot be modified.", | ||||||
|     "cannotsubmitduetostatementsubmission": "You can't make a submission in the app because the submission statement could not be retrieved from the site.", |     "cannotsubmitduetostatementsubmission": "You can't make a submission in the app because the submission statement could not be retrieved from the site.", | ||||||
|  |     "confirmstart": "Your submission will have a time limit of {{$a}}. When you start, the timer will begin to count down and cannot be paused. You must finish your submission before it expires. Are you sure you wish to start now?", | ||||||
|     "confirmsubmission": "Are you sure you want to submit your work for grading? You will not be able to make any more changes.", |     "confirmsubmission": "Are you sure you want to submit your work for grading? You will not be able to make any more changes.", | ||||||
|     "currentattempt": "This is attempt {{$a}}.", |     "currentattempt": "This is attempt {{$a}}.", | ||||||
|     "currentattemptof": "This is attempt {{$a.attemptnumber}} ( {{$a.maxattempts}} attempts allowed ).", |     "currentattemptof": "This is attempt {{$a.attemptnumber}} ( {{$a.maxattempts}} attempts allowed ).", | ||||||
| @ -94,7 +98,10 @@ | |||||||
|     "submitassignment_help": "Once this assignment is submitted you will not be able to make any more changes.", |     "submitassignment_help": "Once this assignment is submitted you will not be able to make any more changes.", | ||||||
|     "submittedearly": "Assignment was submitted {{$a}} early", |     "submittedearly": "Assignment was submitted {{$a}} early", | ||||||
|     "submittedlate": "Assignment was submitted {{$a}} late", |     "submittedlate": "Assignment was submitted {{$a}} late", | ||||||
|  |     "submittedovertime": "Assignment was submitted {{$a}} over the time limit", | ||||||
|  |     "submittedundertime": "Assignment was submitted {{$a}} under the time limit", | ||||||
|     "syncblockedusercomponent": "user grade", |     "syncblockedusercomponent": "user grade", | ||||||
|  |     "timelimit": "Time limit", | ||||||
|     "timemodified": "Last modified", |     "timemodified": "Last modified", | ||||||
|     "timeremaining": "Time remaining", |     "timeremaining": "Time remaining", | ||||||
|     "ungroupedusers": "The setting 'Require group to make submission' is enabled and some users are either not a member of any group, or are a member of more than one group, so are unable to make submissions.", |     "ungroupedusers": "The setting 'Require group to make submission' is enabled and some users are either not a member of any group, or are a member of more than one group, so are unable to make submissions.", | ||||||
|  | |||||||
| @ -20,6 +20,25 @@ | |||||||
|     <core-loading [hideUntil]="loaded"> |     <core-loading [hideUntil]="loaded"> | ||||||
|         <ion-list *ngIf="userSubmission && userSubmission.plugins && userSubmission.plugins.length"> |         <ion-list *ngIf="userSubmission && userSubmission.plugins && userSubmission.plugins.length"> | ||||||
|             <!-- @todo: plagiarism_print_disclosure --> |             <!-- @todo: plagiarism_print_disclosure --> | ||||||
|  |             <core-timer *ngIf="timeLimitEndTime > 0" [endTime]="timeLimitEndTime" (finished)="timeUp()" timeUpText="00:00:00" | ||||||
|  |                 [timerText]="'addon.mod_assign.assigntimeleft' | translate" [align]="'center'" [timeLeftClassThreshold]="-1" | ||||||
|  |                 [underTimeClassThresholds]="[300, 900]" class="ion-margin-horizontal"> | ||||||
|  |             </core-timer> | ||||||
|  | 
 | ||||||
|  |             <!-- Assign activity instructions and attachments if needed. --> | ||||||
|  |             <ion-item class="ion-text-wrap" *ngIf="activityInstructions"> | ||||||
|  |                 <ion-label> | ||||||
|  |                     <core-format-text [text]="activityInstructions" [component]="component" [componentId]="moduleId" contextLevel="module" | ||||||
|  |                         [contextInstanceId]="moduleId" [courseId]="courseId" [maxHeight]="120"> | ||||||
|  |                     </core-format-text> | ||||||
|  |                 </ion-label> | ||||||
|  |             </ion-item> | ||||||
|  | 
 | ||||||
|  |             <ng-container *ngIf="assign?.submissionattachments"> | ||||||
|  |                 <core-file *ngFor="let file of introAttachments" [file]="file" [component]="component" [componentId]="moduleId"> | ||||||
|  |                 </core-file> | ||||||
|  |             </ng-container> | ||||||
|  | 
 | ||||||
|             <form name="addon-mod_assign-edit-form" #editSubmissionForm> |             <form name="addon-mod_assign-edit-form" #editSubmissionForm> | ||||||
|                 <!-- Submission statement. --> |                 <!-- Submission statement. --> | ||||||
|                 <ion-item class="ion-text-wrap" *ngIf="submissionStatement"> |                 <ion-item class="ion-text-wrap" *ngIf="submissionStatement"> | ||||||
|  | |||||||
							
								
								
									
										17
									
								
								src/addons/mod/assign/pages/edit/edit.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/addons/mod/assign/pages/edit/edit.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | |||||||
|  | :host ::ng-deep { | ||||||
|  |     core-timer { | ||||||
|  |         display: block; | ||||||
|  | 
 | ||||||
|  |         .core-timer { | ||||||
|  |             &.core-timer-under-300 { | ||||||
|  |                 background-color: var(--danger-tint); | ||||||
|  |                 color: var(--danger-shade); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             &.core-timer-under-900 { | ||||||
|  |                 background-color: var(--warning-tint); | ||||||
|  |                 color: var(--warning-shade); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -32,11 +32,13 @@ import { | |||||||
|     AddonModAssignSubmissionStatusOptions, |     AddonModAssignSubmissionStatusOptions, | ||||||
|     AddonModAssignGetSubmissionStatusWSResponse, |     AddonModAssignGetSubmissionStatusWSResponse, | ||||||
|     AddonModAssignSavePluginData, |     AddonModAssignSavePluginData, | ||||||
|  |     AddonModAssignSubmissionStatusValues, | ||||||
| } from '../../services/assign'; | } from '../../services/assign'; | ||||||
| import { AddonModAssignHelper } from '../../services/assign-helper'; | import { AddonModAssignHelper } from '../../services/assign-helper'; | ||||||
| import { AddonModAssignOffline } from '../../services/assign-offline'; | import { AddonModAssignOffline } from '../../services/assign-offline'; | ||||||
| import { AddonModAssignSync } from '../../services/assign-sync'; | import { AddonModAssignSync } from '../../services/assign-sync'; | ||||||
| import { CoreUtils } from '@services/utils/utils'; | import { CoreUtils } from '@services/utils/utils'; | ||||||
|  | import { CoreWSExternalFile } from '@services/ws'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Page that allows adding or editing an assigment submission. |  * Page that allows adding or editing an assigment submission. | ||||||
| @ -44,6 +46,7 @@ import { CoreUtils } from '@services/utils/utils'; | |||||||
| @Component({ | @Component({ | ||||||
|     selector: 'page-addon-mod-assign-edit', |     selector: 'page-addon-mod-assign-edit', | ||||||
|     templateUrl: 'edit.html', |     templateUrl: 'edit.html', | ||||||
|  |     styleUrls: ['edit.scss'], | ||||||
| }) | }) | ||||||
| export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave { | export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave { | ||||||
| 
 | 
 | ||||||
| @ -58,6 +61,10 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave { | |||||||
|     submissionStatement?: string; // The submission statement.
 |     submissionStatement?: string; // The submission statement.
 | ||||||
|     submissionStatementAccepted = false; // Whether submission statement is accepted.
 |     submissionStatementAccepted = false; // Whether submission statement is accepted.
 | ||||||
|     loaded = false; // Whether data has been loaded.
 |     loaded = false; // Whether data has been loaded.
 | ||||||
|  |     timeLimitEndTime = 0; // If time limit is enabled, the end time for the timer.
 | ||||||
|  |     activityInstructions?: string; // Activity instructions.
 | ||||||
|  |     introAttachments?: CoreWSExternalFile[]; // Intro attachments.
 | ||||||
|  |     component = AddonModAssignProvider.COMPONENT; | ||||||
| 
 | 
 | ||||||
|     protected userId: number; // User doing the submission.
 |     protected userId: number; // User doing the submission.
 | ||||||
|     protected isBlind = false; // Whether blind is used.
 |     protected isBlind = false; // Whether blind is used.
 | ||||||
| @ -179,6 +186,22 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave { | |||||||
|                 throw new CoreError(Translate.instant('core.nopermissions', { $a: this.editText })); |                 throw new CoreError(Translate.instant('core.nopermissions', { $a: this.editText })); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  |             submissionStatus = await this.startSubmissionIfNeeded(submissionStatus, options); | ||||||
|  | 
 | ||||||
|  |             if (submissionStatus.assignmentdata?.activity) { | ||||||
|  |                 // There are activity instructions. Make sure to display it with filters applied.
 | ||||||
|  |                 const filteredSubmissionStatus = options.filter ? | ||||||
|  |                     submissionStatus : | ||||||
|  |                     await AddonModAssign.getSubmissionStatus(this.assign.id, { | ||||||
|  |                         ...options, | ||||||
|  |                         filter: true, | ||||||
|  |                     }); | ||||||
|  | 
 | ||||||
|  |                 this.activityInstructions = filteredSubmissionStatus.assignmentdata?.activity; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             this.introAttachments = submissionStatus.assignmentdata?.attachments?.intro ?? this.assign.introattachments; | ||||||
|  | 
 | ||||||
|             this.allowOffline = true; // If offline isn't allowed we shouldn't have reached this point.
 |             this.allowOffline = true; // If offline isn't allowed we shouldn't have reached this point.
 | ||||||
|             // Only show submission statement if we are editing our own submission.
 |             // Only show submission statement if we are editing our own submission.
 | ||||||
|             if (this.assign.requiresubmissionstatement && !this.assign.submissiondrafts && this.userId == currentUserId) { |             if (this.assign.requiresubmissionstatement && !this.assign.submissiondrafts && this.userId == currentUserId) { | ||||||
| @ -187,6 +210,12 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave { | |||||||
|                 this.submissionStatement = undefined; |                 this.submissionStatement = undefined; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  |             if (this.assign.timelimit && this.userSubmission?.timestarted) { | ||||||
|  |                 this.timeLimitEndTime = AddonModAssignHelper.calculateEndTime(this.assign, this.userSubmission); | ||||||
|  |             } else { | ||||||
|  |                 this.timeLimitEndTime = 0; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|             try { |             try { | ||||||
|                 // Check if there's any offline data for this submission.
 |                 // Check if there's any offline data for this submission.
 | ||||||
|                 const offlineData = await AddonModAssignOffline.getSubmission(this.assign.id, this.userId); |                 const offlineData = await AddonModAssignOffline.getSubmission(this.assign.id, this.userId); | ||||||
| @ -204,6 +233,45 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Start the submission if needed. | ||||||
|  |      * | ||||||
|  |      * @param submissionStatus Current submission status. | ||||||
|  |      * @param options Options. | ||||||
|  |      * @return Promise resolved with the new submission status if it changed, original submission status otherwise. | ||||||
|  |      */ | ||||||
|  |     protected async startSubmissionIfNeeded( | ||||||
|  |         submissionStatus: AddonModAssignGetSubmissionStatusWSResponse, | ||||||
|  |         options: AddonModAssignSubmissionStatusOptions, | ||||||
|  |     ): Promise<AddonModAssignGetSubmissionStatusWSResponse> { | ||||||
|  |         if (!this.assign || !this.assign.timelimit) { | ||||||
|  |             // Submission only needs to be started if there's a timelimit.
 | ||||||
|  |             return submissionStatus; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (this.userSubmission && this.userSubmission.status !== AddonModAssignSubmissionStatusValues.NEW && | ||||||
|  |             this.userSubmission.status !== AddonModAssignSubmissionStatusValues.REOPENED) { | ||||||
|  |             // There is an ongoing submission, no need to start it.
 | ||||||
|  |             return submissionStatus; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         await AddonModAssign.startSubmission(this.assign.id); | ||||||
|  | 
 | ||||||
|  |         CoreEvents.trigger(AddonModAssignProvider.STARTED_EVENT, { | ||||||
|  |             assignmentId: this.assign.id, | ||||||
|  |         }, CoreSites.getCurrentSiteId()); | ||||||
|  | 
 | ||||||
|  |         // Submission started, update the submission status.
 | ||||||
|  |         const newSubmissionStatus = await AddonModAssign.getSubmissionStatus(this.assign.id, { | ||||||
|  |             ...options, | ||||||
|  |             readingStrategy: CoreSitesReadingStrategy.ONLY_NETWORK, // Make sure not to use cache.
 | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         this.userSubmission = AddonModAssign.getSubmissionObjectFromAttempt(this.assign, newSubmissionStatus.lastattempt); | ||||||
|  | 
 | ||||||
|  |         return newSubmissionStatus; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Get the input data. |      * Get the input data. | ||||||
|      * |      * | ||||||
| @ -392,6 +460,18 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Function called when the time is up. | ||||||
|  |      */ | ||||||
|  |     timeUp(): void { | ||||||
|  |         CoreDomUtils.showToastWithOptions({ | ||||||
|  |             message: Translate.instant('addon.mod_assign.caneditsubmission'), | ||||||
|  |             duration: 0, | ||||||
|  |             buttons: [Translate.instant('core.dismiss')], | ||||||
|  |             cssClass: 'core-danger-toast', | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Component being destroyed. |      * Component being destroyed. | ||||||
|      */ |      */ | ||||||
|  | |||||||
| @ -44,6 +44,25 @@ import { CoreFileEntry } from '@services/file-helper'; | |||||||
| @Injectable({ providedIn: 'root' }) | @Injectable({ providedIn: 'root' }) | ||||||
| export class AddonModAssignHelperProvider { | export class AddonModAssignHelperProvider { | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Calculate the end time (timestamp) for an assign and submission. | ||||||
|  |      * | ||||||
|  |      * @param assign Assign instance. | ||||||
|  |      * @param submission Submission. | ||||||
|  |      * @return End time. | ||||||
|  |      */ | ||||||
|  |     calculateEndTime(assign: AddonModAssignAssign, submission?: AddonModAssignSubmissionFormatted): number { | ||||||
|  |         const timeDue = (submission?.timestarted || 0) + (assign.timelimit || 0); | ||||||
|  | 
 | ||||||
|  |         if (assign.duedate) { | ||||||
|  |             return Math.min(timeDue, assign.duedate); | ||||||
|  |         } else if (assign.cutoffdate) { | ||||||
|  |             return Math.min(timeDue, assign.cutoffdate); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return timeDue; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Check if a submission can be edited in offline. |      * Check if a submission can be edited in offline. | ||||||
|      * |      * | ||||||
|  | |||||||
| @ -49,6 +49,7 @@ declare module '@singletons/events' { | |||||||
|         [AddonModAssignProvider.SUBMISSION_SAVED_EVENT]: AddonModAssignSubmissionSavedEventData; |         [AddonModAssignProvider.SUBMISSION_SAVED_EVENT]: AddonModAssignSubmissionSavedEventData; | ||||||
|         [AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT]: AddonModAssignSubmittedForGradingEventData; |         [AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT]: AddonModAssignSubmittedForGradingEventData; | ||||||
|         [AddonModAssignProvider.GRADED_EVENT]: AddonModAssignGradedEventData; |         [AddonModAssignProvider.GRADED_EVENT]: AddonModAssignGradedEventData; | ||||||
|  |         [AddonModAssignProvider.STARTED_EVENT]: AddonModAssignStartedEventData; | ||||||
|         [AddonModAssignSyncProvider.MANUAL_SYNCED]: AddonModAssignManualSyncData; |         [AddonModAssignSyncProvider.MANUAL_SYNCED]: AddonModAssignManualSyncData; | ||||||
|         [AddonModAssignSyncProvider.AUTO_SYNCED]: AddonModAssignAutoSyncData; |         [AddonModAssignSyncProvider.AUTO_SYNCED]: AddonModAssignAutoSyncData; | ||||||
|     } |     } | ||||||
| @ -73,6 +74,7 @@ export class AddonModAssignProvider { | |||||||
|     static readonly SUBMISSION_SAVED_EVENT = 'addon_mod_assign_submission_saved'; |     static readonly SUBMISSION_SAVED_EVENT = 'addon_mod_assign_submission_saved'; | ||||||
|     static readonly SUBMITTED_FOR_GRADING_EVENT = 'addon_mod_assign_submitted_for_grading'; |     static readonly SUBMITTED_FOR_GRADING_EVENT = 'addon_mod_assign_submitted_for_grading'; | ||||||
|     static readonly GRADED_EVENT = 'addon_mod_assign_graded'; |     static readonly GRADED_EVENT = 'addon_mod_assign_graded'; | ||||||
|  |     static readonly STARTED_EVENT = 'addon_mod_assign_started'; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Check if the user can submit in offline. This should only be used if submissionStatus.lastattempt.cansubmit cannot |      * Check if the user can submit in offline. This should only be used if submissionStatus.lastattempt.cansubmit cannot | ||||||
| @ -1069,6 +1071,35 @@ export class AddonModAssignProvider { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Start a submission. | ||||||
|  |      * | ||||||
|  |      * @param assignId Assign ID. | ||||||
|  |      * @param siteId Site ID. If not defined, use current site. | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     async startSubmission(assignId: number, siteId?: string): Promise<void> { | ||||||
|  |         const site = await CoreSites.getSite(siteId); | ||||||
|  | 
 | ||||||
|  |         const params: AddonModAssignStartSubmissionWSParams = { | ||||||
|  |             assignid: assignId, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         const result = await site.write<AddonModAssignStartSubmissionWSResponse>('mod_assign_start_submission', params); | ||||||
|  | 
 | ||||||
|  |         if (!result.warnings?.length) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Ignore some warnings.
 | ||||||
|  |         const warning = result.warnings.find(warning => | ||||||
|  |             warning.warningcode !== 'timelimitnotenabled' && warning.warningcode !== 'opensubmissionexists'); | ||||||
|  | 
 | ||||||
|  |         if (warning) { | ||||||
|  |             throw new CoreWSError(warning); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Submit the current user assignment for grading. |      * Submit the current user assignment for grading. | ||||||
|      * |      * | ||||||
| @ -1351,6 +1382,11 @@ export type AddonModAssignAssign = { | |||||||
|     introformat?: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
 |     introformat?: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
 | ||||||
|     introfiles?: CoreWSExternalFile[]; |     introfiles?: CoreWSExternalFile[]; | ||||||
|     introattachments?: CoreWSExternalFile[]; |     introattachments?: CoreWSExternalFile[]; | ||||||
|  |     activity?: string; // @since 4.0. Description of activity.
 | ||||||
|  |     activityformat?: number; // @since 4.0. Format of activity.
 | ||||||
|  |     activityattachments?: CoreWSExternalFile[]; // @since 4.0. Files from activity field.
 | ||||||
|  |     timelimit?: number; // @since 4.0. Time limit to complete assigment.
 | ||||||
|  |     submissionattachments?: number; // @since 4.0. Flag to only show files during submission.
 | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
| @ -1395,6 +1431,7 @@ export type AddonModAssignSubmission = { | |||||||
|     latest?: number; // Latest attempt.
 |     latest?: number; // Latest attempt.
 | ||||||
|     plugins?: AddonModAssignPlugin[]; // Plugins.
 |     plugins?: AddonModAssignPlugin[]; // Plugins.
 | ||||||
|     gradingstatus?: AddonModAssignGradingStates; // Grading status.
 |     gradingstatus?: AddonModAssignGradingStates; // Grading status.
 | ||||||
|  |     timestarted?: number; // @since 4.0. Submission start time.
 | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
| @ -1445,6 +1482,7 @@ export type AddonModAssignSubmissionAttempt = { | |||||||
|     blindmarking: boolean; // Whether blind marking is enabled.
 |     blindmarking: boolean; // Whether blind marking is enabled.
 | ||||||
|     gradingstatus: AddonModAssignGradingStates; // Grading status.
 |     gradingstatus: AddonModAssignGradingStates; // Grading status.
 | ||||||
|     usergroups: number[]; // User groups in the course.
 |     usergroups: number[]; // User groups in the course.
 | ||||||
|  |     timelimit?: number; // @since 4.0. Time limit for submission.
 | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
| @ -1604,6 +1642,14 @@ export type AddonModAssignGetSubmissionStatusWSResponse = { | |||||||
|     lastattempt?: AddonModAssignSubmissionAttempt; // Last attempt information.
 |     lastattempt?: AddonModAssignSubmissionAttempt; // Last attempt information.
 | ||||||
|     feedback?: AddonModAssignSubmissionFeedback; // Feedback for the last attempt.
 |     feedback?: AddonModAssignSubmissionFeedback; // Feedback for the last attempt.
 | ||||||
|     previousattempts?: AddonModAssignSubmissionPreviousAttempt[]; // List all the previous attempts did by the user.
 |     previousattempts?: AddonModAssignSubmissionPreviousAttempt[]; // List all the previous attempts did by the user.
 | ||||||
|  |     assignmentdata?: { // @since 4.0. Extra information about assignment.
 | ||||||
|  |         attachments?: { // Intro and activity attachments.
 | ||||||
|  |             intro?: CoreWSExternalFile[]; // Intro attachments files.
 | ||||||
|  |             activity?: CoreWSExternalFile[]; // Activity attachments files.
 | ||||||
|  |         }; | ||||||
|  |         activity?: string; // Text of activity.
 | ||||||
|  |         activityformat?: number; // Format of activity.
 | ||||||
|  |     }; | ||||||
|     warnings?: CoreWSExternalWarning[]; |     warnings?: CoreWSExternalWarning[]; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| @ -1715,6 +1761,25 @@ type AddonModAssignSubmitGradingFormWSParams = { | |||||||
|     jsonformdata: string; // The data from the grading form, encoded as a json array.
 |     jsonformdata: string; // The data from the grading form, encoded as a json array.
 | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * Params of mod_assign_start_submission WS. | ||||||
|  |  * | ||||||
|  |  * @since 4.0 | ||||||
|  |  */ | ||||||
|  | type AddonModAssignStartSubmissionWSParams = { | ||||||
|  |     assignid: number; // Assignment instance id.
 | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Data returned by mod_assign_start_submission WS. | ||||||
|  |  * | ||||||
|  |  * @since 4.0 | ||||||
|  |  */ | ||||||
|  | export type AddonModAssignStartSubmissionWSResponse = { | ||||||
|  |     submissionid: number; // New submission ID.
 | ||||||
|  |     warnings?: CoreWSExternalWarning[]; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Assignment grade outcomes. |  * Assignment grade outcomes. | ||||||
|  */ |  */ | ||||||
| @ -1739,6 +1804,13 @@ export type AddonModAssignSubmissionSavedEventData = AddonModAssignSubmittedForG | |||||||
|  */ |  */ | ||||||
| export type AddonModAssignGradedEventData = AddonModAssignSubmittedForGradingEventData; | export type AddonModAssignGradedEventData = AddonModAssignSubmittedForGradingEventData; | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * Data sent by STARTED_EVENT event. | ||||||
|  |  */ | ||||||
|  | export type AddonModAssignStartedEventData = { | ||||||
|  |     assignmentId: number; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Submission status. |  * Submission status. | ||||||
|  * Constants on LMS starting with ASSIGN_SUBMISSION_STATUS_ |  * Constants on LMS starting with ASSIGN_SUBMISSION_STATUS_ | ||||||
|  | |||||||
| @ -87,9 +87,10 @@ export class AddonModAssignPrefetchHandlerService extends CoreCourseActivityPref | |||||||
| 
 | 
 | ||||||
|         try { |         try { | ||||||
|             const assign = await AddonModAssign.getAssignment(courseId, module.id, { siteId }); |             const assign = await AddonModAssign.getAssignment(courseId, module.id, { siteId }); | ||||||
|             // Get intro files and attachments.
 |             // Get intro files, attachments and activity files.
 | ||||||
|             let files: CoreWSFile[] = assign.introattachments || []; |             let files: CoreWSFile[] = assign.introattachments || []; | ||||||
|             files = files.concat(this.getIntroFilesFromInstance(module, assign)); |             files = files.concat(this.getIntroFilesFromInstance(module, assign)); | ||||||
|  |             files = files.concat(assign.activityattachments || []); | ||||||
| 
 | 
 | ||||||
|             // Now get the files in the submissions.
 |             // Now get the files in the submissions.
 | ||||||
|             const submissionData = await AddonModAssign.getSubmissions(assign.id, { cmId: module.id, siteId }); |             const submissionData = await AddonModAssign.getSubmissions(assign.id, { cmId: module.id, siteId }); | ||||||
| @ -100,19 +101,26 @@ export class AddonModAssignPrefetchHandlerService extends CoreCourseActivityPref | |||||||
|                     await AddonModAssignHelper.getSubmissionsUserData(assign, submissionData.submissions, 0, { siteId }); |                     await AddonModAssignHelper.getSubmissionsUserData(assign, submissionData.submissions, 0, { siteId }); | ||||||
| 
 | 
 | ||||||
|                 // Get all the files in the submissions.
 |                 // Get all the files in the submissions.
 | ||||||
|                 const promises = submissions.map((submission) => |                 const promises = submissions.map(async (submission) => { | ||||||
|                     this.getSubmissionFiles(assign, submission.submitid!, !!submission.blindid, siteId).then((submissionFiles) => { |                     try { | ||||||
|                         files = files.concat(submissionFiles); |                         const submissionFiles = await this.getSubmissionFiles( | ||||||
|  |                             assign, | ||||||
|  |                             submission.submitid!, | ||||||
|  |                             !!submission.blindid, | ||||||
|  |                             true, | ||||||
|  |                             siteId, | ||||||
|  |                         ); | ||||||
| 
 | 
 | ||||||
|                         return; |                         files = files.concat(submissionFiles); | ||||||
|                     }).catch((error) => { |                     } catch (error) { | ||||||
|                         if (error && error.errorcode == 'nopermission') { |                         if (error && error.errorcode == 'nopermission') { | ||||||
|                             // The user does not have persmission to view this submission, ignore it.
 |                             // The user does not have persmission to view this submission, ignore it.
 | ||||||
|                             return; |                             return; | ||||||
|                         } |                         } | ||||||
| 
 | 
 | ||||||
|                         throw error; |                         throw error; | ||||||
|                     })); |                     } | ||||||
|  |                 }); | ||||||
| 
 | 
 | ||||||
|                 await Promise.all(promises); |                 await Promise.all(promises); | ||||||
|             } else { |             } else { | ||||||
| @ -120,7 +128,7 @@ export class AddonModAssignPrefetchHandlerService extends CoreCourseActivityPref | |||||||
|                 const userId = CoreSites.getCurrentSiteUserId(); |                 const userId = CoreSites.getCurrentSiteUserId(); | ||||||
|                 const blindMarking = !!assign.blindmarking && !assign.revealidentities; |                 const blindMarking = !!assign.blindmarking && !assign.revealidentities; | ||||||
| 
 | 
 | ||||||
|                 const submissionFiles = await this.getSubmissionFiles(assign, userId, blindMarking, siteId); |                 const submissionFiles = await this.getSubmissionFiles(assign, userId, blindMarking, false, siteId); | ||||||
|                 files = files.concat(submissionFiles); |                 files = files.concat(submissionFiles); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| @ -137,6 +145,7 @@ export class AddonModAssignPrefetchHandlerService extends CoreCourseActivityPref | |||||||
|      * @param assign Assign. |      * @param assign Assign. | ||||||
|      * @param submitId User ID of the submission to get. |      * @param submitId User ID of the submission to get. | ||||||
|      * @param blindMarking True if blind marking, false otherwise. |      * @param blindMarking True if blind marking, false otherwise. | ||||||
|  |      * @param canViewAllSubmissions Whether the user can view all submissions. | ||||||
|      * @param siteId Site ID. If not defined, current site. |      * @param siteId Site ID. If not defined, current site. | ||||||
|      * @return Promise resolved with array of files. |      * @return Promise resolved with array of files. | ||||||
|      */ |      */ | ||||||
| @ -144,6 +153,7 @@ export class AddonModAssignPrefetchHandlerService extends CoreCourseActivityPref | |||||||
|         assign: AddonModAssignAssign, |         assign: AddonModAssignAssign, | ||||||
|         submitId: number, |         submitId: number, | ||||||
|         blindMarking: boolean, |         blindMarking: boolean, | ||||||
|  |         canViewAllSubmissions: boolean, | ||||||
|         siteId?: string, |         siteId?: string, | ||||||
|     ): Promise<CoreWSFile[]> { |     ): Promise<CoreWSFile[]> { | ||||||
| 
 | 
 | ||||||
| @ -154,8 +164,15 @@ export class AddonModAssignPrefetchHandlerService extends CoreCourseActivityPref | |||||||
|         }); |         }); | ||||||
|         const userSubmission = AddonModAssign.getSubmissionObjectFromAttempt(assign, submissionStatus.lastattempt); |         const userSubmission = AddonModAssign.getSubmissionObjectFromAttempt(assign, submissionStatus.lastattempt); | ||||||
| 
 | 
 | ||||||
|  |         // Get intro and activity files from the submission status if it's a student.
 | ||||||
|  |         // It's ok if they were already obtained from the assignment instance, they won't be downloaded twice.
 | ||||||
|  |         const files = canViewAllSubmissions ? | ||||||
|  |             [] : | ||||||
|  |             (submissionStatus.assignmentdata?.attachments?.intro || []) | ||||||
|  |                 .concat(submissionStatus.assignmentdata?.attachments?.activity || []); | ||||||
|  | 
 | ||||||
|         if (!submissionStatus.lastattempt || !userSubmission) { |         if (!submissionStatus.lastattempt || !userSubmission) { | ||||||
|             return []; |             return files; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const promises: Promise<CoreWSFile[]>[] = []; |         const promises: Promise<CoreWSFile[]>[] = []; | ||||||
| @ -176,7 +193,7 @@ export class AddonModAssignPrefetchHandlerService extends CoreCourseActivityPref | |||||||
| 
 | 
 | ||||||
|         const filesLists = await Promise.all(promises); |         const filesLists = await Promise.all(promises); | ||||||
| 
 | 
 | ||||||
|         return [].concat.apply([], filesLists); |         return files.concat.apply(files, filesLists); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -18,6 +18,7 @@ import { CoreCourseContentsPage } from '@features/course/pages/contents/contents | |||||||
| import { IonContent } from '@ionic/angular'; | import { IonContent } from '@ionic/angular'; | ||||||
| import { CoreNavigator } from '@services/navigator'; | import { CoreNavigator } from '@services/navigator'; | ||||||
| import { CoreTimeUtils } from '@services/utils/time'; | import { CoreTimeUtils } from '@services/utils/time'; | ||||||
|  | import { CoreTime } from '@singletons/time'; | ||||||
| import { AddonModChat, AddonModChatChat, AddonModChatProvider } from '../../services/chat'; | import { AddonModChat, AddonModChatChat, AddonModChatProvider } from '../../services/chat'; | ||||||
| import { AddonModChatModuleHandlerService } from '../../services/handlers/module'; | import { AddonModChatModuleHandlerService } from '../../services/handlers/module'; | ||||||
| 
 | 
 | ||||||
| @ -67,7 +68,7 @@ export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComp | |||||||
|         if (this.chat.chattime && this.chat.schedule && span > 0) { |         if (this.chat.chattime && this.chat.schedule && span > 0) { | ||||||
|             this.chatInfo = { |             this.chatInfo = { | ||||||
|                 date: CoreTimeUtils.userDate(this.chat.chattime * 1000), |                 date: CoreTimeUtils.userDate(this.chat.chattime * 1000), | ||||||
|                 fromnow: CoreTimeUtils.formatTime(span), |                 fromnow: CoreTime.formatTime(span), | ||||||
|             }; |             }; | ||||||
|         } else { |         } else { | ||||||
|             this.chatInfo = undefined; |             this.chatInfo = undefined; | ||||||
|  | |||||||
| @ -16,7 +16,6 @@ import { Injectable } from '@angular/core'; | |||||||
| 
 | 
 | ||||||
| import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; | import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; | ||||||
| import { CoreWSExternalWarning, CoreWSExternalFile, CoreWSFile } from '@services/ws'; | import { CoreWSExternalWarning, CoreWSExternalFile, CoreWSFile } from '@services/ws'; | ||||||
| import { CoreTimeUtils } from '@services/utils/time'; |  | ||||||
| import { CoreUtils } from '@services/utils/utils'; | import { CoreUtils } from '@services/utils/utils'; | ||||||
| import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; | import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; | ||||||
| import { CoreCourseLogHelper } from '@features/course/services/log-helper'; | import { CoreCourseLogHelper } from '@features/course/services/log-helper'; | ||||||
| @ -27,6 +26,7 @@ import { makeSingleton, Translate } from '@singletons/index'; | |||||||
| import { CoreWSError } from '@classes/errors/wserror'; | import { CoreWSError } from '@classes/errors/wserror'; | ||||||
| import { CoreError } from '@classes/errors/error'; | import { CoreError } from '@classes/errors/error'; | ||||||
| import { AddonModH5PActivityAutoSyncData, AddonModH5PActivitySyncProvider } from './h5pactivity-sync'; | import { AddonModH5PActivityAutoSyncData, AddonModH5PActivitySyncProvider } from './h5pactivity-sync'; | ||||||
|  | import { CoreTime } from '@singletons/time'; | ||||||
| 
 | 
 | ||||||
| const ROOT_CACHE_KEY = 'mmaModH5PActivity:'; | const ROOT_CACHE_KEY = 'mmaModH5PActivity:'; | ||||||
| 
 | 
 | ||||||
| @ -90,8 +90,8 @@ export class AddonModH5PActivityProvider { | |||||||
|             formattedAttempt.durationReadable = '-'; |             formattedAttempt.durationReadable = '-'; | ||||||
|             formattedAttempt.durationCompact = '-'; |             formattedAttempt.durationCompact = '-'; | ||||||
|         } else { |         } else { | ||||||
|             formattedAttempt.durationReadable = CoreTimeUtils.formatTime(attempt.duration); |             formattedAttempt.durationReadable = CoreTime.formatTime(attempt.duration, 3); | ||||||
|             formattedAttempt.durationCompact = CoreTimeUtils.formatDurationShort(attempt.duration); |             formattedAttempt.durationCompact = CoreTime.formatTimeShort(attempt.duration); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return formattedAttempt; |         return formattedAttempt; | ||||||
|  | |||||||
| @ -25,7 +25,6 @@ import { CoreNavigator } from '@services/navigator'; | |||||||
| import { CoreDomUtils } from '@services/utils/dom'; | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
| import { CoreForms } from '@singletons/form'; | import { CoreForms } from '@singletons/form'; | ||||||
| import { CoreTextUtils } from '@services/utils/text'; | import { CoreTextUtils } from '@services/utils/text'; | ||||||
| import { CoreTimeUtils } from '@services/utils/time'; |  | ||||||
| import { CoreUtils } from '@services/utils/utils'; | import { CoreUtils } from '@services/utils/utils'; | ||||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||||
| import { AddonModLessonRetakeFinishedInSyncDBRecord } from '../../services/database/lesson'; | import { AddonModLessonRetakeFinishedInSyncDBRecord } from '../../services/database/lesson'; | ||||||
| @ -47,6 +46,7 @@ import { | |||||||
|     AddonModLessonSyncResult, |     AddonModLessonSyncResult, | ||||||
| } from '../../services/lesson-sync'; | } from '../../services/lesson-sync'; | ||||||
| import { AddonModLessonModuleHandlerService } from '../../services/handlers/module'; | import { AddonModLessonModuleHandlerService } from '../../services/handlers/module'; | ||||||
|  | import { CoreTime } from '@singletons/time'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Component that displays a lesson entry page. |  * Component that displays a lesson entry page. | ||||||
| @ -505,15 +505,15 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo | |||||||
|         // Format times and grades.
 |         // Format times and grades.
 | ||||||
|         if (formattedData.avetime != null && formattedData.numofattempts) { |         if (formattedData.avetime != null && formattedData.numofattempts) { | ||||||
|             formattedData.avetime = Math.floor(formattedData.avetime / formattedData.numofattempts); |             formattedData.avetime = Math.floor(formattedData.avetime / formattedData.numofattempts); | ||||||
|             this.avetimeReadable = CoreTimeUtils.formatTime(formattedData.avetime); |             this.avetimeReadable = CoreTime.formatTime(formattedData.avetime); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (formattedData.hightime != null) { |         if (formattedData.hightime != null) { | ||||||
|             this.hightimeReadable = CoreTimeUtils.formatTime(formattedData.hightime); |             this.hightimeReadable = CoreTime.formatTime(formattedData.hightime); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (formattedData.lowtime != null) { |         if (formattedData.lowtime != null) { | ||||||
|             this.lowtimeReadable = CoreTimeUtils.formatTime(formattedData.lowtime); |             this.lowtimeReadable = CoreTime.formatTime(formattedData.lowtime); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (formattedData.lessonscored) { |         if (formattedData.lessonscored) { | ||||||
|  | |||||||
| @ -30,7 +30,7 @@ | |||||||
|         <div *ngIf="lesson" [ngClass]='{"addon-mod_lesson-slideshow": lesson.slideshow}' |         <div *ngIf="lesson" [ngClass]='{"addon-mod_lesson-slideshow": lesson.slideshow}' | ||||||
|             [ngStyle]="{'width': lessonWidth, 'height': lessonHeight}"> |             [ngStyle]="{'width': lessonWidth, 'height': lessonHeight}"> | ||||||
| 
 | 
 | ||||||
|             <core-timer *ngIf="endTime" [endTime]="endTime" (finished)="timeUp()" |             <core-timer *ngIf="endTime" [endTime]="endTime" (finished)="timeUp()" [timeLeftClassThreshold]="-1" | ||||||
|                 [timerText]="'addon.mod_lesson.timeremaining' | translate"> |                 [timerText]="'addon.mod_lesson.timeremaining' | translate"> | ||||||
|             </core-timer> |             </core-timer> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -34,8 +34,8 @@ import { | |||||||
|     AddonModLessonUserAttemptAnswerPageWSData, |     AddonModLessonUserAttemptAnswerPageWSData, | ||||||
| } from '../../services/lesson'; | } from '../../services/lesson'; | ||||||
| import { AddonModLessonAnswerData, AddonModLessonHelper } from '../../services/lesson-helper'; | import { AddonModLessonAnswerData, AddonModLessonHelper } from '../../services/lesson-helper'; | ||||||
| import { CoreTimeUtils } from '@services/utils/time'; |  | ||||||
| import { CoreCourse } from '@features/course/services/course'; | import { CoreCourse } from '@features/course/services/course'; | ||||||
|  | import { CoreTime } from '@singletons/time'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Page that displays a retake made by a certain user. |  * Page that displays a retake made by a certain user. | ||||||
| @ -222,7 +222,7 @@ export class AddonModLessonUserRetakePage implements OnInit { | |||||||
|         if (formattedData.userstats.gradeinfo) { |         if (formattedData.userstats.gradeinfo) { | ||||||
|             // Completed.
 |             // Completed.
 | ||||||
|             formattedData.userstats.grade = CoreTextUtils.roundToDecimals(formattedData.userstats.grade, 2); |             formattedData.userstats.grade = CoreTextUtils.roundToDecimals(formattedData.userstats.grade, 2); | ||||||
|             this.timeTakenReadable = CoreTimeUtils.formatTime(formattedData.userstats.timetotake); |             this.timeTakenReadable = CoreTime.formatTime(formattedData.userstats.timetotake); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Format pages data.
 |         // Format pages data.
 | ||||||
|  | |||||||
| @ -26,6 +26,7 @@ import { | |||||||
|     AddonModLessonGetPageDataWSResponse, |     AddonModLessonGetPageDataWSResponse, | ||||||
|     AddonModLessonProvider, |     AddonModLessonProvider, | ||||||
| } from './lesson'; | } from './lesson'; | ||||||
|  | import { CoreTime } from '@singletons/time'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Helper service that provides some features for quiz. |  * Helper service that provides some features for quiz. | ||||||
| @ -531,7 +532,7 @@ export class AddonModLessonHelperProvider { | |||||||
|             } |             } | ||||||
|             data.timestart = CoreTimeUtils.userDate(retake.timestart * 1000); |             data.timestart = CoreTimeUtils.userDate(retake.timestart * 1000); | ||||||
|             if (includeDuration) { |             if (includeDuration) { | ||||||
|                 data.duration = CoreTimeUtils.formatTime(retake.timeend - retake.timestart); |                 data.duration = CoreTime.formatTime(retake.timeend - retake.timestart); | ||||||
|             } |             } | ||||||
|         } else { |         } else { | ||||||
|             // The user has not completed the retake.
 |             // The user has not completed the retake.
 | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ import { Component, Input, OnInit } from '@angular/core'; | |||||||
| import { FormGroup } from '@angular/forms'; | import { FormGroup } from '@angular/forms'; | ||||||
| 
 | 
 | ||||||
| import { AddonModQuizAttemptWSData, AddonModQuizQuizWSData } from '@addons/mod/quiz/services/quiz'; | import { AddonModQuizAttemptWSData, AddonModQuizQuizWSData } from '@addons/mod/quiz/services/quiz'; | ||||||
| import { CoreTimeUtils } from '@services/utils/time'; | import { CoreTime } from '@singletons/time'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Component to render the preflight for time limit. |  * Component to render the preflight for time limit. | ||||||
| @ -41,7 +41,7 @@ export class AddonModQuizAccessTimeLimitComponent implements OnInit { | |||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.readableTimeLimit = CoreTimeUtils.formatTime(this.quiz?.timelimit); |         this.readableTimeLimit = CoreTime.formatTime(this.quiz?.timelimit); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -24,7 +24,6 @@ import { CoreNavigator } from '@services/navigator'; | |||||||
| import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; | import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; | ||||||
| import { CoreSync } from '@services/sync'; | import { CoreSync } from '@services/sync'; | ||||||
| import { CoreDomUtils } from '@services/utils/dom'; | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
| import { CoreTimeUtils } from '@services/utils/time'; |  | ||||||
| import { CoreUtils } from '@services/utils/utils'; | import { CoreUtils } from '@services/utils/utils'; | ||||||
| import { ModalController, Translate } from '@singletons'; | import { ModalController, Translate } from '@singletons'; | ||||||
| import { CoreEvents } from '@singletons/events'; | import { CoreEvents } from '@singletons/events'; | ||||||
| @ -47,6 +46,7 @@ import { AddonModQuizSync } from '../../services/quiz-sync'; | |||||||
| import { CanLeave } from '@guards/can-leave'; | import { CanLeave } from '@guards/can-leave'; | ||||||
| import { CoreForms } from '@singletons/form'; | import { CoreForms } from '@singletons/form'; | ||||||
| import { CoreDom } from '@singletons/dom'; | import { CoreDom } from '@singletons/dom'; | ||||||
|  | import { CoreTime } from '@singletons/time'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Page that allows attempting a quiz. |  * Page that allows attempting a quiz. | ||||||
| @ -352,7 +352,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { | |||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (this.quiz!.timelimit && this.quiz!.timelimit > 0) { |             if (this.quiz!.timelimit && this.quiz!.timelimit > 0) { | ||||||
|                 this.readableTimeLimit = CoreTimeUtils.formatTime(this.quiz.timelimit); |                 this.readableTimeLimit = CoreTime.formatTime(this.quiz.timelimit); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // Get access information for the quiz.
 |             // Get access information for the quiz.
 | ||||||
|  | |||||||
| @ -1,3 +1,8 @@ | |||||||
|  | @import "~theme/globals"; | ||||||
|  | 
 | ||||||
|  | $quiz-timer-warn-color: $red !default; | ||||||
|  | $quiz-timer-iterations: 15 !default; | ||||||
|  | 
 | ||||||
| :host { | :host { | ||||||
|     .addon-mod_quiz-question-note p { |     .addon-mod_quiz-question-note p { | ||||||
|         margin-top: 2px; |         margin-top: 2px; | ||||||
| @ -7,4 +12,19 @@ | |||||||
|     ion-content ion-toolbar { |     ion-content ion-toolbar { | ||||||
|         border-bottom: 1px solid var(--stroke); |         border-bottom: 1px solid var(--stroke); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     core-timer .core-timer { | ||||||
|  |         // Make the timer go red when it's reaching 0. | ||||||
|  |         @for $i from 0 through $quiz-timer-iterations { | ||||||
|  |             &.core-timer-timeleft-#{$i} { | ||||||
|  |                 background-color: rgba($quiz-timer-warn-color, 1 - ($i / $quiz-timer-iterations)) !important; | ||||||
|  | 
 | ||||||
|  |                 @if $i <= $quiz-timer-iterations / 2 { | ||||||
|  |                     label, span, ion-icon { | ||||||
|  |                         color: var(--white); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -19,10 +19,10 @@ import { CoreQuestionHelper } from '@features/question/services/question-helper' | |||||||
| import { IonContent, IonRefresher } from '@ionic/angular'; | import { IonContent, IonRefresher } from '@ionic/angular'; | ||||||
| import { CoreNavigator } from '@services/navigator'; | import { CoreNavigator } from '@services/navigator'; | ||||||
| import { CoreDomUtils } from '@services/utils/dom'; | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
| import { CoreTimeUtils } from '@services/utils/time'; |  | ||||||
| import { CoreUtils } from '@services/utils/utils'; | import { CoreUtils } from '@services/utils/utils'; | ||||||
| import { Translate } from '@singletons'; | import { Translate } from '@singletons'; | ||||||
| import { CoreDom } from '@singletons/dom'; | import { CoreDom } from '@singletons/dom'; | ||||||
|  | import { CoreTime } from '@singletons/time'; | ||||||
| import { | import { | ||||||
|     AddonModQuizNavigationModalComponent, |     AddonModQuizNavigationModalComponent, | ||||||
|     AddonModQuizNavigationModalReturn, |     AddonModQuizNavigationModalReturn, | ||||||
| @ -276,11 +276,11 @@ export class AddonModQuizReviewPage implements OnInit { | |||||||
|         const timeTaken = (this.attempt.timefinish || 0) - (this.attempt.timestart || 0); |         const timeTaken = (this.attempt.timefinish || 0) - (this.attempt.timestart || 0); | ||||||
|         if (timeTaken > 0) { |         if (timeTaken > 0) { | ||||||
|             // Format time taken.
 |             // Format time taken.
 | ||||||
|             this.timeTaken = CoreTimeUtils.formatTime(timeTaken); |             this.timeTaken = CoreTime.formatTime(timeTaken); | ||||||
| 
 | 
 | ||||||
|             // Calculate overdue time.
 |             // Calculate overdue time.
 | ||||||
|             if (this.quiz.timelimit && timeTaken > this.quiz.timelimit + 60) { |             if (this.quiz.timelimit && timeTaken > this.quiz.timelimit + 60) { | ||||||
|                 this.overTime = CoreTimeUtils.formatTime(timeTaken - this.quiz.timelimit); |                 this.overTime = CoreTime.formatTime(timeTaken - this.quiz.timelimit); | ||||||
|             } |             } | ||||||
|         } else { |         } else { | ||||||
|             this.timeTaken = undefined; |             this.timeTaken = undefined; | ||||||
|  | |||||||
| @ -1,10 +1,21 @@ | |||||||
| <ion-item class="core-timer" role="timer" [ngClass]="{'ion-text-center': align == 'center', 'ion-text-end': align == 'right'}"> | <ion-item *ngIf="mode !== modeBasic" class="core-timer" role="timer" | ||||||
|  |     [ngClass]="{'ion-text-center': align == 'center', 'ion-text-end': align == 'right'}"> | ||||||
|     <ion-icon name="fas-clock" slot="start" aria-hidden="true"></ion-icon> |     <ion-icon name="fas-clock" slot="start" aria-hidden="true"></ion-icon> | ||||||
|     <ion-label> |     <ion-label> | ||||||
|         <span *ngIf="timeLeft && timeLeft > 0 && timerText" class="core-timer-text">{{ timerText }}</span> |         <ng-container *ngTemplateOutlet="timerTemplate"></ng-container> | ||||||
|         <span *ngIf="timeLeft && timeLeft > 0" class="core-timer-time-left">{{ timeLeft | coreSecondsToHMS }}</span> |  | ||||||
|         <span class="core-timesup" *ngIf="timeLeft !== undefined && timeLeft <= 0"> |  | ||||||
|             {{ 'core.timesup' | translate }} |  | ||||||
|         </span> |  | ||||||
|     </ion-label> |     </ion-label> | ||||||
| </ion-item> | </ion-item> | ||||||
|  | 
 | ||||||
|  | <div *ngIf="mode === modeBasic" class="core-timer ion-padding" role="timer" | ||||||
|  |     [ngClass]="{'ion-text-center': align == 'center', 'ion-text-end': align == 'right'}"> | ||||||
|  |     <ng-container *ngTemplateOutlet="timerTemplate"></ng-container> | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | <ng-template #timerTemplate> | ||||||
|  |     <span *ngIf="timerText" class="core-timer-text">{{ timerText }}</span> | ||||||
|  |     <span *ngIf="timeLeft && timeLeft > 0" class="core-timer-time-left">{{ timeLeft | coreSecondsToHMS }}</span> | ||||||
|  |     <span class="core-timesup" *ngIf="timeLeft !== undefined && timeLeft <= 0"> | ||||||
|  |         <ng-container *ngIf="timeUpText">{{ timeUpText }}</ng-container> | ||||||
|  |         <ng-container *ngIf="!timeUpText">{{ 'core.timesup' | translate }}</ng-container> | ||||||
|  |     </span> | ||||||
|  | </ng-template> | ||||||
|  | |||||||
| @ -1,8 +1,3 @@ | |||||||
| @import "~theme/globals"; |  | ||||||
| 
 |  | ||||||
| $core-timer-warn-color: $red !default; |  | ||||||
| $core-timer-iterations: 15 !default; |  | ||||||
| 
 |  | ||||||
| :host { | :host { | ||||||
|     .core-timer { |     .core-timer { | ||||||
|         --background: transparent !important; |         --background: transparent !important; | ||||||
| @ -15,18 +10,5 @@ $core-timer-iterations: 15 !default; | |||||||
|         span { |         span { | ||||||
|             margin-right: 5px; |             margin-right: 5px; | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|         // Create the timer warning colors. |  | ||||||
|         @for $i from 0 through $core-timer-iterations { |  | ||||||
|             &.core-timer-timeleft-#{$i} { |  | ||||||
|                 background-color: rgba($core-timer-warn-color, 1 - ($i / $core-timer-iterations)) !important; |  | ||||||
| 
 |  | ||||||
|                 @if $i <= $core-timer-iterations / 2 { |  | ||||||
|                     label, span, ion-icon { |  | ||||||
|                         color: var(--white); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -32,10 +32,15 @@ export class CoreTimerComponent implements OnInit, OnDestroy { | |||||||
|     @Input() endTime?: string | number; // Timestamp (in seconds) when the timer should end.
 |     @Input() endTime?: string | number; // Timestamp (in seconds) when the timer should end.
 | ||||||
|     @Input() timerText?: string; // Text to show next to the timer. If not defined, no text shown.
 |     @Input() timerText?: string; // Text to show next to the timer. If not defined, no text shown.
 | ||||||
|     @Input() timeLeftClass?: string; // Name of the class to apply with each second. By default, 'core-timer-timeleft-'.
 |     @Input() timeLeftClass?: string; // Name of the class to apply with each second. By default, 'core-timer-timeleft-'.
 | ||||||
|  |     @Input() timeLeftClassThreshold = 100; // Number of seconds to start adding the timeLeftClass. Set it to -1 to not add it.
 | ||||||
|     @Input() align?: string; // Where to align the time and text. Defaults to 'left'. Other values: 'center', 'right'.
 |     @Input() align?: string; // Where to align the time and text. Defaults to 'left'. Other values: 'center', 'right'.
 | ||||||
|  |     @Input() timeUpText?: string; // Text to show when the timer reaches 0. If not defined, 'core.timesup'.
 | ||||||
|  |     @Input() mode: CoreTimerMode = CoreTimerMode.ITEM; // How to display data.
 | ||||||
|  |     @Input() underTimeClassThresholds = []; // Number of seconds to add the class 'core-timer-under-'.
 | ||||||
|     @Output() finished = new EventEmitter<void>(); // Will emit an event when the timer reaches 0.
 |     @Output() finished = new EventEmitter<void>(); // Will emit an event when the timer reaches 0.
 | ||||||
| 
 | 
 | ||||||
|     timeLeft?: number; // Seconds left to end.
 |     timeLeft?: number; // Seconds left to end.
 | ||||||
|  |     modeBasic = CoreTimerMode.BASIC; | ||||||
| 
 | 
 | ||||||
|     protected timeInterval?: number; |     protected timeInterval?: number; | ||||||
|     protected element?: HTMLElement; |     protected element?: HTMLElement; | ||||||
| @ -50,31 +55,51 @@ export class CoreTimerComponent implements OnInit, OnDestroy { | |||||||
|     ngOnInit(): void { |     ngOnInit(): void { | ||||||
|         const timeLeftClass = this.timeLeftClass || 'core-timer-timeleft-'; |         const timeLeftClass = this.timeLeftClass || 'core-timer-timeleft-'; | ||||||
|         const endTime = Math.round(Number(this.endTime)); |         const endTime = Math.round(Number(this.endTime)); | ||||||
|         const container: HTMLElement | undefined = this.elementRef.nativeElement.querySelector('.core-timer'); |         this.underTimeClassThresholds.sort((a, b) => a - b); // Sort by increase order.
 | ||||||
| 
 | 
 | ||||||
|         if (!endTime) { |         if (!endTime) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         let container: HTMLElement | undefined; | ||||||
|  | 
 | ||||||
|         // Check time left every 200ms.
 |         // Check time left every 200ms.
 | ||||||
|         this.timeInterval = window.setInterval(() => { |         this.timeInterval = window.setInterval(() => { | ||||||
|             this.timeLeft = endTime - CoreTimeUtils.timestamp(); |             container = container || this.elementRef.nativeElement.querySelector('.core-timer'); | ||||||
|  |             this.timeLeft = Math.max(endTime - CoreTimeUtils.timestamp(), 0); | ||||||
| 
 | 
 | ||||||
|             if (this.timeLeft < 0) { |             if (container) { | ||||||
|  |                 // Add class if timer is below timeLeftClassThreshold.
 | ||||||
|  |                 if (this.timeLeft < this.timeLeftClassThreshold && !container.classList.contains(timeLeftClass + this.timeLeft)) { | ||||||
|  |                     // Time left has changed. Remove previous classes and add the new one.
 | ||||||
|  |                     container.classList.remove(timeLeftClass + (this.timeLeft + 1)); | ||||||
|  |                     container.classList.remove(timeLeftClass + (this.timeLeft + 2)); | ||||||
|  |                     container.classList.add(timeLeftClass + this.timeLeft); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // Add classes for underTimeClassThresholds.
 | ||||||
|  |                 for (let i = 0; i < this.underTimeClassThresholds.length; i++) { | ||||||
|  |                     const threshold = this.underTimeClassThresholds[i]; | ||||||
|  |                     if (this.timeLeft <= threshold) { | ||||||
|  |                         if (!container.classList.contains('core-timer-under-' + this.timeLeft)) { | ||||||
|  |                             // Add new class and remove the previous one.
 | ||||||
|  |                             const nextTreshold = this.underTimeClassThresholds[i + 1]; | ||||||
|  |                             container.classList.add('core-timer-under-' + threshold); | ||||||
|  |                             nextTreshold && container.classList.remove('core-timer-under-' + nextTreshold); | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         break; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (this.timeLeft === 0) { | ||||||
|                 // Time is up! Stop the timer and call the finish function.
 |                 // Time is up! Stop the timer and call the finish function.
 | ||||||
|                 clearInterval(this.timeInterval); |                 clearInterval(this.timeInterval); | ||||||
|                 this.finished.emit(); |                 this.finished.emit(); | ||||||
| 
 | 
 | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
| 
 |  | ||||||
|             // If the time has nearly expired, change the color.
 |  | ||||||
|             if (this.timeLeft < 100 && container && !container.classList.contains(timeLeftClass + this.timeLeft)) { |  | ||||||
|                 // Time left has changed. Remove previous classes and add the new one.
 |  | ||||||
|                 container.classList.remove(timeLeftClass + (this.timeLeft + 1)); |  | ||||||
|                 container.classList.remove(timeLeftClass + (this.timeLeft + 2)); |  | ||||||
|                 container.classList.add(timeLeftClass + this.timeLeft); |  | ||||||
|             } |  | ||||||
|         }, 200); |         }, 200); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -86,3 +111,8 @@ export class CoreTimerComponent implements OnInit, OnDestroy { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export enum CoreTimerMode { | ||||||
|  |     ITEM = 'item', | ||||||
|  |     BASIC = 'basic', | ||||||
|  | } | ||||||
|  | |||||||
| @ -18,6 +18,7 @@ import { CoreLang } from '@services/lang'; | |||||||
| import { CoreLocalNotifications } from '@services/local-notifications'; | import { CoreLocalNotifications } from '@services/local-notifications'; | ||||||
| import { CoreSites } from '@services/sites'; | import { CoreSites } from '@services/sites'; | ||||||
| import { CoreUpdateManager } from '@services/update-manager'; | import { CoreUpdateManager } from '@services/update-manager'; | ||||||
|  | import { CoreTimeUtils } from '@services/utils/time'; | ||||||
| 
 | 
 | ||||||
| export default async function(): Promise<void> { | export default async function(): Promise<void> { | ||||||
|     await Promise.all([ |     await Promise.all([ | ||||||
| @ -27,5 +28,6 @@ export default async function(): Promise<void> { | |||||||
|         CoreLang.initialize(), |         CoreLang.initialize(), | ||||||
|         CoreLocalNotifications.initialize(), |         CoreLocalNotifications.initialize(), | ||||||
|         CoreUpdateManager.initialize(), |         CoreUpdateManager.initialize(), | ||||||
|  |         CoreTimeUtils.initialize(), | ||||||
|     ]); |     ]); | ||||||
| } | } | ||||||
|  | |||||||
| @ -14,7 +14,7 @@ | |||||||
| 
 | 
 | ||||||
| import { Pipe, PipeTransform } from '@angular/core'; | import { Pipe, PipeTransform } from '@angular/core'; | ||||||
| import { CoreLogger } from '@singletons/logger'; | import { CoreLogger } from '@singletons/logger'; | ||||||
| import { CoreTimeUtils } from '@services/utils/time'; | import { CoreTime } from '@singletons/time'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Filter to turn a number of seconds to a duration. E.g. 60 -> 1 minute. |  * Filter to turn a number of seconds to a duration. E.g. 60 -> 1 minute. | ||||||
| @ -48,7 +48,7 @@ export class CoreDurationPipe implements PipeTransform { | |||||||
|             seconds = numberSeconds; |             seconds = numberSeconds; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return CoreTimeUtils.formatTime(seconds); |         return CoreTime.formatTime(seconds); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -14,7 +14,7 @@ | |||||||
| 
 | 
 | ||||||
| import { Injectable, SimpleChange, ElementRef, KeyValueChanges } from '@angular/core'; | import { Injectable, SimpleChange, ElementRef, KeyValueChanges } from '@angular/core'; | ||||||
| import { IonContent } from '@ionic/angular'; | import { IonContent } from '@ionic/angular'; | ||||||
| import { ModalOptions, PopoverOptions, AlertOptions, AlertButton, TextFieldTypes, getMode } from '@ionic/core'; | import { ModalOptions, PopoverOptions, AlertOptions, AlertButton, TextFieldTypes, getMode, ToastOptions } from '@ionic/core'; | ||||||
| import { Md5 } from 'ts-md5'; | import { Md5 } from 'ts-md5'; | ||||||
| 
 | 
 | ||||||
| import { CoreApp } from '@services/app'; | import { CoreApp } from '@services/app'; | ||||||
| @ -1633,6 +1633,24 @@ export class CoreDomUtilsProvider { | |||||||
|         return loader; |         return loader; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Show toast with some options. | ||||||
|  |      * | ||||||
|  |      * @param options Options. | ||||||
|  |      * @return Promise resolved with Toast instance. | ||||||
|  |      */ | ||||||
|  |     async showToastWithOptions(options: ToastOptions): Promise<HTMLIonToastElement> { | ||||||
|  |         // Set some default values.
 | ||||||
|  |         options.duration = options.duration ?? 2000; | ||||||
|  |         options.position = options.position ?? 'bottom'; | ||||||
|  | 
 | ||||||
|  |         const loader = await ToastController.create(options); | ||||||
|  | 
 | ||||||
|  |         await loader.present(); | ||||||
|  | 
 | ||||||
|  |         return loader; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Stores a component/directive instance. |      * Stores a component/directive instance. | ||||||
|      * |      * | ||||||
|  | |||||||
| @ -15,8 +15,8 @@ | |||||||
| import { Injectable } from '@angular/core'; | import { Injectable } from '@angular/core'; | ||||||
| 
 | 
 | ||||||
| import moment, { LongDateFormatKey } from 'moment'; | import moment, { LongDateFormatKey } from 'moment'; | ||||||
| import { CoreConstants } from '@/core/constants'; |  | ||||||
| import { makeSingleton, Translate } from '@singletons'; | import { makeSingleton, Translate } from '@singletons'; | ||||||
|  | import { CoreTime } from '@singletons/time'; | ||||||
| 
 | 
 | ||||||
| /* | /* | ||||||
|  * "Utils" service with helper functions for date and time. |  * "Utils" service with helper functions for date and time. | ||||||
| @ -68,6 +68,20 @@ export class CoreTimeUtilsProvider { | |||||||
|         '%%': '%', |         '%%': '%', | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Initialize. | ||||||
|  |      */ | ||||||
|  |     initialize(): void { | ||||||
|  |         // Set relative time thresholds for humanize(), otherwise for example 47 minutes were converted to 'an hour'.
 | ||||||
|  |         moment.relativeTimeThreshold('s', 60); | ||||||
|  |         moment.relativeTimeThreshold('m', 60); | ||||||
|  |         moment.relativeTimeThreshold('h', 24); | ||||||
|  |         moment.relativeTimeThreshold('d', 31); | ||||||
|  |         moment.relativeTimeThreshold('M', 12); | ||||||
|  |         moment.relativeTimeThreshold('y', 365); | ||||||
|  |         moment.relativeTimeThreshold('ss', 0); // To display exact number of seconds instead of just "a few seconds".
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Convert a PHP format to a Moment format. |      * Convert a PHP format to a Moment format. | ||||||
|      * |      * | ||||||
| @ -142,69 +156,26 @@ export class CoreTimeUtilsProvider { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Returns hours, minutes and seconds in a human readable format |      * Returns years, months, days, hours, minutes and seconds in a human readable format. | ||||||
|      * |      * | ||||||
|      * @param seconds A number of seconds |      * @param seconds A number of seconds | ||||||
|  |      * @param precision Number of elements to have in precision. | ||||||
|      * @return Seconds in a human readable format. |      * @return Seconds in a human readable format. | ||||||
|  |      * @deprecated since app 4.0. Use CoreTime.formatTime instead. | ||||||
|      */ |      */ | ||||||
|     formatTime(seconds: number): string { |     formatTime(seconds: number, precision = 2): string { | ||||||
|         const totalSecs = Math.abs(seconds); |         return CoreTime.formatTime(seconds, precision); | ||||||
|         const years = Math.floor(totalSecs / CoreConstants.SECONDS_YEAR); |  | ||||||
|         let remainder = totalSecs - (years * CoreConstants.SECONDS_YEAR); |  | ||||||
|         const days = Math.floor(remainder / CoreConstants.SECONDS_DAY); |  | ||||||
| 
 |  | ||||||
|         remainder = totalSecs - (days * CoreConstants.SECONDS_DAY); |  | ||||||
| 
 |  | ||||||
|         const hours = Math.floor(remainder / CoreConstants.SECONDS_HOUR); |  | ||||||
|         remainder = remainder - (hours * CoreConstants.SECONDS_HOUR); |  | ||||||
| 
 |  | ||||||
|         const mins = Math.floor(remainder / CoreConstants.SECONDS_MINUTE); |  | ||||||
|         const secs = remainder - (mins * CoreConstants.SECONDS_MINUTE); |  | ||||||
| 
 |  | ||||||
|         const ss = Translate.instant('core.' + (secs == 1 ? 'sec' : 'secs')); |  | ||||||
|         const sm = Translate.instant('core.' + (mins == 1 ? 'min' : 'mins')); |  | ||||||
|         const sh = Translate.instant('core.' + (hours == 1 ? 'hour' : 'hours')); |  | ||||||
|         const sd = Translate.instant('core.' + (days == 1 ? 'day' : 'days')); |  | ||||||
|         const sy = Translate.instant('core.' + (years == 1 ? 'year' : 'years')); |  | ||||||
|         let oyears = ''; |  | ||||||
|         let odays = ''; |  | ||||||
|         let ohours = ''; |  | ||||||
|         let omins = ''; |  | ||||||
|         let osecs = ''; |  | ||||||
| 
 |  | ||||||
|         if (years) { |  | ||||||
|             oyears = years + ' ' + sy; |  | ||||||
|         } |  | ||||||
|         if (days) { |  | ||||||
|             odays = days + ' ' + sd; |  | ||||||
|         } |  | ||||||
|         if (hours) { |  | ||||||
|             ohours = hours + ' ' + sh; |  | ||||||
|         } |  | ||||||
|         if (mins) { |  | ||||||
|             omins = mins + ' ' + sm; |  | ||||||
|         } |  | ||||||
|         if (secs) { |  | ||||||
|             osecs = secs + ' ' + ss; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|         if (years) { |     /** | ||||||
|             return oyears + ' ' + odays; |      * Converts a number of seconds into a short human readable format: minutes and seconds, in fromat: 3' 27''. | ||||||
|         } |      * | ||||||
|         if (days) { |      * @param seconds Seconds | ||||||
|             return odays + ' ' + ohours; |      * @return Short human readable text. | ||||||
|         } |      * @deprecated since app 4.0. Use CoreTime.formatTimeShort instead. | ||||||
|         if (hours) { |      */ | ||||||
|             return ohours + ' ' + omins; |     formatTimeShort(duration: number): string { | ||||||
|         } |         return CoreTime.formatTimeShort(duration); | ||||||
|         if (mins) { |  | ||||||
|             return omins + ' ' + osecs; |  | ||||||
|         } |  | ||||||
|         if (secs) { |  | ||||||
|             return osecs; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return Translate.instant('core.now'); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -213,35 +184,10 @@ export class CoreTimeUtilsProvider { | |||||||
|      * @param duration Duration in seconds |      * @param duration Duration in seconds | ||||||
|      * @param precision Number of elements to have in precision. 0 or undefined to full precission. |      * @param precision Number of elements to have in precision. 0 or undefined to full precission. | ||||||
|      * @return Duration in a human readable format. |      * @return Duration in a human readable format. | ||||||
|  |      * @deprecated since app 4.0. Use CoreTime.formatTime instead. | ||||||
|      */ |      */ | ||||||
|     formatDuration(duration: number, precision?: number): string { |     formatDuration(duration: number, precision?: number): string { | ||||||
|         precision = precision || 5; |         return CoreTime.formatTime(duration, precision); | ||||||
| 
 |  | ||||||
|         const eventDuration = moment.duration(duration, 'seconds'); |  | ||||||
|         let durationString = ''; |  | ||||||
| 
 |  | ||||||
|         if (precision && eventDuration.years() > 0) { |  | ||||||
|             durationString += ' ' + moment.duration(eventDuration.years(), 'years').humanize(); |  | ||||||
|             precision--; |  | ||||||
|         } |  | ||||||
|         if (precision && eventDuration.months() > 0) { |  | ||||||
|             durationString += ' ' + moment.duration(eventDuration.months(), 'months').humanize(); |  | ||||||
|             precision--; |  | ||||||
|         } |  | ||||||
|         if (precision && eventDuration.days() > 0) { |  | ||||||
|             durationString += ' ' + moment.duration(eventDuration.days(), 'days').humanize(); |  | ||||||
|             precision--; |  | ||||||
|         } |  | ||||||
|         if (precision && eventDuration.hours() > 0) { |  | ||||||
|             durationString += ' ' + moment.duration(eventDuration.hours(), 'hours').humanize(); |  | ||||||
|             precision--; |  | ||||||
|         } |  | ||||||
|         if (precision && eventDuration.minutes() > 0) { |  | ||||||
|             durationString += ' ' + moment.duration(eventDuration.minutes(), 'minutes').humanize(); |  | ||||||
|             precision--; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return durationString.trim(); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -249,21 +195,10 @@ export class CoreTimeUtilsProvider { | |||||||
|      * |      * | ||||||
|      * @param duration Duration in seconds |      * @param duration Duration in seconds | ||||||
|      * @return Duration in a short human readable format. |      * @return Duration in a short human readable format. | ||||||
|  |      * @deprecated since app 4.0. Use CoreTime.formatTimeShort instead. | ||||||
|      */ |      */ | ||||||
|     formatDurationShort(duration: number): string { |     formatDurationShort(duration: number): string { | ||||||
|         const minutes = Math.floor(duration / 60); |         return CoreTime.formatTimeShort(duration); | ||||||
|         const seconds = duration - minutes * 60; |  | ||||||
|         const durations = <string[]>[]; |  | ||||||
| 
 |  | ||||||
|         if (minutes > 0) { |  | ||||||
|             durations.push(minutes + '\''); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (seconds > 0 || minutes === 0) { |  | ||||||
|             durations.push(seconds + '\'\''); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return durations.join(' '); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -12,11 +12,76 @@ | |||||||
| // See the License for the specific language governing permissions and
 | // See the License for the specific language governing permissions and
 | ||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
|  | import moment from 'moment'; | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Singleton with helper functions for time operations. |  * Singleton with helper functions for time operations. | ||||||
|  */ |  */ | ||||||
| export class CoreTime { | export class CoreTime { | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns years, months, days, hours, minutes and seconds in a human readable format. | ||||||
|  |      * | ||||||
|  |      * @param seconds A number of seconds | ||||||
|  |      * @param precision Number of elements to have in precision. | ||||||
|  |      * @return Seconds in a human readable format. | ||||||
|  |      */ | ||||||
|  |     static formatTime(seconds: number, precision = 2): string { | ||||||
|  |         precision = precision || 6; // Use max precision if 0 is passed.
 | ||||||
|  | 
 | ||||||
|  |         const eventDuration = moment.duration(Math.abs(seconds), 'seconds'); | ||||||
|  |         let durationString = ''; | ||||||
|  | 
 | ||||||
|  |         if (precision && eventDuration.years() > 0) { | ||||||
|  |             durationString += ' ' + moment.duration(eventDuration.years(), 'years').humanize(); | ||||||
|  |             precision--; | ||||||
|  |         } | ||||||
|  |         if (precision && eventDuration.months() > 0) { | ||||||
|  |             durationString += ' ' + moment.duration(eventDuration.months(), 'months').humanize(); | ||||||
|  |             precision--; | ||||||
|  |         } | ||||||
|  |         if (precision && eventDuration.days() > 0) { | ||||||
|  |             durationString += ' ' + moment.duration(eventDuration.days(), 'days').humanize(); | ||||||
|  |             precision--; | ||||||
|  |         } | ||||||
|  |         if (precision && eventDuration.hours() > 0) { | ||||||
|  |             durationString += ' ' + moment.duration(eventDuration.hours(), 'hours').humanize(); | ||||||
|  |             precision--; | ||||||
|  |         } | ||||||
|  |         if (precision && eventDuration.minutes() > 0) { | ||||||
|  |             durationString += ' ' + moment.duration(eventDuration.minutes(), 'minutes').humanize(); | ||||||
|  |             precision--; | ||||||
|  |         } | ||||||
|  |         if (precision && (eventDuration.seconds() > 0 || !durationString)) { | ||||||
|  |             durationString += ' ' + moment.duration(eventDuration.seconds(), 'seconds').humanize(); | ||||||
|  |             precision--; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return durationString.trim(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Converts a number of seconds into a short human readable format: minutes and seconds, in fromat: 3' 27''. | ||||||
|  |      * | ||||||
|  |      * @param seconds Seconds | ||||||
|  |      * @return Short human readable text. | ||||||
|  |      */ | ||||||
|  |     static formatTimeShort(duration: number): string { | ||||||
|  |         const minutes = Math.floor(duration / 60); | ||||||
|  |         const seconds = duration - minutes * 60; | ||||||
|  |         const durations = <string[]>[]; | ||||||
|  | 
 | ||||||
|  |         if (minutes > 0) { | ||||||
|  |             durations.push(minutes + '\''); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (seconds > 0 || minutes === 0) { | ||||||
|  |             durations.push(seconds + '\'\''); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return durations.join(' '); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Wrap a function so that it is called only once. |      * Wrap a function so that it is called only once. | ||||||
|      * |      * | ||||||
|  | |||||||
| @ -504,10 +504,19 @@ ion-toast { | |||||||
|     @include media-breakpoint-down(sm) { |     @include media-breakpoint-down(sm) { | ||||||
|         &::part(container) { |         &::part(container) { | ||||||
|             flex-direction: column; |             flex-direction: column; | ||||||
|  |             align-items: flex-end; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @each $color-name, $unused in $colors { | ||||||
|  |     ion-toast.core-#{$color-name}-toast { | ||||||
|  |         --background: var(--ion-color-#{$color-name}-tint); | ||||||
|  |         --color: var(--ion-color-#{$color-name}-shade); | ||||||
|  |         --button-color: var(--ion-color-#{$color-name}-shade); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // Ionic list. | // Ionic list. | ||||||
| ion-list { | ion-list { | ||||||
|     padding: 0 !important; |     padding: 0 !important; | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user