MOBILE-3919 assign: Support time limit in assigns

main
Dani Palou 2022-03-04 09:42:18 +01:00
parent f02ba784e3
commit 1da073eefa
23 changed files with 536 additions and 213 deletions

View File

@ -331,14 +331,18 @@
"addon.mod_assign.allowsubmissionsfromdatesummary": "assign",
"addon.mod_assign.applytoteam": "assign",
"addon.mod_assign.assignmentisdue": "assign",
"addon.mod_assign.assigntimeleft": "assign",
"addon.mod_assign.attemptnumber": "assign",
"addon.mod_assign.attemptreopenmethod": "assign",
"addon.mod_assign.attemptreopenmethod_manual": "assign",
"addon.mod_assign.attemptreopenmethod_untilpass": "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.cannotgradefromapp": "local_moodlemobileapp",
"addon.mod_assign.cannotsubmitduetostatementsubmission": "local_moodlemobileapp",
"addon.mod_assign.confirmstart": "assign",
"addon.mod_assign.confirmsubmission": "assign",
"addon.mod_assign.currentattempt": "assign",
"addon.mod_assign.currentattemptof": "assign",
@ -416,7 +420,10 @@
"addon.mod_assign.submitassignment_help": "assign",
"addon.mod_assign.submittedearly": "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.timelimit": "assign",
"addon.mod_assign.timemodified": "assign",
"addon.mod_assign.timeremaining": "assign",
"addon.mod_assign.ungroupedusers": "assign",

View File

@ -33,6 +33,14 @@
</ion-select>
</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-label>
<h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2>

View File

