Skip to content

Commit 400086b

Browse files
committed
feat: handle progress saving error
1 parent 3279a98 commit 400086b

File tree

2 files changed

+198
-126
lines changed

2 files changed

+198
-126
lines changed

src/app/home/course/course.page.html

Lines changed: 146 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -1,149 +1,174 @@
11
<link rel="stylesheet" href="course.page.scss">
22
<ion-header>
3-
<ion-toolbar>
4-
<ion-buttons slot="start" [routerLink]="'/home/'+year">
5-
<ion-back-button [defaultHref]="'/home/'+year"></ion-back-button>
6-
</ion-buttons>
7-
<ion-title>
8-
{{ course ?? 'Course' }}
9-
@if (courseProgress.duration > 0) {
10-
<small>{{ (courseProgress.duration - courseProgress.viewed) / 3600 | number:'1.0-1' }} hours left</small>
11-
}
12-
</ion-title>
13-
</ion-toolbar>
3+
<ion-toolbar>
4+
<ion-buttons slot="start" [routerLink]="'/home/'+year">
5+
<ion-back-button [defaultHref]="'/home/'+year"></ion-back-button>
6+
</ion-buttons>
7+
<ion-title>
8+
{{ course ?? 'Course' }}
9+
@if (courseProgress.duration > 0) {
10+
<small>{{ (courseProgress.duration - courseProgress.viewed) / 3600 | number:'1.0-1' }} hours left</small>
11+
}
12+
</ion-title>
13+
</ion-toolbar>
1414
</ion-header>
1515

