Oviya
commited on
Commit
·
00ec4c0
1
Parent(s):
39d4a08
add pronragupgrade
Browse files- src/app/app.module.ts +3 -1
- src/app/home/home.component.html +5 -0
- src/app/home/home.component.ts +10 -0
- src/app/pronunciationragupgrade/pronunciationragupgrade.component.css +920 -0
- src/app/pronunciationragupgrade/pronunciationragupgrade.component.html +165 -0
- src/app/pronunciationragupgrade/pronunciationragupgrade.component.ts +679 -0
- src/assets/pronvideo/animvideo/apple.mp4 +3 -0
- src/assets/pronvideo/animvideo/ball.mp4 +3 -0
- src/assets/pronvideo/animvideo/cat.mp4 +3 -0
- src/assets/pronvideo/animvideo/dog.mp4 +3 -0
- src/assets/pronvideo/animvideo/egg.mp4 +3 -0
- src/assets/pronvideo/animvideo/fish.mp4 +3 -0
- src/assets/pronvideo/animvideo/grapes.mp4 +3 -0
- src/assets/pronvideo/animvideo/hat.mp4 +3 -0
- src/assets/pronvideo/animvideo/icecream.mp4 +3 -0
- src/assets/pronvideo/animvideo/jar.mp4 +3 -0
- src/assets/pronvideo/animvideo/kite.mp4 +3 -0
- src/assets/pronvideo/animvideo/lion.mp4 +3 -0
- src/assets/pronvideo/animvideo/moon.mp4 +3 -0
- src/assets/pronvideo/animvideo/nest.mp4 +3 -0
- src/assets/pronvideo/animvideo/orange.mp4 +3 -0
- src/assets/pronvideo/animvideo/pig.mp4 +3 -0
- src/assets/pronvideo/animvideo/queen.mp4 +3 -0
- src/assets/pronvideo/animvideo/rabbit.mp4 +3 -0
- src/assets/pronvideo/animvideo/sun.mp4 +3 -0
- src/assets/pronvideo/animvideo/tree.mp4 +3 -0
- src/assets/pronvideo/animvideo/umbrella.mp4 +3 -0
- src/assets/pronvideo/animvideo/van.mp4 +3 -0
- src/assets/pronvideo/animvideo/watch.mp4 +3 -0
- src/assets/pronvideo/animvideo/xylophone.mp4 +3 -0
- src/assets/pronvideo/animvideo/yarn.mp4 +3 -0
- src/assets/pronvideo/animvideo/zebra.mp4 +3 -0
- src/assets/pronvideo/listening.mp4 +3 -0
- src/assets/pronvideo/slate.png +3 -0
- src/assets/pronvideo/videos/lion-old.mp4 +3 -0
- src/assets/pronvideo/videos/lion.mp4 +2 -2
- src/assets/pronvideo/videos/rabbit-old.mp4 +3 -0
- src/assets/pronvideo/videos/rabbit.mp4 +2 -2
- src/assets/pronvideo/videos/van-old.mp4 +3 -0
- src/assets/pronvideo/videos/van.mp4 +2 -2
- src/assets/pronvideo/videos/xylophone-old.mp4 +3 -0
- src/assets/pronvideo/videos/xylophone.mp4 +2 -2
- src/assets/pronvideo/videos/yarn-old.mp4 +3 -0
- src/assets/pronvideo/videos/yarn.mp4 +2 -2
- src/assets/pronvideo/videos/{zebra.mp4 → zebra-old.mp4} +0 -0
src/app/app.module.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { FooterComponent } from './footer/footer.component';
|
|
| 29 |
import { ButtonComponent } from './shared/button/button.component';
|
| 30 |
import { PronunciationVideoComponent } from './pronunciationvideo/pronunciationvideo.component';
|
| 31 |
import { PronunciationRaggComponent } from './pronunciationragg/pronunciationragg.component';
|
|
|
|
| 32 |
|
| 33 |
|
| 34 |
@NgModule({
|
|
@@ -40,7 +41,8 @@ import { PronunciationRaggComponent } from './pronunciationragg/pronunciationrag
|
|
| 40 |
RootRedirectComponent,
|
| 41 |
PronunciationComponent,
|
| 42 |
PronunciationVideoComponent,
|
| 43 |
-
PronunciationRaggComponent
|
|
|
|
| 44 |
],
|
| 45 |
imports: [
|
| 46 |
BrowserModule,
|
|
|
|
| 29 |
import { ButtonComponent } from './shared/button/button.component';
|
| 30 |
import { PronunciationVideoComponent } from './pronunciationvideo/pronunciationvideo.component';
|
| 31 |
import { PronunciationRaggComponent } from './pronunciationragg/pronunciationragg.component';
|
| 32 |
+
import { PronunciationRagUpgradeComponent } from './pronunciationragupgrade/pronunciationragupgrade.component';
|
| 33 |
|
| 34 |
|
| 35 |
@NgModule({
|
|
|
|
| 41 |
RootRedirectComponent,
|
| 42 |
PronunciationComponent,
|
| 43 |
PronunciationVideoComponent,
|
| 44 |
+
PronunciationRaggComponent,
|
| 45 |
+
PronunciationRagUpgradeComponent
|
| 46 |
],
|
| 47 |
imports: [
|
| 48 |
BrowserModule,
|
src/app/home/home.component.html
CHANGED
|
@@ -35,6 +35,11 @@
|
|
| 35 |
Pronunciation Trainer Rag
|
| 36 |
</a>
|
| 37 |
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
<li><a class="nav-link--disabled" routerLink="/personality-improvement" routerLinkActive="active-link">Personality Improvement</a></li>
|
| 39 |
<li><a class="nav-link--disabled" routerLink="/body-language-improvement" routerLinkActive="active-link">Body Language Improvement</a></li>
|
| 40 |
</ul>
|
|
|
|
| 35 |
Pronunciation Trainer Rag
|
| 36 |
</a>
|
| 37 |
</li>
|
| 38 |
+
<li>
|
| 39 |
+
<a href="#" (click)="openPronunciationRagUpgrade(); $event.preventDefault()" role="button" aria-pressed="false">
|
| 40 |
+
Pronunciation Trainer Rag Upgrade
|
| 41 |
+
</a>
|
| 42 |
+
</li>
|
| 43 |
<li><a class="nav-link--disabled" routerLink="/personality-improvement" routerLinkActive="active-link">Personality Improvement</a></li>
|
| 44 |
<li><a class="nav-link--disabled" routerLink="/body-language-improvement" routerLinkActive="active-link">Body Language Improvement</a></li>
|
| 45 |
</ul>
|
src/app/home/home.component.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { MatDialog } from '@angular/material/dialog';
|
|
| 7 |
import { PronunciationComponent } from '../pronunciation/pronunciation.component';
|
| 8 |
import { PronunciationVideoComponent } from '../pronunciationvideo/pronunciationvideo.component';
|
| 9 |
import { PronunciationRaggComponent } from '../pronunciationragg/pronunciationragg.component';
|
|
|
|
| 10 |
|
| 11 |
@Component({
|
| 12 |
selector: 'app-home',
|
|
@@ -190,4 +191,13 @@ export class HomeComponent implements AfterViewInit, OnInit, OnDestroy {
|
|
| 190 |
disableClose: true
|
| 191 |
});
|
| 192 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
}
|
|
|
|
| 7 |
import { PronunciationComponent } from '../pronunciation/pronunciation.component';
|
| 8 |
import { PronunciationVideoComponent } from '../pronunciationvideo/pronunciationvideo.component';
|
| 9 |
import { PronunciationRaggComponent } from '../pronunciationragg/pronunciationragg.component';
|
| 10 |
+
import { PronunciationRagUpgradeComponent } from '../pronunciationragupgrade/pronunciationragupgrade.component';
|
| 11 |
|
| 12 |
@Component({
|
| 13 |
selector: 'app-home',
|
|
|
|
| 191 |
disableClose: true
|
| 192 |
});
|
| 193 |
}
|
| 194 |
+
|
| 195 |
+
openPronunciationRagUpgrade(): void {
|
| 196 |
+
const dialogRef = this.dialog.open(PronunciationRagUpgradeComponent, {
|
| 197 |
+
width: '90vw',
|
| 198 |
+
maxWidth: '95vw',
|
| 199 |
+
height: '85vh',
|
| 200 |
+
disableClose: true
|
| 201 |
+
});
|
| 202 |
+
}
|
| 203 |
}
|
src/app/pronunciationragupgrade/pronunciationragupgrade.component.css
ADDED
|
@@ -0,0 +1,920 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:host {
|
| 2 |
+
display: block;
|
| 3 |
+
/*font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;*/
|
| 4 |
+
font-family: Raleway, Roboto, "Helvetica Neue", sans-serif;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
/* Page background */
|
| 8 |
+
.pp-page {
|
| 9 |
+
height: 85vh;
|
| 10 |
+
/* background: #e9f7f6;*/
|
| 11 |
+
padding: 28px 24px 18px;
|
| 12 |
+
box-sizing: border-box;
|
| 13 |
+
border: 7px solid #3aaea8;
|
| 14 |
+
border-radius: 1vw;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
/* Header */
|
| 18 |
+
.pp-header {
|
| 19 |
+
text-align: center;
|
| 20 |
+
margin-bottom: 18px;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.pp-header h1 {
|
| 24 |
+
margin: 0;
|
| 25 |
+
font-size: 42px;
|
| 26 |
+
font-weight: 800;
|
| 27 |
+
color: #3aaea8;
|
| 28 |
+
letter-spacing: 0.3px;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
.pp-sub {
|
| 32 |
+
margin-top: 6px;
|
| 33 |
+
color: #6b7f7e;
|
| 34 |
+
font-size: 15px;
|
| 35 |
+
position: relative;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.pp-tooltip {
|
| 39 |
+
margin-left: 8px;
|
| 40 |
+
display: inline-block;
|
| 41 |
+
background: #ffffff;
|
| 42 |
+
border: 1px solid #d8eeee;
|
| 43 |
+
color: #2f6f6b;
|
| 44 |
+
padding: 6px 10px;
|
| 45 |
+
border-radius: 14px;
|
| 46 |
+
font-size: 12px;
|
| 47 |
+
box-shadow: 0 6px 14px rgba(0,0,0,0.06);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
/* Main 3 columns */
|
| 51 |
+
.pp-main {
|
| 52 |
+
display: flex;
|
| 53 |
+
gap: 1vw;
|
| 54 |
+
align-items: start;
|
| 55 |
+
justify-content: space-around;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
/* LEFT */
|
| 59 |
+
.pp-left {
|
| 60 |
+
display: flex;
|
| 61 |
+
justify-content: center;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.word-card {
|
| 65 |
+
width: 22vw;
|
| 66 |
+
height: 34vw;
|
| 67 |
+
background: #e9f7f6;
|
| 68 |
+
border-radius: 18px;
|
| 69 |
+
padding: 22px 18px 26px;
|
| 70 |
+
text-align: center;
|
| 71 |
+
box-shadow: 0 12px 26px rgba(0, 0, 0, 0.08);
|
| 72 |
+
border: 3px dashed #3aaea8;
|
| 73 |
+
gap: 0.5vw;
|
| 74 |
+
display: flex;
|
| 75 |
+
flex-direction: column;
|
| 76 |
+
align-items: center;
|
| 77 |
+
justify-content: space-between;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.word-img-wrap {
|
| 81 |
+
width: 20vw;
|
| 82 |
+
height: 20vw;
|
| 83 |
+
display: flex;
|
| 84 |
+
align-items: center;
|
| 85 |
+
justify-content: center;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.word-img-wrap img {
|
| 89 |
+
max-width: 100%;
|
| 90 |
+
max-height: 100%;
|
| 91 |
+
object-fit: contain;
|
| 92 |
+
border-radius: 1vw;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.word-text {
|
| 96 |
+
font-size: 3vw;
|
| 97 |
+
font-weight: 800;
|
| 98 |
+
color: #1f2b2a;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.phonetic-pill {
|
| 102 |
+
color: #3aaea8;
|
| 103 |
+
font-size: 1.5vw;
|
| 104 |
+
font-weight: 600;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.audio-img {
|
| 108 |
+
width: 4.8vw;
|
| 109 |
+
cursor: pointer;
|
| 110 |
+
}
|
| 111 |
+
/* CENTER */
|
| 112 |
+
.pp-center {
|
| 113 |
+
display: flex;
|
| 114 |
+
flex-direction: column;
|
| 115 |
+
align-items: center;
|
| 116 |
+
gap: 14px;
|
| 117 |
+
position: relative;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.teacher-frame {
|
| 121 |
+
/* width: 240px;
|
| 122 |
+
height: 210px;
|
| 123 |
+
border-radius: 18px;
|
| 124 |
+
padding: 12px;
|
| 125 |
+
background: #e7f4f3;
|
| 126 |
+
border: 2px dashed #b9dedb;
|
| 127 |
+
display: flex;
|
| 128 |
+
align-items: center;
|
| 129 |
+
justify-content: center;*/
|
| 130 |
+
width: 22vw;
|
| 131 |
+
height: 33vw;
|
| 132 |
+
background: #e9f7f6;
|
| 133 |
+
border-radius: 18px;
|
| 134 |
+
padding: 22px 18px 26px;
|
| 135 |
+
text-align: center;
|
| 136 |
+
box-shadow: 0 12px 26px rgba(0, 0, 0, 0.08);
|
| 137 |
+
border: 3px dashed #3aaea8;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.teacher-frame img {
|
| 141 |
+
width: 100%;
|
| 142 |
+
height: 100%;
|
| 143 |
+
object-fit: cover;
|
| 144 |
+
border-radius: 12px;
|
| 145 |
+
background: #ffffff;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
/* Listen button */
|
| 149 |
+
.listen-btn {
|
| 150 |
+
border: none;
|
| 151 |
+
background: #49b6ae;
|
| 152 |
+
color: #ffffff;
|
| 153 |
+
padding: 12px 18px;
|
| 154 |
+
border-radius: 12px;
|
| 155 |
+
font-weight: 700;
|
| 156 |
+
font-size: 16px;
|
| 157 |
+
display: inline-flex;
|
| 158 |
+
align-items: center;
|
| 159 |
+
gap: 10px;
|
| 160 |
+
cursor: pointer;
|
| 161 |
+
box-shadow: 0 8px 18px rgba(0,0,0,0.08);
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.listen-btn:active {
|
| 165 |
+
transform: scale(0.99);
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.listen-ico {
|
| 169 |
+
font-size: 18px;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
/* Record circle */
|
| 173 |
+
.rec-circle {
|
| 174 |
+
width: 92px;
|
| 175 |
+
height: 92px;
|
| 176 |
+
border-radius: 50%;
|
| 177 |
+
border: none;
|
| 178 |
+
background: #f07b48;
|
| 179 |
+
color: #ffffff;
|
| 180 |
+
cursor: pointer;
|
| 181 |
+
/* box-shadow: 0 12px 22px rgba(0,0,0,0.12);*/
|
| 182 |
+
display: flex;
|
| 183 |
+
align-items: center;
|
| 184 |
+
justify-content: center;
|
| 185 |
+
/* transition: transform 0.08s ease, filter 0.2s ease;*/
|
| 186 |
+
box-shadow: 0px 10px 30px rgba(0, 0, 0, 0.4);
|
| 187 |
+
transition: all 0.3s ease;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
.rec-circle:active {
|
| 191 |
+
transform: scale(0.98);
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.rec-circle.recording {
|
| 195 |
+
filter: brightness(0.95);
|
| 196 |
+
animation: recPulse 1s infinite;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
@keyframes recPulse {
|
| 200 |
+
0% {
|
| 201 |
+
box-shadow: 0 0 0 0 rgba(240,123,72,1);
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
70% {
|
| 205 |
+
box-shadow: 0 0 0 18px rgba(240,123,72,0);
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
100% {
|
| 209 |
+
box-shadow: 0 0 0 0 rgba(240,123,72,0);
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.rec-inner {
|
| 214 |
+
text-align: center;
|
| 215 |
+
line-height: 1.1;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
.mic {
|
| 219 |
+
font-size: 22px;
|
| 220 |
+
margin-bottom: 4px;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.rec-text {
|
| 224 |
+
font-size: 12px;
|
| 225 |
+
font-weight: 800;
|
| 226 |
+
letter-spacing: 0.6px;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
/* RIGHT */
|
| 230 |
+
.pp-right {
|
| 231 |
+
display: flex;
|
| 232 |
+
justify-content: flex-start;
|
| 233 |
+
align-items: center;
|
| 234 |
+
flex-direction: column;
|
| 235 |
+
gap: 18vw;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
/* little connector dots */
|
| 239 |
+
.connector {
|
| 240 |
+
display: flex;
|
| 241 |
+
flex-direction: column;
|
| 242 |
+
gap: 2vw;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
.connector span {
|
| 246 |
+
width: 34px;
|
| 247 |
+
height: 10px;
|
| 248 |
+
border-radius: 999px;
|
| 249 |
+
background: #98d8d4;
|
| 250 |
+
opacity: 0.6;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
/* feedback card */
|
| 254 |
+
.feedback-card {
|
| 255 |
+
width: 15vw;
|
| 256 |
+
background: #9edfd9;
|
| 257 |
+
border-radius: 16px;
|
| 258 |
+
padding: 22px 22px 24px;
|
| 259 |
+
box-shadow: 0 12px 22px rgba(0,0,0,0.08);
|
| 260 |
+
height: 15vw;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
.feedback-title {
|
| 264 |
+
font-size: 18px;
|
| 265 |
+
font-weight: 800;
|
| 266 |
+
color: #205f5a;
|
| 267 |
+
letter-spacing: 0.6px;
|
| 268 |
+
margin-bottom: 8px;
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
.feedback-body {
|
| 272 |
+
background: transparent;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.feedback-muted {
|
| 276 |
+
color: #2c6d68;
|
| 277 |
+
opacity: 0.8;
|
| 278 |
+
font-style: italic;
|
| 279 |
+
font-size: 14px;
|
| 280 |
+
margin-top: 8px;
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
/* Result UI inside feedback */
|
| 284 |
+
.feedback-result {
|
| 285 |
+
margin-top: 8px;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.score-row {
|
| 289 |
+
display: flex;
|
| 290 |
+
align-items: baseline;
|
| 291 |
+
gap: 10px;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
.score-label {
|
| 295 |
+
font-size: 12px;
|
| 296 |
+
color: #1d514d;
|
| 297 |
+
font-weight: 700;
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
.score-value {
|
| 301 |
+
font-size: 28px;
|
| 302 |
+
font-weight: 900;
|
| 303 |
+
color: #103c39;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
/* Speakometer */
|
| 307 |
+
.meter {
|
| 308 |
+
margin-top: 10px;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.meter-track {
|
| 312 |
+
height: 10px;
|
| 313 |
+
background: rgba(255,255,255,0.45);
|
| 314 |
+
border-radius: 999px;
|
| 315 |
+
overflow: hidden;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
.meter-fill {
|
| 319 |
+
height: 100%;
|
| 320 |
+
width: 0%;
|
| 321 |
+
background: #2b8f88;
|
| 322 |
+
transition: width 0.5s ease;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
/* Stars */
|
| 326 |
+
.stars {
|
| 327 |
+
margin-top: 10px;
|
| 328 |
+
font-size: 22px;
|
| 329 |
+
display: flex;
|
| 330 |
+
gap: 4px;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.stars span {
|
| 334 |
+
color: rgba(255,255,255,0.55);
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
.stars span.active {
|
| 338 |
+
color: #ffcc4d;
|
| 339 |
+
animation: starPop 0.3s ease;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
@keyframes starPop {
|
| 343 |
+
0% {
|
| 344 |
+
transform: scale(0.85);
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
60% {
|
| 348 |
+
transform: scale(1.12);
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
100% {
|
| 352 |
+
transform: scale(1);
|
| 353 |
+
}
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
/* 3 lines feedback */
|
| 357 |
+
.feedback-lines {
|
| 358 |
+
margin: 10px 0 0 18px;
|
| 359 |
+
color: #114744;
|
| 360 |
+
font-size: 13.5px;
|
| 361 |
+
font-weight: 600;
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
/* Bottom area */
|
| 365 |
+
.pp-bottom {
|
| 366 |
+
max-width: 1200px;
|
| 367 |
+
margin: 22px auto 0;
|
| 368 |
+
display: flex;
|
| 369 |
+
flex-direction: column;
|
| 370 |
+
align-items: center;
|
| 371 |
+
gap: 14px;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
/* Prev/Next line */
|
| 375 |
+
/* Prev/Next line */
|
| 376 |
+
.nav-row {
|
| 377 |
+
display: flex;
|
| 378 |
+
align-items: center;
|
| 379 |
+
gap: 3vw;
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
/* Increased arrow size and perfectly center it inside the circular button */
|
| 383 |
+
.nav-btn {
|
| 384 |
+
width: 7vw;
|
| 385 |
+
height: 7vw;
|
| 386 |
+
border-radius: 50%;
|
| 387 |
+
border: none;
|
| 388 |
+
background: #dcefee;
|
| 389 |
+
color: #1b5551;
|
| 390 |
+
font-size: 7vw; /* larger arrow */
|
| 391 |
+
display: flex; /* center text horizontally & vertically */
|
| 392 |
+
align-items: center;
|
| 393 |
+
justify-content: center;
|
| 394 |
+
text-align: center;
|
| 395 |
+
line-height: 1; /* avoid font baseline shifts */
|
| 396 |
+
padding: 0; /* ensure perfect centering */
|
| 397 |
+
cursor: pointer;
|
| 398 |
+
font-weight: 700;
|
| 399 |
+
box-shadow: 0 8px 18px rgba(0,0,0,0.06);
|
| 400 |
+
transition: transform 0.08s ease;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
.nav-btn:active {
|
| 404 |
+
transform: scale(0.98);
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
.nav-btn:disabled {
|
| 408 |
+
opacity: 0.5;
|
| 409 |
+
cursor: not-allowed;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.nav-center {
|
| 413 |
+
display: inline-flex;
|
| 414 |
+
align-items: baseline;
|
| 415 |
+
gap: 8px;
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
.nav-letter {
|
| 419 |
+
font-size: 5vw;
|
| 420 |
+
font-weight: 900;
|
| 421 |
+
color: #3aaea8;
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
.nav-count {
|
| 425 |
+
font-size: 14px;
|
| 426 |
+
color: #667b79;
|
| 427 |
+
font-weight: 600;
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
/* Responsive override so buttons don't overflow on smaller screens */
|
| 431 |
+
@media (max-width: 980px) {
|
| 432 |
+
.nav-btn {
|
| 433 |
+
width: 56px;
|
| 434 |
+
height: 56px;
|
| 435 |
+
font-size: 28px;
|
| 436 |
+
}
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
/* Alphabet pills */
|
| 440 |
+
.alpha-row {
|
| 441 |
+
display: flex;
|
| 442 |
+
flex-wrap: wrap;
|
| 443 |
+
justify-content: center;
|
| 444 |
+
gap: 8px;
|
| 445 |
+
max-width: 900px;
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
.alpha-pill {
|
| 449 |
+
width: 34px;
|
| 450 |
+
height: 34px;
|
| 451 |
+
border-radius: 50%;
|
| 452 |
+
border: none;
|
| 453 |
+
background: #dfeeee;
|
| 454 |
+
color: #3a5a58;
|
| 455 |
+
font-weight: 700;
|
| 456 |
+
cursor: pointer;
|
| 457 |
+
font-size: 13px;
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
.alpha-pill.active {
|
| 461 |
+
background: #49b6ae;
|
| 462 |
+
color: #ffffff;
|
| 463 |
+
box-shadow: 0 6px 14px rgba(0,0,0,0.08);
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
/* Responsive */
|
| 467 |
+
@media (max-width: 1100px) {
|
| 468 |
+
.pp-main {
|
| 469 |
+
grid-template-columns: 260px 320px 1fr;
|
| 470 |
+
}
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
@media (max-width: 980px) {
|
| 474 |
+
.pp-main {
|
| 475 |
+
grid-template-columns: 1fr;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
.pp-left, .pp-center, .pp-right {
|
| 479 |
+
justify-content: center;
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
.connector {
|
| 483 |
+
display: none;
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
.feedback-card {
|
| 487 |
+
width: 100%;
|
| 488 |
+
max-width: 520px;
|
| 489 |
+
}
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
.gauge-wrapper {
|
| 493 |
+
position: relative;
|
| 494 |
+
width: 20vw;
|
| 495 |
+
height: 10vw;
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
.gauge {
|
| 499 |
+
position: absolute;
|
| 500 |
+
left: 50%;
|
| 501 |
+
top: 0;
|
| 502 |
+
transform: translateX(-50%);
|
| 503 |
+
width: 100%;
|
| 504 |
+
height: 100%;
|
| 505 |
+
border-radius: 260px 260px 0 0;
|
| 506 |
+
overflow: hidden;
|
| 507 |
+
background: #f3f3f3;
|
| 508 |
+
box-shadow: 0 4px 10px rgba(0,0,0,0.25) inset;
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
.gauge-arc {
|
| 512 |
+
position: absolute;
|
| 513 |
+
inset: 0;
|
| 514 |
+
border-radius: 50%;
|
| 515 |
+
background: conic-gradient( from 270deg, #e53935 0deg 45deg, #fb8c00 45deg 90deg, #fbc02d 90deg 135deg, #43a047 135deg 180deg, transparent 180deg 360deg );
|
| 516 |
+
height: 20vw;
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
.needle {
|
| 520 |
+
position: absolute;
|
| 521 |
+
bottom: 0vw;
|
| 522 |
+
left: 50%;
|
| 523 |
+
width: 0.7vw;
|
| 524 |
+
height: 8vw;
|
| 525 |
+
background: #333;
|
| 526 |
+
transform: translateX(-50%) rotate(var(--angle, -90deg));
|
| 527 |
+
transform-origin: 50% 100%;
|
| 528 |
+
transition: transform 700ms cubic-bezier(.2,.9,.2,1);
|
| 529 |
+
border-radius: 10px;
|
| 530 |
+
box-shadow: 0 2px 6px rgba(0,0,0,0.5);
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
.mic-badge {
|
| 534 |
+
position: absolute;
|
| 535 |
+
bottom: -0.3vw;
|
| 536 |
+
left: 50%;
|
| 537 |
+
transform: translate(-50%, 35%);
|
| 538 |
+
width: 3vw;
|
| 539 |
+
height: 3vw;
|
| 540 |
+
border-radius: 50%;
|
| 541 |
+
background: #000;
|
| 542 |
+
box-shadow: 0 8px 18px rgba(0,0,0,0.4);
|
| 543 |
+
display: flex;
|
| 544 |
+
align-items: center;
|
| 545 |
+
justify-content: center;
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
.score-span {
|
| 549 |
+
color: white;
|
| 550 |
+
font-size: 1vw;
|
| 551 |
+
font-weight: bold;
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
.notepad {
|
| 555 |
+
display: flex;
|
| 556 |
+
align-items: center;
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
.user-guide-close-icon {
|
| 560 |
+
position: fixed;
|
| 561 |
+
top: 3vw;
|
| 562 |
+
right: 4vw;
|
| 563 |
+
background: #009688;
|
| 564 |
+
border: none;
|
| 565 |
+
width: 44px;
|
| 566 |
+
height: 44px;
|
| 567 |
+
border-radius: 50%;
|
| 568 |
+
display: flex;
|
| 569 |
+
align-items: center;
|
| 570 |
+
justify-content: center;
|
| 571 |
+
font-size: 2vw;
|
| 572 |
+
color: black;
|
| 573 |
+
cursor: pointer;
|
| 574 |
+
z-index: 2010;
|
| 575 |
+
box-shadow: 0 2px 8px rgba(93, 145, 195, 0.18);
|
| 576 |
+
transition: background 0.2s, color 0.2s;
|
| 577 |
+
}
|
| 578 |
+
/* Teacher media container — ensures image and video occupy exactly the same box */
|
| 579 |
+
.teacher-media {
|
| 580 |
+
width: 20vw; /* same as you used inline before */
|
| 581 |
+
max-width: 260px; /* optional limit for very wide screens */
|
| 582 |
+
min-width: 140px; /* optional floor */
|
| 583 |
+
aspect-ratio: 3 / 4; /* change to 16 / 9 if your videos are widescreen */
|
| 584 |
+
position: relative;
|
| 585 |
+
overflow: hidden;
|
| 586 |
+
display: block;
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
/* Shared styles for image and video so sizes match exactly */
|
| 590 |
+
.teacher-media__img,
|
| 591 |
+
.teacher-media__video {
|
| 592 |
+
width: 100%;
|
| 593 |
+
height: 100%;
|
| 594 |
+
display: block;
|
| 595 |
+
border-radius: 1vw;
|
| 596 |
+
border: 2px solid #ccc; /* keep previously visible border */
|
| 597 |
+
object-fit: cover; /* makes video/image fill box similarly */
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
/* A neutral background for video while it loads */
|
| 601 |
+
.teacher-media__video {
|
| 602 |
+
background-color: #000;
|
| 603 |
+
}
|
| 604 |
+
/* Make play/pause image match record button size and add shadow */
|
| 605 |
+
.listen-img {
|
| 606 |
+
width: 4.8vw; /* match .rec-circle size */
|
| 607 |
+
height: 4.8vw;
|
| 608 |
+
border-radius: 50%;
|
| 609 |
+
display: inline-block;
|
| 610 |
+
object-fit: contain; /* keep icon aspect */
|
| 611 |
+
cursor: pointer;
|
| 612 |
+
user-select: none;
|
| 613 |
+
margin-right: 1vw;
|
| 614 |
+
box-shadow: 0px 10px 30px rgba(0, 0, 0, 0.4);
|
| 615 |
+
transition: all 0.3s ease;
|
| 616 |
+
/*box-shadow: 0 12px 22px rgba(0,0,0,0.12);
|
| 617 |
+
transition: transform 0.08s ease, filter 0.15s ease, box-shadow 0.15s ease;
|
| 618 |
+
|
| 619 |
+
box-sizing: border-box;*/
|
| 620 |
+
border: none;
|
| 621 |
+
}
|
| 622 |
+
|
| 623 |
+
/* pressed / active */
|
| 624 |
+
.listen-img:active {
|
| 625 |
+
transform: scale(0.98);
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
/* playing state — subtle visual change */
|
| 629 |
+
.listen-img.playing,
|
| 630 |
+
.listen-img[aria-pressed="true"] {
|
| 631 |
+
filter: brightness(0.95);
|
| 632 |
+
box-shadow: 0 18px 30px rgba(0,0,0,0.16);
|
| 633 |
+
}
|
| 634 |
+
|
| 635 |
+
/* keyboard focus for accessibility */
|
| 636 |
+
.listen-img:focus {
|
| 637 |
+
outline: 3px solid rgba(58,174,168,0.18);
|
| 638 |
+
outline-offset: 3px;
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
/* Small-screen fallback (keep sizes proportional) */
|
| 642 |
+
@media (max-width: 980px) {
|
| 643 |
+
.listen-img {
|
| 644 |
+
width: 72px;
|
| 645 |
+
height: 72px;
|
| 646 |
+
padding: 14px;
|
| 647 |
+
}
|
| 648 |
+
}
|
| 649 |
+
/* add oscillation keyframes and state */
|
| 650 |
+
@keyframes needleOscillate {
|
| 651 |
+
0% {
|
| 652 |
+
transform: translateX(-50%) rotate(-70deg);
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
25% {
|
| 656 |
+
transform: translateX(-50%) rotate(-20deg);
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
50% {
|
| 660 |
+
transform: translateX(-50%) rotate(60deg);
|
| 661 |
+
}
|
| 662 |
+
|
| 663 |
+
75% {
|
| 664 |
+
transform: translateX(-50%) rotate(-10deg);
|
| 665 |
+
}
|
| 666 |
+
|
| 667 |
+
100% {
|
| 668 |
+
transform: translateX(-50%) rotate(-70deg);
|
| 669 |
+
}
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
/* existing needle default uses CSS variable for final angle */
|
| 673 |
+
.needle {
|
| 674 |
+
position: absolute;
|
| 675 |
+
bottom: 0vw;
|
| 676 |
+
left: 50%;
|
| 677 |
+
width: 0.7vw;
|
| 678 |
+
height: 8vw;
|
| 679 |
+
background: #333;
|
| 680 |
+
transform: translateX(-50%) rotate(var(--angle, -90deg));
|
| 681 |
+
transform-origin: 50% 100%;
|
| 682 |
+
transition: transform 700ms cubic-bezier(.2,.9,.2,1);
|
| 683 |
+
border-radius: 10px;
|
| 684 |
+
box-shadow: 0 2px 6px rgba(0,0,0,0.5);
|
| 685 |
+
}
|
| 686 |
+
|
| 687 |
+
/* while recording / waiting for score, run oscillation animation */
|
| 688 |
+
.needle.oscillate {
|
| 689 |
+
/* override transform with animation while oscillating */
|
| 690 |
+
animation: needleOscillate 1.2s ease-in-out infinite;
|
| 691 |
+
/* disable the smooth transition while animation runs to prevent conflicts */
|
| 692 |
+
transition: none;
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
/* when oscillation ends (isRecording/isScoring false), the animation class is removed
|
| 696 |
+
and the element will smoothly transition to the value provided by --angle */
|
| 697 |
+
.container {
|
| 698 |
+
display: flex;
|
| 699 |
+
justify-content: center;
|
| 700 |
+
align-items: center;
|
| 701 |
+
gap: 40px; /* Increase the space between elements */
|
| 702 |
+
}
|
| 703 |
+
|
| 704 |
+
.arrow {
|
| 705 |
+
font-size: 4rem; /* Increased font size for bigger buttons */
|
| 706 |
+
background-color: #e0f7fa;
|
| 707 |
+
border: none;
|
| 708 |
+
width: 92px; /* Set width */
|
| 709 |
+
height: 92px; /* Set height */
|
| 710 |
+
border-radius: 50%; /* Make the button circular */
|
| 711 |
+
display: flex;
|
| 712 |
+
justify-content: center;
|
| 713 |
+
align-items: center;
|
| 714 |
+
cursor: pointer;
|
| 715 |
+
box-shadow: 4px 4px 15px rgba(0, 0, 0, 0.2); /* Box shadow for the button */
|
| 716 |
+
color: #00796b; /* Set color of the arrows */
|
| 717 |
+
transition: background-color 0.3s, color 0.3s; /* Smooth transition for background and text color */
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
/* Disabled state styles */
|
| 721 |
+
.arrow:disabled {
|
| 722 |
+
background-color: #cfd8dc; /* Light gray background when disabled */
|
| 723 |
+
color: #90a4ae; /* Gray color for the arrow */
|
| 724 |
+
cursor: not-allowed; /* Change cursor to indicate the button is disabled */
|
| 725 |
+
box-shadow: none; /* Remove box shadow for disabled button */
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
.center-text {
|
| 729 |
+
font-size: 5rem; /* Increased font size for the 'Y' text */
|
| 730 |
+
font-weight: bold;
|
| 731 |
+
color: #00796b;
|
| 732 |
+
width: 4vw;
|
| 733 |
+
text-align: center;
|
| 734 |
+
}
|
| 735 |
+
|
| 736 |
+
|
| 737 |
+
/* Styling the container */
|
| 738 |
+
.image-container {
|
| 739 |
+
display: flex;
|
| 740 |
+
justify-content: center;
|
| 741 |
+
align-items: center;
|
| 742 |
+
height: 100vh;
|
| 743 |
+
}
|
| 744 |
+
|
| 745 |
+
/* Styling the round image with shadow */
|
| 746 |
+
.round-image {
|
| 747 |
+
width: 4.8vw; /* Adjust the size as needed */
|
| 748 |
+
height: 4.8vw;
|
| 749 |
+
border-radius: 50%; /* Makes the image round */
|
| 750 |
+
box-shadow: 0px 10px 30px rgba(0, 0, 0, 0.4);
|
| 751 |
+
transition: all 0.3s ease; /* Smooth transition for animation */
|
| 752 |
+
cursor: pointer;
|
| 753 |
+
}
|
| 754 |
+
|
| 755 |
+
/* Scaling effect on click */
|
| 756 |
+
.round-image:active {
|
| 757 |
+
transform: scale(1.1); /* Scale up by 10% when clicked */
|
| 758 |
+
}
|
| 759 |
+
|
| 760 |
+
/* Subtle zoom in/out */
|
| 761 |
+
.apple-anim {
|
| 762 |
+
transform-origin: 50% 50%;
|
| 763 |
+
animation: appleZoom 2.8s ease-in-out infinite alternate;
|
| 764 |
+
will-change: transform;
|
| 765 |
+
}
|
| 766 |
+
|
| 767 |
+
@keyframes appleZoom {
|
| 768 |
+
from {
|
| 769 |
+
transform: scale(0.8);
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
to {
|
| 773 |
+
transform: scale(1.06);
|
| 774 |
+
}
|
| 775 |
+
/* slight zoom */
|
| 776 |
+
}
|
| 777 |
+
|
| 778 |
+
/* Respect reduced motion preference */
|
| 779 |
+
@media (prefers-reduced-motion: reduce) {
|
| 780 |
+
.apple-anim {
|
| 781 |
+
animation: none;
|
| 782 |
+
transform: none;
|
| 783 |
+
}
|
| 784 |
+
}
|
| 785 |
+
|
| 786 |
+
|
| 787 |
+
|
| 788 |
+
|
| 789 |
+
|
| 790 |
+
/* Progress / record button (circular) */
|
| 791 |
+
.progress-btn {
|
| 792 |
+
width: 4.8vw;
|
| 793 |
+
height: 4.8vw;
|
| 794 |
+
border-radius: 50%;
|
| 795 |
+
display: inline-flex;
|
| 796 |
+
align-items: center;
|
| 797 |
+
justify-content: center;
|
| 798 |
+
padding: 0;
|
| 799 |
+
border: none;
|
| 800 |
+
background: #f07b48;
|
| 801 |
+
position: relative;
|
| 802 |
+
cursor: pointer;
|
| 803 |
+
box-shadow: 0px 10px 30px rgba(0, 0, 0, 0.4);
|
| 804 |
+
transition: transform .12s ease, box-shadow .12s ease;
|
| 805 |
+
}
|
| 806 |
+
|
| 807 |
+
.progress-btn:active {
|
| 808 |
+
transform: scale(.98);
|
| 809 |
+
}
|
| 810 |
+
|
| 811 |
+
.progress-btn.recording {
|
| 812 |
+
/* box-shadow: 0 10px 26px rgba(63,81,181,0.18);*/
|
| 813 |
+
filter: brightness(0.95);
|
| 814 |
+
animation: recPulse 1s infinite;
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
/* SVG ring occupies full button */
|
| 818 |
+
.progress-ring {
|
| 819 |
+
width: 100%;
|
| 820 |
+
height: 100%;
|
| 821 |
+
transform: rotate(-90deg); /* rotate so progress starts at top */
|
| 822 |
+
display: block;
|
| 823 |
+
}
|
| 824 |
+
|
| 825 |
+
/* style the static background ring and active bar */
|
| 826 |
+
.progress-ring__background {
|
| 827 |
+
stroke: #eee;
|
| 828 |
+
opacity: 1;
|
| 829 |
+
}
|
| 830 |
+
|
| 831 |
+
.progress-ring__bar {
|
| 832 |
+
stroke: #3aaea8;
|
| 833 |
+
transition: stroke-dashoffset 0.12s linear;
|
| 834 |
+
}
|
| 835 |
+
|
| 836 |
+
/* central label stacked over the SVG */
|
| 837 |
+
.label {
|
| 838 |
+
position: absolute;
|
| 839 |
+
left: 0;
|
| 840 |
+
right: 0;
|
| 841 |
+
top: 0;
|
| 842 |
+
bottom: 0;
|
| 843 |
+
display: flex;
|
| 844 |
+
align-items: center;
|
| 845 |
+
justify-content: center;
|
| 846 |
+
pointer-events: none;
|
| 847 |
+
font-weight: 700;
|
| 848 |
+
color: #222;
|
| 849 |
+
font-size: 2.5rem;
|
| 850 |
+
}
|
| 851 |
+
|
| 852 |
+
/* seconds shown while counting down */
|
| 853 |
+
.seconds {
|
| 854 |
+
color: #fff;
|
| 855 |
+
/* background: rgba(0,0,0,0.5);*/
|
| 856 |
+
padding: 4px 8px;
|
| 857 |
+
border-radius: 6px;
|
| 858 |
+
}
|
| 859 |
+
|
| 860 |
+
/* dot shown when recording/waiting for result */
|
| 861 |
+
.finished-dot {
|
| 862 |
+
color: #d32f2f;
|
| 863 |
+
font-size: 1.4rem;
|
| 864 |
+
}
|
| 865 |
+
|
| 866 |
+
|
| 867 |
+
/*.fb-board {
|
| 868 |
+
position: absolute;
|
| 869 |
+
top: 15.5vw;
|
| 870 |
+
right: 3.8vw;
|
| 871 |
+
}*/
|
| 872 |
+
|
| 873 |
+
|
| 874 |
+
.fb-board {
|
| 875 |
+
position: absolute;
|
| 876 |
+
top: 15.3vw;
|
| 877 |
+
right: 3.6vw;
|
| 878 |
+
}
|
| 879 |
+
|
| 880 |
+
.fb-board img {
|
| 881 |
+
width: 100%;
|
| 882 |
+
display: block;
|
| 883 |
+
}
|
| 884 |
+
|
| 885 |
+
.center-text1 {
|
| 886 |
+
position: absolute;
|
| 887 |
+
top: 50%;
|
| 888 |
+
left: 50%;
|
| 889 |
+
transform: translate(-50%, -50%);
|
| 890 |
+
color: white;
|
| 891 |
+
font-size: 20px;
|
| 892 |
+
font-weight: bold;
|
| 893 |
+
text-align: center;
|
| 894 |
+
width: 14vw;
|
| 895 |
+
}
|
| 896 |
+
|
| 897 |
+
|
| 898 |
+
|
| 899 |
+
/* Responsive video container */
|
| 900 |
+
.video-frame {
|
| 901 |
+
width: 100%;
|
| 902 |
+
aspect-ratio: 16 / 9; /* default; adjust per need */
|
| 903 |
+
border-radius: 1vw;
|
| 904 |
+
overflow: hidden;
|
| 905 |
+
background: #000; /* avoids blank edges before first frame */
|
| 906 |
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18), 0 12px 24px rgba(0, 0, 0, 0.16), 0 24px 48px rgba(0, 0, 0, 0.12);
|
| 907 |
+
}
|
| 908 |
+
|
| 909 |
+
/* For square videos (like the apple video) */
|
| 910 |
+
.video-frame.square {
|
| 911 |
+
aspect-ratio: 1 / 1;
|
| 912 |
+
}
|
| 913 |
+
|
| 914 |
+
/* Make the video fill the frame */
|
| 915 |
+
.video-frame > video {
|
| 916 |
+
width: 100%;
|
| 917 |
+
height: 100%;
|
| 918 |
+
display: block;
|
| 919 |
+
object-fit: cover; /* use 'contain' if you do not want cropping */
|
| 920 |
+
}
|
src/app/pronunciationragupgrade/pronunciationragupgrade.component.html
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div class="pp-page">
|
| 2 |
+
|
| 3 |
+
<!-- Header -->
|
| 4 |
+
<div class="pp-header">
|
| 5 |
+
<h1>Pronunciation Practice</h1>
|
| 6 |
+
</div>
|
| 7 |
+
|
| 8 |
+
<!-- Main area -->
|
| 9 |
+
<div class="pp-main">
|
| 10 |
+
|
| 11 |
+
<!-- LEFT: Word card -->
|
| 12 |
+
<div class="pp-left">
|
| 13 |
+
<div class="word-card">
|
| 14 |
+
<div class="word-img-wrap">
|
| 15 |
+
<div class="video-frame square">
|
| 16 |
+
<video [src]="current.imgSrc"
|
| 17 |
+
autoplay
|
| 18 |
+
loop
|
| 19 |
+
muted
|
| 20 |
+
playsinline>
|
| 21 |
+
Your browser does not support the video tag.
|
| 22 |
+
</video>
|
| 23 |
+
</div>
|
| 24 |
+
</div>
|
| 25 |
+
|
| 26 |
+
<div class="word-text">{{ current.word }}</div>
|
| 27 |
+
|
| 28 |
+
<div class="phonetic-pill">
|
| 29 |
+
{{ current.phonetics }}
|
| 30 |
+
</div>
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
<div class="image-container">
|
| 34 |
+
<img src="assets/pronvideo/audio.png" alt="Round Image" class="round-image" (click)="playWordAudio()">
|
| 35 |
+
</div>
|
| 36 |
+
|
| 37 |
+
</div>
|
| 38 |
+
</div>
|
| 39 |
+
|
| 40 |
+
<!-- CENTER: Teacher area -->
|
| 41 |
+
<div class="pp-center">
|
| 42 |
+
<div class="word-card" style="width:30vw!important">
|
| 43 |
+
<div style="width:28vw;height:25vw">
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
<video *ngIf="!showVideo"
|
| 47 |
+
src="assets/pronvideo/listening.mp4"
|
| 48 |
+
style="border-radius:1vw; object-fit:cover;"
|
| 49 |
+
autoplay
|
| 50 |
+
loop
|
| 51 |
+
muted
|
| 52 |
+
(ended)="onVideoEnded()"
|
| 53 |
+
height="469"
|
| 54 |
+
width="521">
|
| 55 |
+
Your browser does not support the video tag.
|
| 56 |
+
</video>
|
| 57 |
+
|
| 58 |
+
<!-- single video element used for both teacher and feedback videos -->
|
| 59 |
+
<video *ngIf="showVideo"
|
| 60 |
+
#videoEl
|
| 61 |
+
[src]="videoSrc"
|
| 62 |
+
style="border-radius:1vw; object-fit:cover;"
|
| 63 |
+
(play)="onVideoPlay()"
|
| 64 |
+
(pause)="onVideoPause()"
|
| 65 |
+
autoplay
|
| 66 |
+
(ended)="onVideoEnded()"
|
| 67 |
+
height="469"
|
| 68 |
+
width="521">
|
| 69 |
+
Your browser does not support the video tag.
|
| 70 |
+
</video>
|
| 71 |
+
</div>
|
| 72 |
+
|
| 73 |
+
<!-- Toggle listen button: image acts as the button and switches between play/pause -->
|
| 74 |
+
<!-- Replace the button with a single image that acts as a toggle control -->
|
| 75 |
+
|
| 76 |
+
<div style="display:flex;margin-top:1.8vw;gap:2vw;">
|
| 77 |
+
<img class="listen-img"
|
| 78 |
+
[src]="isPlayingVideo ? pauseIconDataUrl : playIconDataUrl"
|
| 79 |
+
[attr.alt]="isPlayingVideo ? 'Pause pronunciation' : 'Play pronunciation'"
|
| 80 |
+
[attr.aria-label]="isPlayingVideo ? 'Pause pronunciation' : 'Play pronunciation'"
|
| 81 |
+
[attr.aria-pressed]="isPlayingVideo"
|
| 82 |
+
role="button"
|
| 83 |
+
tabindex="0"
|
| 84 |
+
(click)="toggleVideoPlay()"
|
| 85 |
+
(keydown.enter)="toggleVideoPlay()"
|
| 86 |
+
(keydown.space)="toggleVideoPlay(); $event.preventDefault()" />
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
<button class="progress-btn"
|
| 90 |
+
[class.recording]="isRecording"
|
| 91 |
+
[attr.aria-pressed]="isRecording"
|
| 92 |
+
(click)="toggleRecording()"
|
| 93 |
+
type="button">
|
| 94 |
+
<svg class="progress-ring" width="85" height="85" viewBox="0 0 90 90" aria-hidden="true">
|
| 95 |
+
<circle class="progress-ring__background"
|
| 96 |
+
stroke="#3aaea8"
|
| 97 |
+
stroke-width="6"
|
| 98 |
+
fill="transparent"
|
| 99 |
+
r="38"
|
| 100 |
+
cx="45"
|
| 101 |
+
cy="45" />
|
| 102 |
+
<circle class="progress-ring__bar"
|
| 103 |
+
stroke="#3aaea8"
|
| 104 |
+
stroke-linecap="round"
|
| 105 |
+
stroke-width="6"
|
| 106 |
+
fill="transparent"
|
| 107 |
+
r="38"
|
| 108 |
+
cx="45"
|
| 109 |
+
cy="45"
|
| 110 |
+
[attr.stroke-dasharray]="circumference"
|
| 111 |
+
[attr.stroke-dashoffset]="strokeDashoffset" />
|
| 112 |
+
</svg>
|
| 113 |
+
|
| 114 |
+
<div class="label" aria-hidden="true">
|
| 115 |
+
<!-- show a live mic while recording -->
|
| 116 |
+
<span class="action-text" *ngIf="isRecording">🎙️</span>
|
| 117 |
+
|
| 118 |
+
<!-- default mic before countdown starts -->
|
| 119 |
+
<span class="action-text" *ngIf="!isCountingDown && !isRecording">🎤</span>
|
| 120 |
+
|
| 121 |
+
<!-- countdown number -->
|
| 122 |
+
<span class="seconds" *ngIf="isCountingDown">{{ timeLeft | number:'1.0-0' }}</span>
|
| 123 |
+
</div>
|
| 124 |
+
</button>
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
</div>
|
| 128 |
+
|
| 129 |
+
<!-- RIGHT: Feedback panel -->
|
| 130 |
+
<div class="pp-right">
|
| 131 |
+
|
| 132 |
+
<!-- needle binding changed to use isOscillating -->
|
| 133 |
+
<div class="gauge-wrapper">
|
| 134 |
+
<div class="gauge">
|
| 135 |
+
<div class="gauge-arc"></div>
|
| 136 |
+
<div class="needle" [class.oscillate]="isOscillating" [style.--angle]="needleAngle + 'deg'"></div>
|
| 137 |
+
</div>
|
| 138 |
+
|
| 139 |
+
<div class="mic-badge">
|
| 140 |
+
<span class="score-span">{{score}}%</span>
|
| 141 |
+
</div>
|
| 142 |
+
</div>
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
<div class="fb-board">
|
| 147 |
+
<img src="assets/pronvideo/slate.png" alt="Sample Image">
|
| 148 |
+
<div class="center-text1">
|
| 149 |
+
{{ shortfeedback ? shortfeedback : 'Speak to get feedback' }}
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
<div class="container">
|
| 155 |
+
<button class="arrow left" (click)="prev()" [disabled]="index === 0">‹</button>
|
| 156 |
+
<span class="center-text">{{ current.letter }}</span>
|
| 157 |
+
<button class="arrow right" (click)="next()" [disabled]="index === items.length - 1">›</button>
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
</div>
|
| 161 |
+
|
| 162 |
+
</div>
|
| 163 |
+
|
| 164 |
+
</div>
|
| 165 |
+
<button aria-label="Close" class="user-guide-close-icon" (click)="closePopup()">×</button>
|
src/app/pronunciationragupgrade/pronunciationragupgrade.component.ts
ADDED
|
@@ -0,0 +1,679 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild, ChangeDetectorRef
|
| 3 |
+
} from '@angular/core';
|
| 4 |
+
import { HttpClient } from '@angular/common/http';
|
| 5 |
+
import { finalize, takeUntil } from 'rxjs/operators';
|
| 6 |
+
import { Subject } from 'rxjs';
|
| 7 |
+
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
| 8 |
+
|
| 9 |
+
interface PracticeItem {
|
| 10 |
+
letter: string;
|
| 11 |
+
word: string;
|
| 12 |
+
phonetics: string;
|
| 13 |
+
imgSrc: string;
|
| 14 |
+
audioSrc: string;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
@Component({
|
| 18 |
+
selector: 'app-pronunciationragupgrade',
|
| 19 |
+
templateUrl: './pronunciationragupgrade.component.html',
|
| 20 |
+
styleUrls: ['./pronunciationragupgrade.component.css']
|
| 21 |
+
})
|
| 22 |
+
export class PronunciationRagUpgradeComponent implements OnInit, OnDestroy {
|
| 23 |
+
|
| 24 |
+
@ViewChild('videoEl') videoElRef?: ElementRef<HTMLVideoElement>;
|
| 25 |
+
|
| 26 |
+
// ---------------- CONFIG ----------------
|
| 27 |
+
private API_BASE = location.hostname.endsWith('hf.space')
|
| 28 |
+
? 'https://pykara-py-learn-backend.hf.space'
|
| 29 |
+
: 'http://localhost:5000';
|
| 30 |
+
|
| 31 |
+
private readonly SCORE_ENDPOINT = `${this.API_BASE}/pronragupgrade/score`;
|
| 32 |
+
|
| 33 |
+
private readonly preferredMimeTypes = [
|
| 34 |
+
'audio/webm;codecs=opus',
|
| 35 |
+
'audio/webm',
|
| 36 |
+
'audio/ogg;codecs=opus',
|
| 37 |
+
'audio/ogg'
|
| 38 |
+
];
|
| 39 |
+
|
| 40 |
+
// ---------------- UI STATE ----------------
|
| 41 |
+
showVideo = false;
|
| 42 |
+
videoSrc = '';
|
| 43 |
+
isPlayingVideo = false;
|
| 44 |
+
playIconDataUrl = 'assets/pronvideo/play.png';
|
| 45 |
+
pauseIconDataUrl = 'assets/pronvideo/pause.png';
|
| 46 |
+
|
| 47 |
+
// ---------------- DATA ----------------
|
| 48 |
+
items: PracticeItem[] = [
|
| 49 |
+
{ letter: 'A', word: 'Apple', phonetics: '/ˈæpəl/', imgSrc: 'assets/pronvideo/animvideo/apple.mp4', audioSrc: 'assets/pronvideo/audio/apple.mp3' },
|
| 50 |
+
{ letter: 'B', word: 'Ball', phonetics: '/bɔːl/', imgSrc: 'assets/pronvideo/animvideo/ball.mp4', audioSrc: 'assets/pronvideo/audio/ball.mp3' },
|
| 51 |
+
{ letter: 'C', word: 'Cat', phonetics: '/kæt/', imgSrc: 'assets/pronvideo/animvideo/cat.mp4', audioSrc: 'assets/pronvideo/audio/cat.mp3' },
|
| 52 |
+
{ letter: 'D', word: 'Dog', phonetics: '/dɒɡ/', imgSrc: 'assets/pronvideo/animvideo/dog.mp4', audioSrc: 'assets/pronvideo/audio/dog.mp3' },
|
| 53 |
+
{ letter: 'E', word: 'Egg', phonetics: '/eɡ/', imgSrc: 'assets/pronvideo/animvideo/egg.mp4', audioSrc: 'assets/pronvideo/audio/egg.mp3' },
|
| 54 |
+
{ letter: 'F', word: 'Fish', phonetics: '/fɪʃ/', imgSrc: 'assets/pronvideo/animvideo/fish.mp4', audioSrc: 'assets/pronvideo/audio/fish.mp3' },
|
| 55 |
+
{ letter: 'G', word: 'Grapes', phonetics: '/ɡreɪps/', imgSrc: 'assets/pronvideo/animvideo/grapes.mp4', audioSrc: 'assets/pronvideo/audio/grapes.mp3' },
|
| 56 |
+
{ letter: 'H', word: 'Hat', phonetics: '/hæt/', imgSrc: 'assets/pronvideo/animvideo/hat.mp4', audioSrc: 'assets/pronvideo/audio/hat.mp3' },
|
| 57 |
+
{ letter: 'I', word: 'Ice cream', phonetics: '/ˈaɪs ˌkriːm/', imgSrc: 'assets/pronvideo/animvideo/icecream.mp4', audioSrc: 'assets/pronvideo/audio/icecream.mp3' },
|
| 58 |
+
{ letter: 'J', word: 'Jar', phonetics: '/dʒɑːr/', imgSrc: 'assets/pronvideo/animvideo/jar.mp4', audioSrc: 'assets/pronvideo/audio/jar.mp3' },
|
| 59 |
+
{ letter: 'K', word: 'Kite', phonetics: '/kaɪt/', imgSrc: 'assets/pronvideo/animvideo/kite.mp4', audioSrc: 'assets/pronvideo/audio/kite.mp3' },
|
| 60 |
+
{ letter: 'L', word: 'Lion', phonetics: '/ˈlaɪən/', imgSrc: 'assets/pronvideo/animvideo/lion.mp4', audioSrc: 'assets/pronvideo/audio/lion.mp3' },
|
| 61 |
+
{ letter: 'M', word: 'Moon', phonetics: '/muːn/', imgSrc: 'assets/pronvideo/animvideo/moon.mp4', audioSrc: 'assets/pronvideo/audio/moon.mp3' },
|
| 62 |
+
{ letter: 'N', word: 'Nest', phonetics: '/nest/', imgSrc: 'assets/pronvideo/animvideo/nest.mp4', audioSrc: 'assets/pronvideo/audio/nest.mp3' },
|
| 63 |
+
{ letter: 'O', word: 'Orange', phonetics: '/ˈɒrɪndʒ/', imgSrc: 'assets/pronvideo/animvideo/orange.mp4', audioSrc: 'assets/pronvideo/audio/orange.mp3' },
|
| 64 |
+
{ letter: 'P', word: 'Pig', phonetics: '/pɪɡ/', imgSrc: 'assets/pronvideo/animvideo/pig.mp4', audioSrc: 'assets/pronvideo/audio/pig.mp3' },
|
| 65 |
+
{ letter: 'Q', word: 'Queen', phonetics: '/kwiːn/', imgSrc: 'assets/pronvideo/animvideo/queen.mp4', audioSrc: 'assets/pronvideo/audio/queen.mp3' },
|
| 66 |
+
{ letter: 'R', word: 'Rabbit', phonetics: '/ˈræbɪt/', imgSrc: 'assets/pronvideo/animvideo/rabbit.mp4', audioSrc: 'assets/pronvideo/audio/rabbit.mp3' },
|
| 67 |
+
{ letter: 'S', word: 'Sun', phonetics: '/sʌn/', imgSrc: 'assets/pronvideo/animvideo/sun.mp4', audioSrc: 'assets/pronvideo/audio/sun.mp3' },
|
| 68 |
+
{ letter: 'T', word: 'Tree', phonetics: '/triː/', imgSrc: 'assets/pronvideo/animvideo/tree.mp4', audioSrc: 'assets/pronvideo/audio/tree.mp3' },
|
| 69 |
+
{ letter: 'U', word: 'Umbrella', phonetics: '/ʌmˈbrelə/', imgSrc: 'assets/pronvideo/animvideo/umbrella.mp4', audioSrc: 'assets/pronvideo/audio/umbrella.mp3' },
|
| 70 |
+
{ letter: 'V', word: 'Van', phonetics: '/væn/', imgSrc: 'assets/pronvideo/animvideo/van.mp4', audioSrc: 'assets/pronvideo/audio/van.mp3' },
|
| 71 |
+
{ letter: 'W', word: 'Watch', phonetics: '/wɒtʃ/', imgSrc: 'assets/pronvideo/animvideo/watch.mp4', audioSrc: 'assets/pronvideo/audio/watch.mp3' },
|
| 72 |
+
{ letter: 'X', word: 'Xylophone', phonetics: '/ˈzaɪləfəʊn/', imgSrc: 'assets/pronvideo/animvideo/xylophone.mp4', audioSrc: 'assets/pronvideo/audio/xylophone.mp3' },
|
| 73 |
+
{ letter: 'Y', word: 'Yarn', phonetics: '/jɑːn/', imgSrc: 'assets/pronvideo/animvideo/yarn.mp4', audioSrc: 'assets/pronvideo/audio/yarn.mp3' },
|
| 74 |
+
{ letter: 'Z', word: 'Zebra', phonetics: '/ˈzebrə/', imgSrc: 'assets/pronvideo/animvideo/zebra.mp4', audioSrc: 'assets/pronvideo/audio/zebra.mp3' }
|
| 75 |
+
];
|
| 76 |
+
|
| 77 |
+
index = 0;
|
| 78 |
+
get current(): PracticeItem { return this.items[this.index]; }
|
| 79 |
+
|
| 80 |
+
// ---------------- RECORDING ----------------
|
| 81 |
+
isRecording = false;
|
| 82 |
+
isScoring = false;
|
| 83 |
+
isOscillating = false;
|
| 84 |
+
|
| 85 |
+
private mediaStream?: MediaStream;
|
| 86 |
+
private mediaRecorder?: MediaRecorder;
|
| 87 |
+
private chunks: BlobPart[] = [];
|
| 88 |
+
private currentMimeType = 'audio/webm';
|
| 89 |
+
|
| 90 |
+
recordedAudioUrl: string | null = null;
|
| 91 |
+
lastRecordedBlob: Blob | null = null;
|
| 92 |
+
|
| 93 |
+
// ---------------- SILENCE DETECTION ----------------
|
| 94 |
+
private audioCtx?: AudioContext;
|
| 95 |
+
private analyser?: AnalyserNode;
|
| 96 |
+
private micSource?: MediaStreamAudioSourceNode;
|
| 97 |
+
private silenceCheckId?: number;
|
| 98 |
+
|
| 99 |
+
private lastSpeechAt = 0;
|
| 100 |
+
private recordingStartedAt = 0;
|
| 101 |
+
private hasSpoken = false;
|
| 102 |
+
|
| 103 |
+
private readonly silenceMs = 5000;
|
| 104 |
+
private readonly startSilenceMs = 5000;
|
| 105 |
+
private readonly silenceThreshold = 0.01;
|
| 106 |
+
|
| 107 |
+
// ---------------- COUNTDOWN ----------------
|
| 108 |
+
duration = 3;
|
| 109 |
+
isCountingDown = false;
|
| 110 |
+
timeLeft = this.duration;
|
| 111 |
+
private preRecordIntervalId?: number;
|
| 112 |
+
|
| 113 |
+
readonly radius = 38;
|
| 114 |
+
readonly circumference = 2 * Math.PI * this.radius;
|
| 115 |
+
strokeDashoffset = this.circumference;
|
| 116 |
+
|
| 117 |
+
// ---------------- RESULT ----------------
|
| 118 |
+
showResult = false;
|
| 119 |
+
score = 0;
|
| 120 |
+
stars = 0;
|
| 121 |
+
feedbackLines: string[] = [];
|
| 122 |
+
videoUrl = '';
|
| 123 |
+
private lastVideoBlobUrl: string | null = null;
|
| 124 |
+
shortfeedback: string = '';
|
| 125 |
+
|
| 126 |
+
// ---------------- CANCEL / RESET CONTROL ----------------
|
| 127 |
+
private cancelScoring$ = new Subject<void>(); // cancels API call
|
| 128 |
+
private recordRunId = 0; // blocks onstop->API after navigation
|
| 129 |
+
|
| 130 |
+
// ---------------- LIFECYCLE ----------------
|
| 131 |
+
constructor(
|
| 132 |
+
private http: HttpClient,
|
| 133 |
+
public dialogRef: MatDialogRef<PronunciationRagUpgradeComponent>,
|
| 134 |
+
@Inject(MAT_DIALOG_DATA) public data: any,
|
| 135 |
+
private cdr: ChangeDetectorRef
|
| 136 |
+
) { }
|
| 137 |
+
|
| 138 |
+
ngOnInit(): void {
|
| 139 |
+
this.setupBestMimeType();
|
| 140 |
+
this.resetResult();
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
ngOnDestroy(): void {
|
| 144 |
+
// cancel any pending api
|
| 145 |
+
this.cancelScoring$.next();
|
| 146 |
+
this.cancelScoring$.complete();
|
| 147 |
+
|
| 148 |
+
this.stopTracks();
|
| 149 |
+
this.safeStopRecorder();
|
| 150 |
+
this.teardownAudioGraph();
|
| 151 |
+
|
| 152 |
+
if (this.lastVideoBlobUrl) {
|
| 153 |
+
try { URL.revokeObjectURL(this.lastVideoBlobUrl); } catch { }
|
| 154 |
+
this.lastVideoBlobUrl = null;
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
// ---------------- RECORD CONTROL ----------------
|
| 159 |
+
async toggleRecording(): Promise<void> {
|
| 160 |
+
if (this.isRecording) {
|
| 161 |
+
this.stopRecording(false);
|
| 162 |
+
return;
|
| 163 |
+
}
|
| 164 |
+
this.startPreRecordCountdown();
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
private startPreRecordCountdown(): void {
|
| 168 |
+
this.score = 0;
|
| 169 |
+
this.shortfeedback = '';
|
| 170 |
+
|
| 171 |
+
if (this.isCountingDown || this.isRecording) return;
|
| 172 |
+
|
| 173 |
+
// stop any previous run/api
|
| 174 |
+
this.cancelScoring$.next();
|
| 175 |
+
this.isScoring = false;
|
| 176 |
+
this.isOscillating = false;
|
| 177 |
+
|
| 178 |
+
this.isCountingDown = true;
|
| 179 |
+
this.timeLeft = this.duration;
|
| 180 |
+
|
| 181 |
+
const totalMs = this.duration * 1000;
|
| 182 |
+
const start = performance.now();
|
| 183 |
+
|
| 184 |
+
this.strokeDashoffset = this.circumference;
|
| 185 |
+
|
| 186 |
+
this.preRecordIntervalId = window.setInterval(() => {
|
| 187 |
+
const elapsed = performance.now() - start;
|
| 188 |
+
const progress = Math.min(1, elapsed / totalMs);
|
| 189 |
+
|
| 190 |
+
this.strokeDashoffset = this.circumference * (1 - progress);
|
| 191 |
+
this.timeLeft = Math.ceil((totalMs - elapsed) / 1000);
|
| 192 |
+
|
| 193 |
+
if (elapsed >= totalMs) {
|
| 194 |
+
if (this.preRecordIntervalId) {
|
| 195 |
+
try { clearInterval(this.preRecordIntervalId); } catch { }
|
| 196 |
+
this.preRecordIntervalId = undefined;
|
| 197 |
+
}
|
| 198 |
+
this.startRecordingInternal();
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
try { this.cdr.detectChanges(); } catch { }
|
| 202 |
+
}, 100);
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
private async startRecordingInternal(): Promise<void> {
|
| 206 |
+
this.isCountingDown = false;
|
| 207 |
+
|
| 208 |
+
// new run id (used to ignore late onstop)
|
| 209 |
+
const myRunId = ++this.recordRunId;
|
| 210 |
+
|
| 211 |
+
this.mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
| 212 |
+
this.mediaRecorder = new MediaRecorder(this.mediaStream, { mimeType: this.currentMimeType });
|
| 213 |
+
this.chunks = [];
|
| 214 |
+
|
| 215 |
+
this.mediaRecorder.ondataavailable = e => e.data.size && this.chunks.push(e.data);
|
| 216 |
+
|
| 217 |
+
this.mediaRecorder.onstop = () => {
|
| 218 |
+
// If user navigated/reset, ignore this stop event
|
| 219 |
+
if (myRunId !== this.recordRunId) return;
|
| 220 |
+
this.onRecordingStopped(myRunId);
|
| 221 |
+
};
|
| 222 |
+
|
| 223 |
+
this.isRecording = true;
|
| 224 |
+
|
| 225 |
+
this.setupSilenceDetection(this.mediaStream);
|
| 226 |
+
this.mediaRecorder.start();
|
| 227 |
+
|
| 228 |
+
try { this.cdr.detectChanges(); } catch { }
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
stopRecording(_isAutoStop: boolean = false): void {
|
| 232 |
+
if (!this.isRecording) return;
|
| 233 |
+
|
| 234 |
+
// stop countdown interval (if any)
|
| 235 |
+
if (this.preRecordIntervalId) {
|
| 236 |
+
try { clearInterval(this.preRecordIntervalId); } catch { }
|
| 237 |
+
this.preRecordIntervalId = undefined;
|
| 238 |
+
}
|
| 239 |
+
this.isCountingDown = false;
|
| 240 |
+
|
| 241 |
+
this.isRecording = false;
|
| 242 |
+
|
| 243 |
+
this.safeStopRecorder();
|
| 244 |
+
this.stopTracks();
|
| 245 |
+
this.teardownAudioGraph();
|
| 246 |
+
|
| 247 |
+
try { this.cdr.detectChanges(); } catch { }
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
private safeStopRecorder(): void {
|
| 251 |
+
try {
|
| 252 |
+
if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
|
| 253 |
+
this.mediaRecorder.stop();
|
| 254 |
+
}
|
| 255 |
+
} catch { }
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
private onRecordingStopped(runId: number): void {
|
| 259 |
+
// if reset happened, do nothing
|
| 260 |
+
if (runId !== this.recordRunId) return;
|
| 261 |
+
|
| 262 |
+
const blob = new Blob(this.chunks, { type: this.currentMimeType });
|
| 263 |
+
this.chunks = [];
|
| 264 |
+
|
| 265 |
+
// If audio is too small, do not call API
|
| 266 |
+
if (!blob || blob.size < 2000) {
|
| 267 |
+
this.isOscillating = false;
|
| 268 |
+
this.isScoring = false;
|
| 269 |
+
this.shortfeedback = 'No voice detected. Please try again.';
|
| 270 |
+
this.showResult = true;
|
| 271 |
+
try { this.cdr.detectChanges(); } catch { }
|
| 272 |
+
return;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
this.lastRecordedBlob = blob;
|
| 276 |
+
|
| 277 |
+
// start gauge oscillation while scoring
|
| 278 |
+
this.isOscillating = true;
|
| 279 |
+
try { this.cdr.detectChanges(); } catch { }
|
| 280 |
+
|
| 281 |
+
this.sendForScoring(blob, this.current.word, runId);
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
// ---------------- SILENCE DETECTION (AUTO STOP) ----------------
|
| 285 |
+
private setupSilenceDetection(stream: MediaStream): void {
|
| 286 |
+
this.teardownAudioGraph();
|
| 287 |
+
|
| 288 |
+
this.audioCtx = new AudioContext();
|
| 289 |
+
this.analyser = this.audioCtx.createAnalyser();
|
| 290 |
+
this.analyser.fftSize = 2048;
|
| 291 |
+
|
| 292 |
+
this.micSource = this.audioCtx.createMediaStreamSource(stream);
|
| 293 |
+
this.micSource.connect(this.analyser);
|
| 294 |
+
|
| 295 |
+
this.recordingStartedAt = performance.now();
|
| 296 |
+
this.lastSpeechAt = this.recordingStartedAt;
|
| 297 |
+
this.hasSpoken = false;
|
| 298 |
+
|
| 299 |
+
const loop = () => {
|
| 300 |
+
if (!this.analyser) return;
|
| 301 |
+
if (!this.isRecording) return;
|
| 302 |
+
|
| 303 |
+
const data = new Float32Array(this.analyser.fftSize);
|
| 304 |
+
this.analyser.getFloatTimeDomainData(data);
|
| 305 |
+
|
| 306 |
+
let sumSq = 0;
|
| 307 |
+
for (let i = 0; i < data.length; i++) sumSq += data[i] * data[i];
|
| 308 |
+
const rms = Math.sqrt(sumSq / data.length);
|
| 309 |
+
|
| 310 |
+
const now = performance.now();
|
| 311 |
+
|
| 312 |
+
if (rms > this.silenceThreshold) {
|
| 313 |
+
this.lastSpeechAt = now;
|
| 314 |
+
this.hasSpoken = true;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
// A) never spoke -> stop
|
| 318 |
+
if (!this.hasSpoken && (now - this.recordingStartedAt) > this.startSilenceMs) {
|
| 319 |
+
this.stopRecording(true);
|
| 320 |
+
return;
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
// B) spoke then silent -> stop
|
| 324 |
+
if (this.hasSpoken && (now - this.lastSpeechAt) > this.silenceMs) {
|
| 325 |
+
this.stopRecording(true);
|
| 326 |
+
return;
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
this.silenceCheckId = window.setTimeout(loop, 100);
|
| 330 |
+
};
|
| 331 |
+
|
| 332 |
+
loop();
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
private teardownAudioGraph(): void {
|
| 336 |
+
if (this.silenceCheckId) {
|
| 337 |
+
try { clearTimeout(this.silenceCheckId); } catch { }
|
| 338 |
+
this.silenceCheckId = undefined;
|
| 339 |
+
}
|
| 340 |
+
try { this.micSource?.disconnect(); } catch { }
|
| 341 |
+
try { this.analyser?.disconnect(); } catch { }
|
| 342 |
+
try { this.audioCtx?.close(); } catch { }
|
| 343 |
+
|
| 344 |
+
this.micSource = undefined;
|
| 345 |
+
this.analyser = undefined;
|
| 346 |
+
this.audioCtx = undefined;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
private stopTracks(): void {
|
| 350 |
+
this.mediaStream?.getTracks().forEach(t => t.stop());
|
| 351 |
+
this.mediaStream = undefined;
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
// ---------------- BACKEND ----------------
|
| 355 |
+
private sendForScoring(blob: Blob, word: string, runId: number): void {
|
| 356 |
+
// if reset happened, do nothing
|
| 357 |
+
if (runId !== this.recordRunId) return;
|
| 358 |
+
|
| 359 |
+
const fd = new FormData();
|
| 360 |
+
fd.append('audio', blob, 'student.webm');
|
| 361 |
+
fd.append('word', word.toLowerCase());
|
| 362 |
+
|
| 363 |
+
this.isScoring = true;
|
| 364 |
+
|
| 365 |
+
this.http.post<any>(this.SCORE_ENDPOINT, fd)
|
| 366 |
+
.pipe(
|
| 367 |
+
takeUntil(this.cancelScoring$), // ✅ cancel API on navigation
|
| 368 |
+
finalize(() => {
|
| 369 |
+
this.isScoring = false;
|
| 370 |
+
this.isOscillating = false;
|
| 371 |
+
try { this.cdr.detectChanges(); } catch { }
|
| 372 |
+
})
|
| 373 |
+
)
|
| 374 |
+
.subscribe(res => {
|
| 375 |
+
// If user navigated/reset while API was in progress, ignore response
|
| 376 |
+
if (runId !== this.recordRunId) return;
|
| 377 |
+
|
| 378 |
+
this.score = this.normalizeScore(res.score);
|
| 379 |
+
this.stars = this.mapStars(this.score);
|
| 380 |
+
this.shortfeedback = res.feedback;
|
| 381 |
+
this.feedbackLines = this.mapFeedbackFromStatus(res.status);
|
| 382 |
+
this.showResult = true;
|
| 383 |
+
|
| 384 |
+
if (res.videoBlobBase64) {
|
| 385 |
+
const bytes = Uint8Array.from(atob(res.videoBlobBase64), c => c.charCodeAt(0));
|
| 386 |
+
const videoBlob = new Blob([bytes], { type: 'video/mp4' });
|
| 387 |
+
|
| 388 |
+
// revoke previous
|
| 389 |
+
if (this.lastVideoBlobUrl) {
|
| 390 |
+
try { URL.revokeObjectURL(this.lastVideoBlobUrl); } catch { }
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
this.videoUrl = URL.createObjectURL(videoBlob);
|
| 394 |
+
this.lastVideoBlobUrl = this.videoUrl;
|
| 395 |
+
this.tryPlayFeedbackVideo(this.videoUrl);
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
try { this.cdr.detectChanges(); } catch { }
|
| 399 |
+
}, _err => {
|
| 400 |
+
if (runId !== this.recordRunId) return;
|
| 401 |
+
this.shortfeedback = 'Error while scoring. Please try again.';
|
| 402 |
+
this.showResult = true;
|
| 403 |
+
try { this.cdr.detectChanges(); } catch { }
|
| 404 |
+
});
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
// ---------------- HARD RESET (for navigation) ----------------
|
| 408 |
+
private cancelAllRunningProcesses(): void {
|
| 409 |
+
// 1) block any pending onstop -> API
|
| 410 |
+
this.recordRunId++;
|
| 411 |
+
|
| 412 |
+
// 2) cancel API call if running
|
| 413 |
+
this.cancelScoring$.next();
|
| 414 |
+
|
| 415 |
+
// 3) stop oscillation/scoring UI immediately
|
| 416 |
+
this.isScoring = false;
|
| 417 |
+
this.isOscillating = false;
|
| 418 |
+
|
| 419 |
+
// 4) stop countdown
|
| 420 |
+
if (this.preRecordIntervalId) {
|
| 421 |
+
try { clearInterval(this.preRecordIntervalId); } catch { }
|
| 422 |
+
this.preRecordIntervalId = undefined;
|
| 423 |
+
}
|
| 424 |
+
this.isCountingDown = false;
|
| 425 |
+
this.timeLeft = this.duration;
|
| 426 |
+
this.strokeDashoffset = this.circumference;
|
| 427 |
+
|
| 428 |
+
// 5) stop recording + audio graph + tracks
|
| 429 |
+
this.isRecording = false;
|
| 430 |
+
this.safeStopRecorder();
|
| 431 |
+
this.stopTracks();
|
| 432 |
+
this.teardownAudioGraph();
|
| 433 |
+
this.chunks = [];
|
| 434 |
+
|
| 435 |
+
// 6) reset speaker/video (pause button -> play)
|
| 436 |
+
this.resetVideoPlayerState();
|
| 437 |
+
|
| 438 |
+
try { this.cdr.detectChanges(); } catch { }
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
private resetVideoPlayerState(): void {
|
| 442 |
+
try {
|
| 443 |
+
const v = this.videoElRef?.nativeElement;
|
| 444 |
+
if (v) {
|
| 445 |
+
v.pause();
|
| 446 |
+
v.currentTime = 0;
|
| 447 |
+
v.removeAttribute('src');
|
| 448 |
+
v.load();
|
| 449 |
+
}
|
| 450 |
+
} catch { }
|
| 451 |
+
|
| 452 |
+
this.showVideo = false; // teacher looping video will show
|
| 453 |
+
this.videoSrc = '';
|
| 454 |
+
this.isPlayingVideo = false;
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
// ---------------- FEEDBACK ----------------
|
| 458 |
+
private mapFeedbackFromStatus(status: string): string[] {
|
| 459 |
+
switch (status) {
|
| 460 |
+
case 'success': return ['Excellent pronunciation.', 'Very clear sound.'];
|
| 461 |
+
case 'vowel_error': return ['Check your vowel sound.', 'Open your mouth clearly.'];
|
| 462 |
+
case 'consonant_error': return ['Focus on consonant sound.', 'Try again slowly.'];
|
| 463 |
+
case 'ending_error': return ['Ending sound missing.', 'Finish the word properly.'];
|
| 464 |
+
case 'stress_error': return ['Stress needs correction.', 'Listen and repeat.'];
|
| 465 |
+
case 'wrong_word': return ['Wrong word spoken.', 'Say only the target word.'];
|
| 466 |
+
case 'silence': return ['No speech detected.', 'Please try again.'];
|
| 467 |
+
default: return ['Good attempt.', 'Try once more.'];
|
| 468 |
+
}
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
private normalizeScore(n: any): number {
|
| 472 |
+
n = Number(n);
|
| 473 |
+
return isNaN(n) ? 0 : Math.min(100, Math.max(0, Math.round(n)));
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
private mapStars(s: number): number {
|
| 477 |
+
if (s >= 90) return 5;
|
| 478 |
+
if (s >= 80) return 4;
|
| 479 |
+
if (s >= 70) return 3;
|
| 480 |
+
if (s >= 60) return 2;
|
| 481 |
+
return 1;
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
// ---------------- VIDEO ----------------
|
| 485 |
+
private tryPlayFeedbackVideo(url: string): void {
|
| 486 |
+
this.showVideo = true;
|
| 487 |
+
this.videoSrc = url;
|
| 488 |
+
|
| 489 |
+
setTimeout(() => {
|
| 490 |
+
const v = this.videoElRef?.nativeElement;
|
| 491 |
+
if (!v) return;
|
| 492 |
+
|
| 493 |
+
v.src = url;
|
| 494 |
+
v.load();
|
| 495 |
+
|
| 496 |
+
v.play()
|
| 497 |
+
.then(() => {
|
| 498 |
+
this.isPlayingVideo = true; // ✅ icon becomes pause
|
| 499 |
+
try { this.cdr.detectChanges(); } catch { }
|
| 500 |
+
})
|
| 501 |
+
.catch(() => {
|
| 502 |
+
this.isPlayingVideo = false;
|
| 503 |
+
try { this.cdr.detectChanges(); } catch { }
|
| 504 |
+
});
|
| 505 |
+
}, 0);
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
// ---------------- UTILS ----------------
|
| 509 |
+
private setupBestMimeType(): void {
|
| 510 |
+
for (const t of this.preferredMimeTypes) {
|
| 511 |
+
if ((window as any).MediaRecorder?.isTypeSupported(t)) {
|
| 512 |
+
this.currentMimeType = t;
|
| 513 |
+
return;
|
| 514 |
+
}
|
| 515 |
+
}
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
private resetResult(): void {
|
| 519 |
+
this.showResult = false;
|
| 520 |
+
this.score = 0;
|
| 521 |
+
this.stars = 0;
|
| 522 |
+
this.feedbackLines = [];
|
| 523 |
+
this.showVideo = false;
|
| 524 |
+
this.videoSrc = '';
|
| 525 |
+
this.shortfeedback = '';
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
// ---------------------------
|
| 529 |
+
// PLAY MODEL PRONUNCIATION AUDIO
|
| 530 |
+
// ---------------------------
|
| 531 |
+
playWordAudio(): void {
|
| 532 |
+
const src = this.current?.audioSrc || this.getAudioSrcFromWord(this.current.word);
|
| 533 |
+
if (!src) return;
|
| 534 |
+
|
| 535 |
+
try {
|
| 536 |
+
const audio = new Audio(src);
|
| 537 |
+
audio.currentTime = 0;
|
| 538 |
+
audio.play().catch(() => { });
|
| 539 |
+
} catch { }
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
private getAudioSrcFromWord(word: string): string {
|
| 543 |
+
if (!word) return '';
|
| 544 |
+
const fileName = word.trim().toLowerCase().replace(/\s+/g, '-');
|
| 545 |
+
return `assets/pronvideo/audio/${fileName}.mp3`;
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
// ---------------------------
|
| 549 |
+
// VIDEO END HANDLER
|
| 550 |
+
// ---------------------------
|
| 551 |
+
onVideoEnded(): void {
|
| 552 |
+
this.resetVideoPlayerState();
|
| 553 |
+
this.isOscillating = false;
|
| 554 |
+
try { this.cdr.detectChanges(); } catch { }
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
onVideoPlay(): void {
|
| 558 |
+
this.isPlayingVideo = true;
|
| 559 |
+
try { this.cdr.detectChanges(); } catch { }
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
+
onVideoPause(): void {
|
| 563 |
+
this.isPlayingVideo = false;
|
| 564 |
+
try { this.cdr.detectChanges(); } catch { }
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
// ---------------------------
|
| 568 |
+
// TOGGLE PLAY / PAUSE FOR VIDEO
|
| 569 |
+
// ---------------------------
|
| 570 |
+
toggleVideoPlay(): void {
|
| 571 |
+
try {
|
| 572 |
+
const v = this.videoElRef?.nativeElement;
|
| 573 |
+
|
| 574 |
+
if (!this.showVideo) {
|
| 575 |
+
this.videoSrc = this.getVideoSrcFromWord(this.current.word);
|
| 576 |
+
this.showVideo = true;
|
| 577 |
+
|
| 578 |
+
setTimeout(() => {
|
| 579 |
+
const video = this.videoElRef?.nativeElement;
|
| 580 |
+
if (!video) return;
|
| 581 |
+
|
| 582 |
+
video.src = this.videoSrc;
|
| 583 |
+
video.load();
|
| 584 |
+
video.play()
|
| 585 |
+
.then(() => {
|
| 586 |
+
this.isPlayingVideo = true;
|
| 587 |
+
try { this.cdr.detectChanges(); } catch { }
|
| 588 |
+
})
|
| 589 |
+
.catch(() => {
|
| 590 |
+
this.isPlayingVideo = false;
|
| 591 |
+
try { this.cdr.detectChanges(); } catch { }
|
| 592 |
+
});
|
| 593 |
+
}, 0);
|
| 594 |
+
|
| 595 |
+
return;
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
if (!v) return;
|
| 599 |
+
|
| 600 |
+
if (v.paused) {
|
| 601 |
+
v.play()
|
| 602 |
+
.then(() => {
|
| 603 |
+
this.isPlayingVideo = true;
|
| 604 |
+
try { this.cdr.detectChanges(); } catch { }
|
| 605 |
+
})
|
| 606 |
+
.catch(() => {
|
| 607 |
+
this.isPlayingVideo = false;
|
| 608 |
+
try { this.cdr.detectChanges(); } catch { }
|
| 609 |
+
});
|
| 610 |
+
} else {
|
| 611 |
+
v.pause();
|
| 612 |
+
this.isPlayingVideo = false;
|
| 613 |
+
try { this.cdr.detectChanges(); } catch { }
|
| 614 |
+
}
|
| 615 |
+
} catch { }
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
private getVideoSrcFromWord(word: string): string {
|
| 619 |
+
if (!word) return '';
|
| 620 |
+
const fileName = word.trim().toLowerCase().replace(/\s+/g, '-');
|
| 621 |
+
return `assets/pronvideo/videos/${fileName}.mp4`;
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
// ---------------------------
|
| 625 |
+
// GAUGE NEEDLE ANGLE (0–100 → -90° to +90°)
|
| 626 |
+
// ---------------------------
|
| 627 |
+
get needleAngle(): number {
|
| 628 |
+
const value = Math.max(0, Math.min(100, Number(this.score || 0)));
|
| 629 |
+
return -90 + (value * 1.8);
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
// ---------------------------
|
| 633 |
+
// NAVIGATION : PREVIOUS / NEXT WORD
|
| 634 |
+
// ---------------------------
|
| 635 |
+
prev(): void {
|
| 636 |
+
if (this.index <= 0) return;
|
| 637 |
+
|
| 638 |
+
// ✅ stop api + reset oscillation + reset speaker + stop countdown/record
|
| 639 |
+
this.cancelAllRunningProcesses();
|
| 640 |
+
|
| 641 |
+
this.index--;
|
| 642 |
+
this.resetAfterNavigation();
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
next(): void {
|
| 646 |
+
if (this.index >= this.items.length - 1) return;
|
| 647 |
+
|
| 648 |
+
// ✅ stop api + reset oscillation + reset speaker + stop countdown/record
|
| 649 |
+
this.cancelAllRunningProcesses();
|
| 650 |
+
|
| 651 |
+
this.index++;
|
| 652 |
+
this.resetAfterNavigation();
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
private resetAfterNavigation(): void {
|
| 656 |
+
// ensure everything is stopped
|
| 657 |
+
this.cancelAllRunningProcesses();
|
| 658 |
+
|
| 659 |
+
this.score = 0;
|
| 660 |
+
this.stars = 0;
|
| 661 |
+
this.feedbackLines = [];
|
| 662 |
+
this.showResult = false;
|
| 663 |
+
this.shortfeedback = '';
|
| 664 |
+
|
| 665 |
+
this.lastRecordedBlob = null;
|
| 666 |
+
if (this.recordedAudioUrl) {
|
| 667 |
+
try { URL.revokeObjectURL(this.recordedAudioUrl); } catch { }
|
| 668 |
+
this.recordedAudioUrl = null;
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
+
try { this.cdr.detectChanges(); } catch { }
|
| 672 |
+
}
|
| 673 |
+
|
| 674 |
+
closePopup(): void {
|
| 675 |
+
// stop everything before closing
|
| 676 |
+
this.cancelAllRunningProcesses();
|
| 677 |
+
this.dialogRef.close();
|
| 678 |
+
}
|
| 679 |
+
}
|
src/assets/pronvideo/animvideo/apple.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:158222ad1b07188440d77474f9cdc6002973e5ed9213a234e603ff66adaa37ca
|
| 3 |
+
size 5790714
|
src/assets/pronvideo/animvideo/ball.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bbf6c21e57ebb0f34acf11dfc57eca9a6ceb944708b1dd573e0bb4f082efe7d7
|
| 3 |
+
size 5634357
|
src/assets/pronvideo/animvideo/cat.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0a4e400917eb73e7b2481b5e89799f4c877f860b96fef49e61f1a6fee2db718d
|
| 3 |
+
size 5360080
|
src/assets/pronvideo/animvideo/dog.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:aced7cd298f418f1d7ac6952c99df14d6030bd56a22ac17d08352bf283b1e0de
|
| 3 |
+
size 5342277
|
src/assets/pronvideo/animvideo/egg.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ea0b1500966539eee652962355c20c380a903ee79bd79ef57e3c730302525382
|
| 3 |
+
size 4935460
|
src/assets/pronvideo/animvideo/fish.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0c724abc31b32d058b69c2142093c24504fa863dc5f7e7fb78310e6c78295013
|
| 3 |
+
size 4907809
|
src/assets/pronvideo/animvideo/grapes.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e8e26b650686a72c010852f336982b188f779af5938d554bf86f755a5fcdf9b6
|
| 3 |
+
size 9053666
|
src/assets/pronvideo/animvideo/hat.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9a435204cb5e26fb7dda794b0b1ae482445841bd8643a3bd8eb4dbca2fa69921
|
| 3 |
+
size 7203469
|
src/assets/pronvideo/animvideo/icecream.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:89530e185f406945a71b19300c5a00711ada88641af6dcb6c59ab5dc5c5835c4
|
| 3 |
+
size 6187864
|
src/assets/pronvideo/animvideo/jar.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:85e954d6f782f5d17da42cf60c2a061d114068396285fd5d14602ca88f975d5d
|
| 3 |
+
size 9213510
|
src/assets/pronvideo/animvideo/kite.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ba84336efc7cb4e687eee64ed861cd753fe99781cc6c5bebe3cefe1b4b5ca4e7
|
| 3 |
+
size 1843752
|
src/assets/pronvideo/animvideo/lion.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:eef94bc7cd6806e735254fdd077c3c211ca6cb1b5eefff27e125a7723ca3b305
|
| 3 |
+
size 6097806
|
src/assets/pronvideo/animvideo/moon.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:147b9e565116439983091812d786a55752e4e44511d7bf61fa218ac9a0805464
|
| 3 |
+
size 4901274
|
src/assets/pronvideo/animvideo/nest.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:820d37c3b5b153753a58532c914e011d5cdbc90df30d1bd3c5788834bfbe6d84
|
| 3 |
+
size 7650498
|
src/assets/pronvideo/animvideo/orange.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e9e3c0daecbcf4c8c26c2ec43a0bc68531922bd621117849624a88f01e1eb5b4
|
| 3 |
+
size 6440257
|
src/assets/pronvideo/animvideo/pig.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b6db18186b1ac3e735eda91d9d73522f900a7414b83985fc063aa7b537be133e
|
| 3 |
+
size 6421810
|
src/assets/pronvideo/animvideo/queen.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b2e153fd06d061dc8f44595c3b5cccf1c11a2b4a3676c0d411f3586a4e965bd3
|
| 3 |
+
size 2724148
|
src/assets/pronvideo/animvideo/rabbit.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2bfb9ec3714cc6dcd7cae3be5594e53a2267a26da0eb82770a60ea5c91bba6b8
|
| 3 |
+
size 7266712
|
src/assets/pronvideo/animvideo/sun.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:85cb830b40c979014ed19ace95229fd8d806db3be52a87a3a387a20a017a6110
|
| 3 |
+
size 1269242
|
src/assets/pronvideo/animvideo/tree.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e6a80995e8b2ab65495d769ca18250c2a979d0b0a2865f8ea6bfcb6e70106a70
|
| 3 |
+
size 12132430
|
src/assets/pronvideo/animvideo/umbrella.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6df6d94e25ac51f98d33f3a452a937bb81c8d6000172fd51564bc8b43f925678
|
| 3 |
+
size 2489346
|
src/assets/pronvideo/animvideo/van.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:86c3bdd636d07bb59384b530c66cc6518ac251a5cdae3c02ca70818c8c69ae04
|
| 3 |
+
size 10097663
|
src/assets/pronvideo/animvideo/watch.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4ed42674319ae37c38f5d1e1b27c7deb7ade849e91a53e7e105c0fbed36f9701
|
| 3 |
+
size 8078945
|
src/assets/pronvideo/animvideo/xylophone.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3fa2d40bc1f9f0bc96f147b0696000b33607ad9662ca5dd319b2f429d6bcfaa6
|
| 3 |
+
size 2050298
|
src/assets/pronvideo/animvideo/yarn.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:742ba761ac8f917629fde9c560598fb2258131bb625d2d5c55f77c8440cf9e08
|
| 3 |
+
size 3490049
|
src/assets/pronvideo/animvideo/zebra.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1144f6b4892b7ed3b31d6895dd692b40428b8e985e41c0f7306087ad4a5a37fc
|
| 3 |
+
size 7161675
|
src/assets/pronvideo/listening.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:26c028bad47e42b3b89e05b2e603d90309b82d53193d7e2be0c88686c5dc57ea
|
| 3 |
+
size 3569866
|
src/assets/pronvideo/slate.png
ADDED
|
Git LFS Details
|
src/assets/pronvideo/videos/lion-old.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ca2d4e211b05380e1152ede6052e0751d29e46da96c78431d7bd22a83f301e94
|
| 3 |
+
size 5431178
|
src/assets/pronvideo/videos/lion.mp4
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:952c1a94530ce8e620851326e7d6d368be370b42709a3677aace687e925303f8
|
| 3 |
+
size 5127880
|
src/assets/pronvideo/videos/rabbit-old.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9c3b59991ee348ba0a9117ad5648fbf0ea03fc69298b4d3f8863d45dd94194ee
|
| 3 |
+
size 4932831
|
src/assets/pronvideo/videos/rabbit.mp4
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cebe3f87a003315b40ef9ceaa62ac7ed47f05b5e37858d891c413b5dd01626a2
|
| 3 |
+
size 4795049
|
src/assets/pronvideo/videos/van-old.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4724e2cab20be89b9148c699a33c2a433e239611a7787c45487942ba7ef9d981
|
| 3 |
+
size 4462232
|
src/assets/pronvideo/videos/van.mp4
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:19ef3b0027192a8d08ebd3069d0bbc09a3dbfdf2c10262ba0c756db54517d57b
|
| 3 |
+
size 4232804
|
src/assets/pronvideo/videos/xylophone-old.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:93fc9aef9f9419f344a0ba97ff3b5d1c0743ad121956f462e504e31388893164
|
| 3 |
+
size 5199424
|
src/assets/pronvideo/videos/xylophone.mp4
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dfc61e0844b3df40b4ec74694fd048c3811ba2dd5fd16b48ac48a2619aa4d3a6
|
| 3 |
+
size 18925813
|
src/assets/pronvideo/videos/yarn-old.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3fc19acd88c47a3f7c33d750ef0f77c2dce6272d51e9e532576c2d00bda52413
|
| 3 |
+
size 5095081
|
src/assets/pronvideo/videos/yarn.mp4
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:640cb94a1c14d8b733556a81f765319b3eea544b26022f5eece33530523c2b95
|
| 3 |
+
size 15318745
|
src/assets/pronvideo/videos/{zebra.mp4 → zebra-old.mp4}
RENAMED
|
File without changes
|