@ -86,6 +86,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
protected savedObserver?: CoreEventObserver;
protected submittedObserver?: CoreEventObserver;
protected gradedObserver?: CoreEventObserver;
protected startedObserver?: CoreEventObserver;
constructor(
protected content?: IonContent,
@ -136,6 +137,13 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
}
}, 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);
}
@ -167,12 +175,17 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
if (submissions.canviewsubmissions) {
// Calculate the messages to display about time remaining and late submissions.
this.timeRemaining = '';
this.lateSubmissions = '';
if (this.assign.duedate > 0) {
if (this.assign.duedate - time <= 0) {
this.timeRemaining = Translate.instant('addon.mod_assign.assignmentisdue');
} else {
this.timeRemaining = CoreTimeUtils.formatDuration(this.assign.duedate - time, 3);
this.timeRemaining = CoreTimeUtils.formatTime(this.assign.duedate - time);
}
if (this.assign.duedate < time) {
if (this.assign.cutoffdate) {
if (this.assign.cutoffdate > time) {
this.lateSubmissions = Translate.instant(
@ -182,13 +195,8 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
} else {
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.
@ -398,6 +406,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
this.savedObserver?.off();
this.submittedObserver?.off();
this.gradedObserver?.off();
this.startedObserver?.off();
}
}

View File

@ -1,5 +1,15 @@
<core-loading [hideUntil]="loaded">
<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. -->
<ion-item class="ion-text-wrap" *ngIf="!blindMarking && user" core-user-link [userId]="submitId" [courseId]="courseId"
[attr.aria-label]="user!.fullname">
@ -31,33 +41,36 @@
<!-- View the submission tab. -->
<core-tab [title]="'addon.mod_assign.submission' | translate" id="submission">
<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. -->
<ion-item class="ion-text-wrap"
*ngIf="userSubmission && userSubmission!.status != statusNew && userSubmission!.timemodified">
<ion-item class="ion-text-wrap" *ngIf="currentAttempt && !isGrading">
<ion-label>
<h2>{{ 'addon.mod_assign.timemodified' | translate }}</h2>
<p>{{ userSubmission!.timemodified * 1000 | coreFormatDate }}</p>
<h2>{{ 'addon.mod_assign.attemptnumber' | translate }}</h2>
<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-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>
<h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2>
<p [innerHTML]="timeRemaining"></p>
<h2>{{ 'addon.mod_assign.submissionslocked' | translate }}</h2>
</ion-label>
</ion-item>
<!-- Dates. -->
<ion-item class="ion-text-wrap" *ngIf="showDates && fromDate && !isSubmittedForGrading">
<ion-label>
<p *ngIf="assign!.intro"
[innerHTML]="'addon.mod_assign.allowsubmissionsfromdatesummary' | translate: {'$a': fromDate}">
</p>
<p *ngIf="!assign!.intro" [innerHTML]="'addon.mod_assign.allowsubmissionsanddescriptionfromdatesummary' | translate:
{'$a': fromDate}">
{'$a': fromDate}">
</p>
</ion-label>
</ion-item>
@ -84,20 +97,48 @@
</ion-label>
</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>
<h2>{{ 'addon.mod_assign.attemptnumber' | translate }}</h2>
<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>
<h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2>
<p *ngIf="!timeLimitEndTime" [innerHTML]="timeRemaining"></p>
<core-timer *ngIf="timeLimitEndTime > 0" [endTime]="timeLimitEndTime" mode="basic" timeUpText="00:00:00"
[timeLeftClassThreshold]="-1" [underTimeClassThresholds]="[300, 900]" (finished)="timeUp()">
</core-timer>
</ion-label>
</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. -->
<ion-item class="ion-text-wrap" *ngIf="canEdit">
<ion-label>
@ -109,7 +150,12 @@
<!-- If no submission or is new, show add submission. -->
<ion-button expand="block" class="ion-text-wrap" (click)="goToEdit()" *ngIf="!hasOffline &&
(!userSubmission || !userSubmission!.status || userSubmission!.status == statusNew)">
{{ 'addon.mod_assign.addsubmission' | translate }}
<ng-container *ngIf="!assign?.timelimit || userSubmission?.timestarted">
{{ 'addon.mod_assign.addsubmission' | translate }}
</ng-container>
<ng-container *ngIf="assign?.timelimit && (!userSubmission || !userSubmission.timestarted)">
{{ 'addon.mod_assign.beginassignment' | translate }}
</ng-container>
</ion-button>
<!-- If reopened, show addfromprevious and addnewattempt. -->
<ng-container *ngIf="!hasOffline && userSubmission?.status == statusReopened">
@ -122,9 +168,8 @@
</ion-button>
</ng-container>
<!-- Else show editsubmission. -->
<ion-button expand="block" class="ion-text-wrap" *ngIf="!hasOffline &&
userSubmission && userSubmission!.status &&
userSubmission!.status != statusNew &&
<ion-button expand="block" class="ion-text-wrap" *ngIf="!hasOffline && userSubmission &&
userSubmission!.status && userSubmission!.status != statusNew &&
userSubmission!.status != statusReopened" (click)="goToEdit()">
{{ 'addon.mod_assign.editsubmission' | translate }}
</ion-button>
@ -191,23 +236,6 @@
</ion-item>
</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>
</core-tab>

View File

@ -1,20 +1,20 @@
:host ::ng-deep {
div.latesubmission,
div.overdue {
ion-item.latesubmission,
ion-item.overdue {
border-bottom: 3px solid var(--danger) !important;
ion-icon {
color: var(--danger);
}
}
div.earlysubmission {
ion-item.earlysubmission {
border-bottom: 3px solid var(--success) !important;
ion-icon {
color: var(--success);
}
}
div.submissioneditable p {
ion-item.submissioneditable p {
color: var(--danger);
}
@ -26,10 +26,22 @@
margin-left: 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 {
div.submissioneditable p {
ion-item.submissioneditable p {
color: var(--danger-tint);
}
}

View File

@ -106,6 +106,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
submissionPlugins: AddonModAssignPlugin[] = []; // List of submission plugins.
timeRemaining = ''; // Message about time remaining.
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.
statusColor = ''; // Color to apply to the status.
unsupportedEditPlugins: string[] = []; // List of submission plugins that don't support edit.
@ -126,6 +127,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
gradeUrl?: string; // URL to grade in browser.
isPreviousAttemptEmpty = true; // Whether the previous attempt contains an empty submission.
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.
statusNew = AddonModAssignSubmissionStatusValues.NEW;
@ -200,7 +202,12 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
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.timeRemainingClass = '';
@ -208,53 +215,53 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
}
const time = CoreTimeUtils.timestamp();
const dueDate = response.lastattempt?.extensionduedate
? response.lastattempt.extensionduedate
: this.assign.duedate;
const timeRemaining = dueDate - time;
const timeLimitEnabled = this.assign.timelimit && submissionStarted;
const dueDateReached = this.assign.duedate > 0 && this.assign.duedate - time <= 0;
const timeLimitEnabledBeforeDueDate = timeLimitEnabled && !dueDateReached;
if (timeRemaining > 0) {
this.timeRemaining = CoreTimeUtils.formatDuration(timeRemaining, 3);
this.timeRemainingClass = '';
if (this.userSubmission && this.userSubmission.status === AddonModAssignSubmissionStatusValues.SUBMITTED) {
// Submitted, display the relevant early/late message.
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;
return;
}
// Not submitted.
if (!this.userSubmission || this.userSubmission.status != AddonModAssignSubmissionStatusValues.SUBMITTED) {
if (response.lastattempt?.submissionsenabled || response.gradingsummary?.submissionsenabled) {
this.timeRemaining = Translate.instant(
'addon.mod_assign.overdue',
{ $a: CoreTimeUtils.formatDuration(-timeRemaining, 3) },
);
this.timeRemainingClass = 'overdue';
return;
}
this.timeRemaining = Translate.instant('addon.mod_assign.duedatereached');
this.timeRemainingClass = '';
return;
}
const timeSubmittedDiff = this.userSubmission.timemodified - dueDate;
if (timeSubmittedDiff > 0) {
this.timeRemaining = Translate.instant(
'addon.mod_assign.submittedlate',
{ $a: CoreTimeUtils.formatDuration(timeSubmittedDiff, 2) },
'addon.mod_assign.' + (onTime ? earlyString : lateString),
{ $a: CoreTimeUtils.formatTime(Math.abs(lateCalculation - lateThreshold)) },
);
this.timeRemainingClass = 'latesubmission';
this.timeRemainingClass = onTime ? 'earlysubmission' : 'latesubmission';
return;
}
this.timeRemaining = Translate.instant(
'addon.mod_assign.submittedearly',
{ $a: CoreTimeUtils.formatDuration(-timeSubmittedDiff, 2) },
);
this.timeRemainingClass = 'earlysubmission';
if (dueDateReached) {
// There is no submission, due date has passed, show assignment is overdue.
const submissionsEnabled = response.lastattempt?.submissionsenabled || response.gradingsummary?.submissionsenabled;
this.timeRemaining = Translate.instant(
'addon.mod_assign.' + (submissionsEnabled ? 'overdue' : 'duedatereached'),
{ $a: CoreTimeUtils.formatTime(time - this.assign.duedate) },
);
this.timeRemainingClass = 'overdue';
this.timeLimitFinished = true;
return;
}
if (timeLimitEnabled && submissionStarted) {
// 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;
}
// Assignment is not overdue, and no submission has been made. Just display the due date.
this.timeRemaining = CoreTimeUtils.formatTime(this.assign.duedate - time);
this.timeRemainingClass = 'timeremaining';
}
/**
@ -292,7 +299,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
if (!this.previousAttempt?.submission) {
// Cannot access previous attempts, just go to edit.
return this.goToEdit();
return this.goToEdit(true);
}
const previousSubmission = this.previousAttempt.submission;
@ -319,7 +326,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
try {
await AddonModAssignHelper.copyPreviousAttempt(this.assign, previousSubmission);
// Now go to edit.
this.goToEdit();
this.goToEdit(true);
if (!this.assign.submissiondrafts && this.userSubmission) {
// No drafts allowed, so it was submitted. Trigger event.
@ -352,8 +359,24 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
/**
* 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: CoreTimeUtils.formatTime(this.assign.timelimit),
}),
undefined,
Translate.instant('addon.mod_assign.beginassignment'),
);
} catch {
return; // User canceled.
}
}
CoreNavigator.navigateToSitePath(
AddonModAssignModuleHandlerService.PAGE_NAME + '/' + this.courseId + '/' + this.moduleId + '/edit',
{
@ -1175,6 +1198,13 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
this.setGradeSyncBlocked(tab.id === 'grade');
}
/**
* Function called when the time is up.
*/
timeUp(): void {
this.timeLimitFinished = true;
}
/**
* Component being destroyed.
*/

View File

@ -9,14 +9,18 @@
"allowsubmissionsfromdatesummary": "This assignment will accept submissions from <strong>{{$a}}</strong>",
"applytoteam": "Apply grades and feedback to entire group",
"assignmentisdue": "Assignment is due",
"assigntimeleft": "Time left",
"attemptnumber": "Attempt number",
"attemptreopenmethod": "Additional attempts",
"attemptreopenmethod_manual": "Manually",
"attemptreopenmethod_untilpass": "Automatically until pass",
"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.",
"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.",
"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.",
"currentattempt": "This is attempt {{$a}}.",
"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.",
"submittedearly": "Assignment was submitted {{$a}} early",
"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",
"timelimit": "Time limit",
"timemodified": "Last modified",
"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.",

View File

@ -20,6 +20,11 @@
<core-loading [hideUntil]="loaded">
<ion-list *ngIf="userSubmission && userSubmission.plugins && userSubmission.plugins.length">
<!-- @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>
<form name="addon-mod_assign-edit-form" #editSubmissionForm>
<!-- Submission statement. -->
<ion-item class="ion-text-wrap" *ngIf="submissionStatement">

View 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);
}
}
}
}

View File

@ -32,6 +32,7 @@ import {
AddonModAssignSubmissionStatusOptions,
AddonModAssignGetSubmissionStatusWSResponse,
AddonModAssignSavePluginData,
AddonModAssignSubmissionStatusValues,
} from '../../services/assign';
import { AddonModAssignHelper } from '../../services/assign-helper';
import { AddonModAssignOffline } from '../../services/assign-offline';
@ -44,6 +45,7 @@ import { CoreUtils } from '@services/utils/utils';
@Component({
selector: 'page-addon-mod-assign-edit',
templateUrl: 'edit.html',
styleUrls: ['edit.scss'],
})
export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave {
@ -58,6 +60,7 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave {
submissionStatement?: string; // The submission statement.
submissionStatementAccepted = false; // Whether submission statement is accepted.
loaded = false; // Whether data has been loaded.
timeLimitEndTime = 0; // If time limit is enabled, the end time for the timer.
protected userId: number; // User doing the submission.
protected isBlind = false; // Whether blind is used.
@ -179,6 +182,8 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave {
throw new CoreError(Translate.instant('core.nopermissions', { $a: this.editText }));
}
submissionStatus = await this.startSubmissionIfNeeded(submissionStatus, options);
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.
if (this.assign.requiresubmissionstatement && !this.assign.submissiondrafts && this.userId == currentUserId) {
@ -187,6 +192,12 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave {
this.submissionStatement = undefined;
}
if (this.assign.timelimit && this.userSubmission?.timestarted) {
this.timeLimitEndTime = AddonModAssignHelper.calculateEndTime(this.assign, this.userSubmission);
} else {
this.timeLimitEndTime = 0;
}
try {
// Check if there's any offline data for this submission.
const offlineData = await AddonModAssignOffline.getSubmission(this.assign.id, this.userId);
@ -204,6 +215,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.
*
@ -392,6 +442,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.
*/

View File

@ -44,6 +44,25 @@ import { CoreFileEntry } from '@services/file-helper';
@Injectable({ providedIn: 'root' })
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.
*

View File

@ -49,6 +49,7 @@ declare module '@singletons/events' {
[AddonModAssignProvider.SUBMISSION_SAVED_EVENT]: AddonModAssignSubmissionSavedEventData;
[AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT]: AddonModAssignSubmittedForGradingEventData;
[AddonModAssignProvider.GRADED_EVENT]: AddonModAssignGradedEventData;
[AddonModAssignProvider.STARTED_EVENT]: AddonModAssignStartedEventData;
[AddonModAssignSyncProvider.MANUAL_SYNCED]: AddonModAssignManualSyncData;
[AddonModAssignSyncProvider.AUTO_SYNCED]: AddonModAssignAutoSyncData;
}
@ -73,6 +74,7 @@ export class AddonModAssignProvider {
static readonly SUBMISSION_SAVED_EVENT = 'addon_mod_assign_submission_saved';
static readonly SUBMITTED_FOR_GRADING_EVENT = 'addon_mod_assign_submitted_for_grading';
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
@ -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.
*
@ -1351,6 +1382,11 @@ export type AddonModAssignAssign = {
introformat?: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
introfiles?: 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.
plugins?: AddonModAssignPlugin[]; // Plugins.
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.
gradingstatus: AddonModAssignGradingStates; // Grading status.
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.
feedback?: AddonModAssignSubmissionFeedback; // Feedback for the last attempt.
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[];
};
@ -1715,6 +1761,25 @@ type AddonModAssignSubmitGradingFormWSParams = {
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.
*/
@ -1739,6 +1804,13 @@ export type AddonModAssignSubmissionSavedEventData = AddonModAssignSubmittedForG
*/
export type AddonModAssignGradedEventData = AddonModAssignSubmittedForGradingEventData;
/**
* Data sent by STARTED_EVENT event.
*/
export type AddonModAssignStartedEventData = {
assignmentId: number;
};
/**
* Submission status.
* Constants on LMS starting with ASSIGN_SUBMISSION_STATUS_

View File

@ -87,9 +87,10 @@ export class AddonModAssignPrefetchHandlerService extends CoreCourseActivityPref
try {
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 || [];
files = files.concat(this.getIntroFilesFromInstance(module, assign));
files = files.concat(assign.activityattachments || []);
// Now get the files in the submissions.
const submissionData = await AddonModAssign.getSubmissions(assign.id, { cmId: module.id, siteId });

View File

@ -90,8 +90,8 @@ export class AddonModH5PActivityProvider {
formattedAttempt.durationReadable = '-';
formattedAttempt.durationCompact = '-';
} else {
formattedAttempt.durationReadable = CoreTimeUtils.formatTime(attempt.duration);
formattedAttempt.durationCompact = CoreTimeUtils.formatDurationShort(attempt.duration);
formattedAttempt.durationReadable = CoreTimeUtils.formatTime(attempt.duration, 3);
formattedAttempt.durationCompact = CoreTimeUtils.formatTimeShort(attempt.duration);
}
return formattedAttempt;

View File

@ -30,7 +30,7 @@
<div *ngIf="lesson" [ngClass]='{"addon-mod_lesson-slideshow": lesson.slideshow}'
[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">
</core-timer>

View File

@ -1,3 +1,8 @@
@import "~theme/globals";
$quiz-timer-warn-color: $red !default;
$quiz-timer-iterations: 15 !default;
:host {
.addon-mod_quiz-question-note p {
margin-top: 2px;
@ -7,4 +12,19 @@
ion-content ion-toolbar {
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);
}
}
}
}
}
}

View File

@ -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-label>
<span *ngIf="timeLeft && timeLeft > 0 && 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">
{{ 'core.timesup' | translate }}
</span>
<ng-container *ngTemplateOutlet="timerTemplate"></ng-container>
</ion-label>
</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>

View File

@ -1,8 +1,3 @@
@import "~theme/globals";
$core-timer-warn-color: $red !default;
$core-timer-iterations: 15 !default;
:host {
.core-timer {
--background: transparent !important;
@ -15,18 +10,5 @@ $core-timer-iterations: 15 !default;
span {
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);
}
}
}
}
}
}

View File

@ -32,10 +32,15 @@ export class CoreTimerComponent implements OnInit, OnDestroy {
@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() 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() 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.
timeLeft?: number; // Seconds left to end.
modeBasic = CoreTimerMode.BASIC;
protected timeInterval?: number;
protected element?: HTMLElement;
@ -50,31 +55,51 @@ export class CoreTimerComponent implements OnInit, OnDestroy {
ngOnInit(): void {
const timeLeftClass = this.timeLeftClass || 'core-timer-timeleft-';
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) {
return;
}
let container: HTMLElement | undefined;
// Check time left every 200ms.
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.
clearInterval(this.timeInterval);
this.finished.emit();
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);
}
@ -86,3 +111,8 @@ export class CoreTimerComponent implements OnInit, OnDestroy {
}
}
export enum CoreTimerMode {
ITEM = 'item',
BASIC = 'basic',
}

View File

@ -18,6 +18,7 @@ import { CoreLang } from '@services/lang';
import { CoreLocalNotifications } from '@services/local-notifications';
import { CoreSites } from '@services/sites';
import { CoreUpdateManager } from '@services/update-manager';
import { CoreTimeUtils } from '@services/utils/time';
export default async function(): Promise<void> {
await Promise.all([
@ -27,5 +28,6 @@ export default async function(): Promise<void> {
CoreLang.initialize(),
CoreLocalNotifications.initialize(),
CoreUpdateManager.initialize(),
CoreTimeUtils.initialize(),
]);
}

View File

@ -14,7 +14,7 @@
import { Injectable, SimpleChange, ElementRef, KeyValueChanges } from '@angular/core';
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 { CoreApp } from '@services/app';
@ -1633,6 +1633,24 @@ export class CoreDomUtilsProvider {
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.
*

View File

@ -15,7 +15,6 @@
import { Injectable } from '@angular/core';
import moment, { LongDateFormatKey } from 'moment';
import { CoreConstants } from '@/core/constants';
import { makeSingleton, Translate } from '@singletons';
/*
@ -68,6 +67,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.
*
@ -142,82 +155,16 @@ 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 precision Number of elements to have in precision.
* @return Seconds in a human readable format.
*/
formatTime(seconds: number): string {
const totalSecs = Math.abs(seconds);
const years = Math.floor(totalSecs / CoreConstants.SECONDS_YEAR);
let remainder = totalSecs - (years * CoreConstants.SECONDS_YEAR);
const days = Math.floor(remainder / CoreConstants.SECONDS_DAY);
formatTime(seconds: number, precision = 2): string {
precision = precision || 6; // Use max precision if 0 is passed.
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;
}
if (days) {
return odays + ' ' + ohours;
}
if (hours) {
return ohours + ' ' + omins;
}
if (mins) {
return omins + ' ' + osecs;
}
if (secs) {
return osecs;
}
return Translate.instant('core.now');
}
/**
* Returns hours, minutes and seconds in a human readable format.
*
* @param duration Duration in seconds
* @param precision Number of elements to have in precision. 0 or undefined to full precission.
* @return Duration in a human readable format.
*/
formatDuration(duration: number, precision?: number): string {
precision = precision || 5;
const eventDuration = moment.duration(duration, 'seconds');
const eventDuration = moment.duration(Math.abs(seconds), 'seconds');
let durationString = '';
if (precision && eventDuration.years() > 0) {
@ -240,17 +187,21 @@ export class CoreTimeUtilsProvider {
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();
}
/**
* Returns duration in a short human readable format: minutes and seconds, in fromat: 3' 27''.
* Converts a number of seconds into a short human readable format: minutes and seconds, in fromat: 3' 27''.
*
* @param duration Duration in seconds
* @return Duration in a short human readable format.
* @param seconds Seconds
* @return Short human readable text.
*/
formatDurationShort(duration: number): string {
formatTimeShort(duration: number): string {
const minutes = Math.floor(duration / 60);
const seconds = duration - minutes * 60;
const durations = <string[]>[];
@ -266,6 +217,29 @@ export class CoreTimeUtilsProvider {
return durations.join(' ');
}
/**
* Returns hours, minutes and seconds in a human readable format.
*
* @param duration Duration in seconds
* @param precision Number of elements to have in precision. 0 or undefined to full precission.
* @return Duration in a human readable format.
* @deprecated since 4.0. Use formatTime instead.
*/
formatDuration(duration: number, precision?: number): string {
return this.formatTime(duration, precision);
}
/**
* Returns duration in a short human readable format: minutes and seconds, in fromat: 3' 27''.
*
* @param duration Duration in seconds
* @return Duration in a short human readable format.
* @deprecated since 4.0. Use formatTime instead.
*/
formatDurationShort(duration: number): string {
return this.formatTimeShort(duration);
}
/**
* Return the current timestamp in a "readable" format: YYYYMMDDHHmmSS.
*

View File

@ -504,10 +504,19 @@ ion-toast {
@include media-breakpoint-down(sm) {
&::part(container) {
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.
ion-list {
padding: 0 !important;