1616
<ion-content>
17-
<ion-grid>
18-
<ion-row>
19-
<ion-col size="12" size-lg [hidden]="!currentVideo">
20-
<video #videoPlayer class="video-js vjs-default-skin fullwidth" controls preload="metadata"
21-
data-setup='{"aspectRatio":"1280:640", "playbackRates": [0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 3]}'
22-
[hidden]="currentVideo && currentVideo.sources.length === 0"
23-
(contextmenu)="preventMouseEvent($event)" tabindex="1">
24-
Your browser does not support the video tag.
25-
</video>
26-
@if (currentVideo) {
27-
<ion-text color="medium">
28-
<p>
29-
{{ currentVideo.title }}@if (currentVideo.lecturer) {
30-
<span> - {{ currentVideo.lecturer }}</span>
31-
}<br/>
32-
<ion-button (click)="setPlaybackSpeed()" (keyup)="setPlaybackSpeed()" fill="outline" size="small" tabindex="0" class="set-playback-speed-button">Set playback speed</ion-button>
33-
@if (currentVideo.sourceExternal?.includes('1.mp4')) {
34-
<span>
17+
<ion-grid>
18+
<ion-row>
19+
<ion-col size="12" size-lg [hidden]="!currentVideo">
20+
<video #videoPlayer class="video-js vjs-default-skin fullwidth" controls preload="metadata"
21+
data-setup='{"aspectRatio":"1280:640", "playbackRates": [0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 3]}'
22+
[hidden]="currentVideo && currentVideo.sources.length === 0"
23+
(contextmenu)="preventMouseEvent($event)" tabindex="1">
24+
Your browser does not support the video tag.
25+
</video>
26+
@if (progressTimedOut) {
27+
<ion-card color="danger">
28+
<ion-card-header>
29+
<ion-card-title>Session timed out.</ion-card-title>
30+
</ion-card-header>
31+
<ion-card-content>
32+
Cannot save your progress. Please refresh the page to continue watching.
33+
You're currently at {{ progressTimedOut }}.
34+
</ion-card-content>
35+
</ion-card>
36+
}
37+
@if (progressNetworkError) {
38+
<ion-card color="danger">
39+
<ion-card-header>
40+
<ion-card-title>Network Error.</ion-card-title>
41+
</ion-card-header>
42+
<ion-card-content>
43+
Cannot save your progress. Please check your internet connection.
44+
</ion-card-content>
45+
</ion-card>
46+
}
47+
@if (currentVideo) {
48+
<ion-text color="medium">
49+
<p>
50+
{{ currentVideo.title }}@if (currentVideo.lecturer) {
51+
<span> - {{ currentVideo.lecturer }}</span>
52+
}<br/>
53+
<ion-button (click)="setPlaybackSpeed()" (keyup)="setPlaybackSpeed()" fill="outline"
54+
size="small" tabindex="0" class="set-playback-speed-button">
55+
Set playback speed
56+
</ion-button>
57+
@if (currentVideo.sourceExternal?.includes('1.mp4')) {
58+
<span>
3559
| <a [href]="sanitize('//player.docchula.com/?url='+currentVideo.sourceExternal)" rel="noreferrer" target="_blank">
3660
Play with Docchula Player</a>
3761
</span>
38-
}
62+
}
3963
@if (isAndroid && currentVideo.sourceExternal) {
40-
<span>
64+
<span>
4165
| <a [href]="sanitize('intent:'+currentVideo.sourceExternal+'#Intent;S.title='+encodeURIComponent(currentVideo.title)+';package=com.mxtech.videoplayer.ad;end')" rel="noreferrer">Play with MX Player</a>
4266
</span>
43-
}
67+
}
4468
@if ((isIos || isAndroid) && currentVideo.sourceExternal) {
45-
<span>
69+
<span>
4670
| <a [href]="sanitize('vlc://'+currentVideo.sourceExternal)" rel="noreferrer">Play with VLC</a>
4771
</span>
48-
}
72+
}
4973
</p>
5074
<p class="ion-hide-md-down small-text">
5175
<strong>Hotkeys</strong>&emsp;&ensp; Space: Pause, ▲/▼: Volume, ◄/►: Seek, F: Fullscreen
5276
</p>
5377
</ion-text>
5478
@if (currentVideo.sources.length === 0) {
55-
<ion-card color="warning">
56-
<ion-card-header>
57-
<ion-card-title>Format not supported</ion-card-title>
58-
</ion-card-header>
59-
<ion-card-content>
60-
Your browser can't play this video. Please try again using Google Chrome or Mozilla Firefox on Windows/Linux/macOS/Android.
61-
</ion-card-content>
62-
</ion-card>
63-
}
64-
@if (currentVideo.attachments.length !== 0) {
65-
<ion-list>
66-
<ion-list-header lines="inset">
67-
<ion-label>Attachments</ion-label>
68-
</ion-list-header>
69-
@for (attachment of currentVideo.attachments; track attachment.src) {
70-
<ion-item>
71-
<ion-label>{{ attachment.name }}</ion-label>
72-
<a [href]="attachment.src" download target="_blank">
73-
<ion-icon name="download" size="small" slot="end"></ion-icon>
74-
</a>
75-
</ion-item>
76-
}
77-
</ion-list>
78-
}
79-
}
80-
</ion-col>
81-
<ion-col size="12" size-lg [ngClass]="currentVideo ? 'scroll-area' : ''">
82-
@if (list$ | async; as list) {
83-
<ion-list>
84-
@for (lecture of list; track lecture.id) {
85-
<ion-item button (click)="viewVideo(lecture)"
86-
[color]="(currentVideo && currentVideo.id && currentVideo.id === lecture.id) ? 'secondary' : ((lastPlayedVideoKey === lecture.id && !currentVideo) ? 'tertiary' : '')">
87-
<ion-label class="ion-text-wrap">
88-
@if (lecture.date) {
89-
<span class="date">{{ lecture.date | date:"dd MMM y" }}</span>
90-
}
91-
@if (lecture.date) {
92-
<span class="date-divider"> | </span>
79+
<ion-card color="warning">
80+
<ion-card-header>
81+
<ion-card-title>Format not supported</ion-card-title>
82+
</ion-card-header>
83+
<ion-card-content>
84+
Your browser can't play this video. Please try again using Google Chrome or Mozilla Firefox on Windows/Linux/macOS/Android.
85+
</ion-card-content>
86+
</ion-card>
87+
}
88+
@if (currentVideo.attachments.length !== 0) {
89+
<ion-list>
90+
<ion-list-header lines="inset">
91+
<ion-label>Attachments</ion-label>
92+
</ion-list-header>
93+
@for (attachment of currentVideo.attachments; track attachment.src) {
94+
<ion-item>
95+
<ion-label>{{ attachment.name }}</ion-label>
96+
<a [href]="attachment.src" download target="_blank">
97+
<ion-icon name="download" size="small" slot="end"></ion-icon>
98+
</a>
99+
</ion-item>
100+
}
101+
</ion-list>
102+
}
93103
}
94-
{{ lecture.title }}
95-
<small>
96-
{{ lecture.lecturer}}
97-
@if (lecture.durationInMin) {
98-
<span class="time-info">- {{ lecture.durationInMin}} min </span>
99-
}
100-
@if (lecture.history.end_time && lecture.history.end_time > 3) {
101-
<span class="time-info">
104+
</ion-col>
105+
<ion-col size="12" size-lg [ngClass]="currentVideo ? 'scroll-area' : ''">
106+
@if (list$ | async; as list) {
107+
<ion-list>
108+
@for (lecture of list; track lecture.id) {
109+
<ion-item button (click)="viewVideo(lecture)"
110+
[color]="(currentVideo && currentVideo.id && currentVideo.id === lecture.id) ? 'secondary' : ((lastPlayedVideoKey === lecture.id && !currentVideo) ? 'tertiary' : '')">
111+
<ion-label class="ion-text-wrap">
112+
@if (lecture.date) {
113+
<span class="date">{{ lecture.date | date:"dd MMM y" }}</span>
114+
}
115+
@if (lecture.date) {
116+
<span class="date-divider"> | </span>
117+
}
118+
{{ lecture.title }}
119+
<small>
120+
{{ lecture.lecturer }}
121+
@if (lecture.durationInMin) {
122+
<span class="time-info">- {{ lecture.durationInMin }} min </span>
123+
}
124+
@if (lecture.history.end_time && lecture.history.end_time > 3) {
125+
<span class="time-info">
102126
@if (!lecture.duration) {
103-
<span>
104-
- {{ (lecture.history.end_time) / 60 | number:'1.0-0'}} min played
127+
<span>
128+
- {{ (lecture.history.end_time) / 60 | number:'1.0-0' }} min played
105129
</span>
106130
}
107-
@if (lecture.duration && lecture.history.end_time/lecture.duration < 0.995) {
108-
<span>
109-
- {{ (lecture.duration - lecture.history.end_time) / 60 | number:'1.0-0'}} min left
131+
@if (lecture.duration && lecture.history.end_time / lecture.duration < 0.995) {
132+
<span>
133+
- {{ (lecture.duration - lecture.history.end_time) / 60 | number:'1.0-0' }} min left
110134
</span>
111-
}
135+
}
112136
</span>
113-
}
114-
@if (lecture.attachments.length !== 0) {
115-
<ion-icon name="document-attach-outline" size="small" color="medium"></ion-icon>
116-
}
117-
</small>
118-
@if (lecture.history.end_time && lecture.duration && lecture.history.end_time > 3 && (lecture.history.end_time/lecture.duration < 0.995)) {
119-
<ion-progress-bar
120-
[value]="lecture.history.end_time/lecture.duration"></ion-progress-bar>
137+
}
138+
@if (lecture.attachments.length !== 0) {
139+
<ion-icon name="document-attach-outline" size="small" color="medium"></ion-icon>
140+
}
141+
</small>
142+
@if (lecture.history.end_time && lecture.duration && lecture.history.end_time > 3 && (lecture.history.end_time / lecture.duration < 0.995)) {
143+
<ion-progress-bar
144+
[color]="(currentVideo && currentVideo.id && currentVideo.id === lecture.id) || (lastPlayedVideoKey === lecture.id && !currentVideo) ? 'light' : 'primary'"
145+
[value]="lecture.history.end_time/lecture.duration" />
146+
}
147+
</ion-label>
148+
@if (lecture.history.end_time && lecture.duration && (lecture.history.end_time / lecture.duration >= 0.995)) {
149+
<ion-icon
150+
class="check-icon"
151+
name="checkmark-outline"
152+
slot="end"
153+
color="success"></ion-icon>
154+
}
155+
@if (lecture.sources.length === 0) {
156+
<ion-icon
157+
name="close-outline"
158+
slot="end"
159+
color="danger"></ion-icon>
160+
}
161+
@if (lastPlayedVideoKey === lecture.id) {
162+
<ion-icon
163+
name="pause-circle-outline"
164+
slot="end"
165+
color="medium"></ion-icon>
166+
}
167+
</ion-item>
168+
}
169+
</ion-list>
121170
}
122-
</ion-label>
123-
@if (lecture.history.end_time && lecture.duration && (lecture.history.end_time/lecture.duration >= 0.995)) {
124-
<ion-icon
125-
class="check-icon"
126-
name="checkmark-outline"
127-
slot="end"
128-
color="success"></ion-icon>
129-
}
130-
@if (lecture.sources.length === 0) {
131-
<ion-icon
132-
name="close-outline"
133-
slot="end"
134-
color="danger"></ion-icon>
135-
}
136-
@if (lastPlayedVideoKey === lecture.id) {
137-
<ion-icon
138-
name="pause-circle-outline"
139-
slot="end"
140-
color="medium"></ion-icon>
141-
}
142-
</ion-item>
143-
}
144-
</ion-list>
145-
}
146-
</ion-col>
147-
</ion-row>
148-
</ion-grid>
171+
</ion-col>
172+
</ion-row>
173+
</ion-grid>
149174
</ion-content>

src/app/home/course/course.page.ts

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,43 @@
1-
import {AfterViewInit, Component, ElementRef, OnInit, ViewChild, OnDestroy} from '@angular/core';
1+
import {AfterViewInit, Component, ElementRef, OnDestroy, OnInit, ViewChild} from '@angular/core';
22
import {combineLatest, EMPTY, Observable, startWith, Subject} from 'rxjs';
3-
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
3+
import {ActivatedRoute, Router, RouterLink} from '@angular/router';
44
import {CourseMembers, Lecture, ManService} from '../../man.service';
55
import {first, map, switchMap} from 'rxjs/operators';
66
import videojs from 'video.js';
77
import 'videojs-hotkeys';
88
import 'videojs-youtube';
9-
import { AlertController, IonHeader, IonToolbar, IonButtons, IonBackButton, IonTitle, IonContent, IonGrid, IonRow, IonCol, IonText, IonButton, IonCard, IonCardHeader, IonCardTitle, IonCardContent, IonList, IonListHeader, IonLabel, IonItem, IonIcon, IonProgressBar } from '@ionic/angular/standalone';
9+
import {
10+
AlertController,
11+
IonBackButton,
12+
IonButton,
13+
IonButtons,
14+
IonCard,
15+
IonCardContent,
16+
IonCardHeader,
17+
IonCardTitle,
18+
IonCol,
19+
IonContent,
20+
IonGrid,
21+
IonHeader,
22+
IonIcon,
23+
IonItem,
24+
IonLabel,
25+
IonList,
26+
IonListHeader,
27+
IonProgressBar,
28+
IonRow,
29+
IonText,
30+
IonTitle,
31+
IonToolbar,
32+
} from '@ionic/angular/standalone';
1033
import {DomSanitizer} from '@angular/platform-browser';
1134
import {PlayHistory} from '../../play-tracker.service';
1235
import {Analytics, logEvent} from '@angular/fire/analytics';
1336
import {addIcons} from "ionicons";
1437
import {checkmarkOutline, closeOutline, documentAttachOutline, download, pauseCircleOutline} from "ionicons/icons";
1538
import type Player from 'video.js/dist/types/player';
1639
import {ulid} from 'ulid';
17-
import { NgClass, AsyncPipe, DecimalPipe, DatePipe } from '@angular/common';
40+
import {AsyncPipe, DatePipe, DecimalPipe, NgClass} from '@angular/common';
1841

1942
@Component({
2043
selector: 'app-course',
@@ -64,6 +87,8 @@ export class CoursePage implements OnInit, AfterViewInit, OnDestroy {
6487
isAndroid = /Android/i.test(navigator.userAgent);
6588
isIos = /iPad/i.test(navigator.userAgent) || /iPhone/i.test(navigator.userAgent);
6689
lastPlayedVideoKey: number = null;
90+
progressTimedOut: string | null;
91+
progressNetworkError: boolean | null;
6792
sessionUid: string; // Unique ID for session x video (new id for each video)
6893
stopPolling = new Subject<boolean>();
6994

@@ -262,7 +287,29 @@ export class CoursePage implements OnInit, AfterViewInit, OnDestroy {
262287
};
263288
}
264289

290+
protected getCurrentPlayTimeString() {
291+
const time = this.videoPlayer.currentTime();
292+
const seconds = Math.floor(time % 60);
293+
const minutes = Math.floor(time % 3600 / 60);
294+
const hours = Math.floor(time / 3600);
295+
return String(hours).padStart(2, "0") + ':'
296+
+ String(minutes).padStart(2, "0") + ':'
297+
+ String(seconds).padStart(2, "0");
298+
}
299+
265300
protected updatePlayRecord() {
266-
this.manService.updatePlayRecord(this.sessionUid, this.currentVideo.id, this.videoPlayer.currentTime(), this.videoPlayer.playbackRate()).subscribe();
301+
this.manService.updatePlayRecord(this.sessionUid, this.currentVideo.id, this.videoPlayer.currentTime(), this.videoPlayer.playbackRate()).subscribe({
302+
next: () => {
303+
this.progressTimedOut = null;
304+
this.progressNetworkError = null;
305+
},
306+
error: e => {
307+
if (e.status === 0) {
308+
this.progressNetworkError = true;
309+
} else {
310+
this.progressTimedOut = this.getCurrentPlayTimeString();
311+
}
312+
},
313+
});
267314
}
268315
}

0 commit comments

Comments
 (0)