Commit 4b441059 authored by liujiangnan's avatar liujiangnan

feat: 迁移完成

parent 8a51c964
...@@ -30,7 +30,10 @@ ...@@ -30,7 +30,10 @@
], ],
"styles": [ "styles": [
"./node_modules/ng-zorro-antd/ng-zorro-antd.min.css", "./node_modules/ng-zorro-antd/ng-zorro-antd.min.css",
"src/styles.css" "./node_modules/font-awesome/css/font-awesome.css",
"./node_modules/bootstrap/dist/css/bootstrap.min.css",
"./node_modules/animate.css/animate.min.css",
"src/styles.scss"
], ],
"scripts": [] "scripts": []
}, },
...@@ -128,5 +131,8 @@ ...@@ -128,5 +131,8 @@
} }
} }
}, },
"defaultProject": "ng-template-generator" "defaultProject": "ng-template-generator",
} "cli": {
"analytics": "19e287b4-6661-4439-96bc-15b9295bb59c"
}
}
\ No newline at end of file
...@@ -24,6 +24,7 @@ import {AudioRecorderComponent} from './common/audio-recorder/audio-recorder.com ...@@ -24,6 +24,7 @@ import {AudioRecorderComponent} from './common/audio-recorder/audio-recorder.com
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome'; import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { fas } from '@fortawesome/free-solid-svg-icons'; import { fas } from '@fortawesome/free-solid-svg-icons';
import { far } from '@fortawesome/free-regular-svg-icons'; import { far } from '@fortawesome/free-regular-svg-icons';
import { BrLineBreakerPipe } from './pipes/br-line-breaker.pipe';
registerLocaleData(zh); registerLocaleData(zh);
...@@ -36,6 +37,7 @@ registerLocaleData(zh); ...@@ -36,6 +37,7 @@ registerLocaleData(zh);
AudioRecorderComponent, AudioRecorderComponent,
UploadImageWithPreviewComponent, UploadImageWithPreviewComponent,
BackgroundImagePipe, BackgroundImagePipe,
BrLineBreakerPipe,
ResourcePipe, ResourcePipe,
TimePipe, TimePipe,
UploadVideoComponent, UploadVideoComponent,
......
<div class="p-video-box">
<div class="up-video" style="display: flex;"> <div class="up-video" style="display: flex;">
<!--<nz-upload class="" [nzDisabled]="!showUploadBtn"--> <!--<nz-upload class="" [nzDisabled]="!showUploadBtn"-->
<!--[nzShowUploadList]="false"--> <!--[nzShowUploadList]="false"-->
...@@ -16,7 +16,8 @@ ...@@ -16,7 +16,8 @@
<button type="button" nz-button nzType="default" *ngIf="showUploadBtn" [disabled]="uploading" <button type="button" nz-button nzType="default" *ngIf="showUploadBtn" [disabled]="uploading"
[nzLoading]="uploading" > [nzLoading]="uploading" >
<i nz-icon nzType="plus" nzTheme="outline"></i> <i nz-icon type="plus" theme="outline"></i>
<span>{{ uploading ? 'Uploading' : 'Select Video' }}</span> <span>{{ uploading ? 'Uploading' : 'Select Video' }}</span>
<!--<span>Select Video</span>--> <!--<span>Select Video</span>-->
</button> </button>
...@@ -55,9 +56,9 @@ ...@@ -55,9 +56,9 @@
</div> </div>
<div class="p-box d-flex align-items-center p-video-uploader"> <div class="p-box d-flex align-items-center p-video-uploader" style="top: 20px;">
<div class="p-upload-icon" *ngIf="!showUploadBtn && !videoUrl && !uploading"> <div class="p-upload-icon" *ngIf="!showUploadBtn && !videoUrl && !uploading">
<i nz-icon nzType="upload" nzTheme="outline"></i> <i nz-icon type="upload" theme="outline"></i>
<div class="m-3"></div> <div class="m-3"></div>
<span>Click here to upload video</span> <span>Click here to upload video</span>
<div class="mt-5 p-progress-bar" *ngIf="uploading"> <div class="mt-5 p-progress-bar" *ngIf="uploading">
...@@ -69,26 +70,24 @@ ...@@ -69,26 +70,24 @@
[ngClass]="{'smart-bar': showUploadBtn}" > [ngClass]="{'smart-bar': showUploadBtn}" >
<div class="i-bg" [style.width]="progress+'%'"></div> <div class="i-bg" [style.width]="progress+'%'"></div>
<div class="i-text"> <div class="i-text">
<fa-icon icon="cloud-upload-alt"></fa-icon>
Uploading... Uploading...
</div> </div>
</div> </div>
<div class="p-upload-check-bg" *ngIf="checking"> <div class="p-upload-check-bg" *ngIf="checking">
<div class="i-bg" [style.width]="progress+'%'"></div> <div class="i-bg" [style.width]="progress+'%'"></div>
<div class="i-text"> <div class="i-text">
<fa-icon icon="cloud-upload-alt"></fa-icon> <i nz-icon type="loading" theme="outline"></i>Checking...
<i nz-icon nzType="loading" nzTheme="outline"></i>Checking...
</div> </div>
</div> </div>
<div class="p-preview" *ngIf="!uploading && videoUrl " > <div class="p-preview" *ngIf="!showUploadBtn && !uploading && videoUrl " >
<!--<video crossorigin="anonymous" [src]="videoUrl" controls #videoNode></video>--> <!--<video crossorigin="anonymous" [src]="videoUrl" controls #videoNode></video>-->
<video [src]="safeVideoUrl(videoUrl)" controls #videoNode></video> <video [src]="safeVideoUrl(videoUrl)" controls #videoNode (loadedmetadata)="videoLoadedMetaData()"></video>
</div> </div>
</div> </div>
<div [style.display]="!checkVideoExists?'none':''"> <div [style.display]="!checkVideoExists?'none':''">
<span><i nz-icon nzType="loading" nzTheme="outline"></i> checking file to upload</span> <span><i nz-icon type="loading" theme="outline"></i> checking file to upload</span>
</div>
</div> </div>
@import '../../style/common_mixin.css'; @import '../../style/common_mixin';
/*.p-video-box{
bottom: 0;
border: 2px dashed #ddd;
border-radius: 0.5rem;
background-color: #fafafa;
text-align: center;
color: #aaa;
padding-top: 56.25%;
//font-size: 4rem;
position: relative;
.p-upload-icon{
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50% ,-50%);
}
}*/
.p-video-uploader { .p-video-uploader {
position: relative; position: relative;
display: block; display: block;
...@@ -33,56 +17,50 @@ ...@@ -33,56 +17,50 @@
background-color: #fafafa; background-color: #fafafa;
text-align: center; text-align: center;
color: #aaa; color: #aaa;
.p-upload-icon {
text-align: center;
margin: auto;
.anticon-upload {
color: #888;
font-size: 5rem;
}
.p-progress-bar {
position: relative;
width: 20rem;
height: 1.5rem;
border: 1px solid #ccc;
border-radius: 1rem;
.p-progress-bg {
background-color: #1890ff;
border-radius: 1rem;
height: 100%;
}
.p-progress-value {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
text-shadow: 0 0 4px #000;
color: white;
text-align: center;
font-size: 0.9rem;
line-height: 1.5rem;
}
}
}
.p-preview {
width: 100%;
height: 100%;
//background-image: url("https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png");
video{
max-height: 100%;
max-width: 100%;
}
}
} }
} }
.p-upload-icon {
text-align: center;
margin: auto;
}
.p-upload-icon .anticon-upload {
color: #888;
font-size: 5rem;
}
p-progress-bar {
position: relative;
width: 20rem;
height: 1.5rem;
border: 1px solid #ccc;
border-radius: 1rem;
}
.p-progress-bar .p-progress-bg {
background-color: #1890ff;
border-radius: 1rem;
height: 100%;
}
.p-progress-bar .p-progress-value {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
text-shadow: 0 0 4px #000;
color: white;
text-align: center;
font-size: 0.9rem;
line-height: 1.5rem;
}
.p-preview {
width: 100%;
height: 100%;
//background-image: url("https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png");
}
.p-preview video{
max-height: 100%;
max-width: 100%;
position: absolute;
display: flex;
}
.p-btn-delete { .p-btn-delete {
position: absolute; position: absolute;
right: -0.5rem; right: -0.5rem;
......
import {Component, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, Output, SecurityContext, ViewChild} from '@angular/core'; import {Component, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, Output, SecurityContext, ViewChild} from '@angular/core';
import {NzMessageService, UploadChangeParam, UploadFile, UploadFileStatus} from 'ng-zorro-antd'; import {NzMessageService, UploadFile} from 'ng-zorro-antd';
import {DomSanitizer, SafeResourceUrl} from '@angular/platform-browser'; import {DomSanitizer, SafeResourceUrl} from '@angular/platform-browser';
...@@ -24,7 +24,7 @@ export class UploadVideoComponent implements OnChanges, OnDestroy { ...@@ -24,7 +24,7 @@ export class UploadVideoComponent implements OnChanges, OnDestroy {
@Input() @Input()
videoUrl = ''; videoUrl = '';
@ViewChild('videoNode', {static: true }) @ViewChild('videoNode')
videoNode: ElementRef; videoNode: ElementRef;
...@@ -47,8 +47,8 @@ export class UploadVideoComponent implements OnChanges, OnDestroy { ...@@ -47,8 +47,8 @@ export class UploadVideoComponent implements OnChanges, OnDestroy {
item: any; item: any;
// videoItem = null; // videoItem = null;
uploadUrl; uploadUrl = (<any> window).courseware.uploadUrl();
uploadData; uploadData = (<any> window).courseware.uploadData();
constructor(private nzMessageService: NzMessageService, constructor(private nzMessageService: NzMessageService,
private sanitization: DomSanitizer private sanitization: DomSanitizer
...@@ -81,7 +81,7 @@ export class UploadVideoComponent implements OnChanges, OnDestroy { ...@@ -81,7 +81,7 @@ export class UploadVideoComponent implements OnChanges, OnDestroy {
} }
safeVideoUrl(url) { safeVideoUrl(url) {
console.log(url); console.log(url)
return this.sanitization.bypassSecurityTrustResourceUrl(url); // `${url}`; return this.sanitization.bypassSecurityTrustResourceUrl(url); // `${url}`;
} }
videoLoadedMetaData() { videoLoadedMetaData() {
...@@ -89,7 +89,7 @@ export class UploadVideoComponent implements OnChanges, OnDestroy { ...@@ -89,7 +89,7 @@ export class UploadVideoComponent implements OnChanges, OnDestroy {
} }
handleChange(info: UploadChangeParam/* { type: string, file: UploadFile, event: any }*/): void { handleChange(info: { type: string, file: UploadFile, event: any }): void {
console.log('info:' , info); console.log('info:' , info);
...@@ -119,8 +119,8 @@ export class UploadVideoComponent implements OnChanges, OnDestroy { ...@@ -119,8 +119,8 @@ export class UploadVideoComponent implements OnChanges, OnDestroy {
break; break;
case 'progress': case 'progress':
this.progress = info.event.percent; this.progress = parseInt(info.event.percent, 10);
this.doProgress(this.progress); // this.doProgress(this.progress);
break; break;
} }
} }
...@@ -162,9 +162,9 @@ export class UploadVideoComponent implements OnChanges, OnDestroy { ...@@ -162,9 +162,9 @@ export class UploadVideoComponent implements OnChanges, OnDestroy {
if (duration) { if (duration) {
duration = duration * 1000; duration = duration * 1000;
} }
file.height = height; file['height'] = height;
file.width = width; file['width'] = width;
file.duration = duration; file['duration'] = duration;
vid.preload = 'none'; vid.preload = 'none';
vid.src = ''; vid.src = '';
vid.remove(); vid.remove();
......
@import '../style/common_mixin.css';
.model-content {
width: 100%;
height: 100%;
}
<div class="courseware-container d-flex row" #videoContainer>
<div class="model-content"> <div class="col-8">
<video class="courseware-video"
crossorigin="anonymous"
<div style="position: absolute; left: 200px; top: 100px; width: 800px;"> *ngIf="item.contentObj.video_url "
[src]="item.contentObj.video_url" controls
<input type="text" nz-input [(ngModel)]="item.text" (blur)="save()"> (loadeddata)="videoLoaded($event,item)"
(error)="videoError($event)"
<app-upload-image-with-preview (seeked)="videoSeeked($event)"
[picUrl]="item.pic_url" #videoNode ></video>
(imageUploaded)="onImageUploadSuccess($event, 'pic_url')" <!--&& item.contentObj.video_url.constructor.name === 'String'-->
></app-upload-image-with-preview> <div *ngIf="!item.contentObj.video_url">
<div class="p-video-box d-flex align-items-center">
<app-audio-recorder <div class="p-upload-icon">
[audioUrl]="item.audio_url" <i class="anticon anticon-caret-right"></i>
(audioUploaded)="onAudioUploadSuccess($event, 'audio_url')" <div class="m-3"></div>
></app-audio-recorder> </div>
<app-custom-hot-zone></app-custom-hot-zone> </div>
<app-upload-video></app-upload-video> </div>
<app-lesson-title-config></app-lesson-title-config> </div>
<div class="col-4">
<app-upload-video
(videoUploaded)="onVideoUploadSuccess($event)"
[item]="item"
[videoUrl]="item.contentObj.video_url"></app-upload-video>
</div> </div>
</div> </div>
<div class="row" style="margin-top: 1rem"
\ No newline at end of file *ngIf="item.contentObj.pic_id && item.contentObj.video_url">
<div class="col-8">
<button nz-button nzType="primary"
style="margin-bottom: 1rem"
(click)="setVideoCover()">
Set Video Cover
</button>
<img *ngIf="item.contentObj.pic_id"
[src]="item.contentObj.pic_id"
style="width:100%;overflow: hidden;box-shadow: 0 0 20px 0px #444;"/>
</div>
</div>
.p-video-box{
bottom: 0;
border: 2px dashed #ddd;
border-radius: 0.5rem;
background-color: #fafafa;
text-align: center;
color: #aaa;
padding-top: 56.25%;
font-size: 4rem;
.p-upload-icon{
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50% ,-50%);
}
}
.courseware-container{
.courseware-video{
//position: absolute;
width: 95%;
max-height: calc(100vh - 5rem);
}
}
import {Component, EventEmitter, Input, OnDestroy, OnChanges, OnInit, Output, ApplicationRef, ChangeDetectorRef} from '@angular/core'; import {Component, ElementRef, ViewChild, EventEmitter, Input, OnDestroy, OnChanges, OnInit, Output, ApplicationRef, ChangeDetectorRef} from '@angular/core';
import {NzMessageService, NzMessageDataFilled} from 'ng-zorro-antd';
@Component({ @Component({
selector: 'app-form', selector: 'app-form',
templateUrl: './form.component.html', templateUrl: './form.component.html',
styleUrls: ['./form.component.css'] styleUrls: ['./form.component.scss']
}) })
export class FormComponent implements OnInit, OnChanges, OnDestroy { export class FormComponent implements OnInit, OnChanges, OnDestroy {
// 储存数据用 // 储存数据用
saveKey = "test_0011"; saveKey = 'ww_video';
// 储存对象 // 储存对象
item; item;
@ViewChild('videoContainer')
videoContainer: ElementRef;
@ViewChild('videoNode')
videoNode: ElementRef;
newVideoCover = true;
private tooBigError: NzMessageDataFilled = null;
constructor(private appRef: ApplicationRef,private changeDetectorRef: ChangeDetectorRef) { constructor(private appRef: ApplicationRef,
private changeDetectorRef: ChangeDetectorRef,
private nzMessageService: NzMessageService) {
} }
ngOnInit() { ngOnInit() {
this.item = {}; this.item = {
contentObj: {
pic_id: null,
video_url: null
}
};
// 获取存储的数据 // 获取存储的数据
(<any> window).courseware.getData((data) => { (window as any).courseware.getData((data) => {
if (data) { if (data) {
this.item = data; this.item = data;
} }
this.init();
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
this.changeDetectorRef.detectChanges(); this.changeDetectorRef.detectChanges();
this.refresh(); this.refresh();
...@@ -47,35 +58,85 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy { ...@@ -47,35 +58,85 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
ngOnDestroy() { ngOnDestroy() {
} }
videoLoaded(evt, item) {
const video = evt.target;
if (video.src.startsWith('blob:') && video.videoWidth > 1280 && video.videoHeight > 720) {
this.tooBigError = this.nzMessageService.error('视频分辨率过大,请调整分辨率后上传,建议 1280x720', {nzDuration: 10 * 1000});
// video.src = null;
return ;
} else {
init() { if (this.tooBigError && this.tooBigError.messageId) {
this.nzMessageService.remove(this.tooBigError.messageId);
this.tooBigError = null;
}
}
if (video.src && video.src.startsWith('blob:')) {
return;
}
if ( item.contentObj.origin_video_url && item.contentObj.origin_video_url.startsWith('http') ) {
item.video_preview_thumb = null;
return;
}
// item.video_preview_thumb = null;
this.newVideoCover = true;
video.currentTime = .1;
}
videoError(evt) {
this.nzMessageService.error('video error');
}
videoSeeked(evt) {
const video = evt.target;
if (video.src && video.src.startsWith('blob:')) {
return;
}
if (!this.item.contentObj.pic_id) {
this.setVideoCover();
this.newVideoCover = false;
}
} }
setVideoCover() {
if (!this.videoNode) {
return;
}
const video = this.videoNode.nativeElement;
if (!video.src) { return; }
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth ;
canvas.height = video.videoHeight ;
canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
/** const video_preview_thumb = canvas.toDataURL();
* 储存图片数据
* @param e
*/
onImageUploadSuccess(e, key) {
this.item[key] = e.url; canvas.remove();
this.save();
}
/** const img = this.convertImageToBlob(video_preview_thumb);
* 储存音频数据 const formData = new FormData();
* @param e formData.append('file', img);
*/
onAudioUploadSuccess(e, key) {
this.item[key] = e.url;
this.save();
} }
convertImageToBlob(data) {
const imgType = data.substring(data.indexOf(':') + 1, data.indexOf(';'));
const binary = atob(data.split(',')[1]);
const array = [];
let i = 0;
while (i < binary.length) {
array.push (binary.charCodeAt(i));
i++;
}
return new Blob([ new Uint8Array(array) ], {type: imgType});
}
onVideoUploadSuccess(e) {
this.item.contentObj.video_url = e.url;
this.save();
}
/** /**
* 储存数据 * 储存数据
*/ */
......
import {Pipe, PipeTransform} from '@angular/core'; import {Pipe, PipeTransform} from '@angular/core';
// import {ElectronUtil} from '../util/ElectronUtil';
declare const APP_PATH;
// const isInElectron = localStorage.getItem('electron');
@Pipe({ @Pipe({
name: 'backgroundImage' name: 'backgroundImage'
}) })
export class BackgroundImagePipe implements PipeTransform { export class BackgroundImagePipe implements PipeTransform {
transform(value: any, coursewareSid?: string) { transform(value: any) {
// const ret = value ? `url(${value})` : ''; return `url(${value})`;
const ret = value ? `url(${value})` : '';
return ret;
// let ret = value > 0 ? `url(/api/media-resource/${value}/file)` : '';
// if (isInElectron && ElectronUtil.hasCached(coursewareSid, value)) {
// ret = `url(file://${APP_PATH}/courseware_cache/${coursewareSid}/${value})`;
// }
// return ret;
} }
} }
import {Pipe, PipeTransform} from '@angular/core';
@Pipe({
name: 'brLineBreaker'
})
export class BrLineBreakerPipe implements PipeTransform {
transform(value: any, args?: any): any {
return value ? value.replace(/\n/g, '<br />') : '';
}
}
...@@ -2,25 +2,17 @@ import {Pipe, PipeTransform} from '@angular/core'; ...@@ -2,25 +2,17 @@ import {Pipe, PipeTransform} from '@angular/core';
// import {ElectronUtil} from '../util/ElectronUtil'; // import {ElectronUtil} from '../util/ElectronUtil';
declare const APP_PATH; declare const APP_PATH;
// const isInElectron = localStorage.getItem('electron'); const isInElectron = localStorage.getItem('electron');
@Pipe({ @Pipe({
name: 'resource' name: 'resource'
}) })
export class ResourcePipe implements PipeTransform { export class ResourcePipe implements PipeTransform {
transform(pic_url: any, coursewareSid?: string): string { transform(value: any, coursewareSid?: string): string {
// return pic_url; return value ? `/api/media-resource/${value}/file` : '';
if (pic_url && typeof pic_url === 'object') { // if (isInElectron && ElectronUtil.hasCached(coursewareSid, value)) {
return pic_url; // ret = `file://${APP_PATH}/courseware_cache/${coursewareSid}/${value}`;
}
// console.log('resource', pic_url)
return `${pic_url}?t=${Math.random()}`;
// let ret = value ? `/api/resource/${value}/file` : '';
// let ret = res_id ? `/resource/audio/${res_id}` : '';
// if (isInElectron && ElectronUtil.hasCached(coursewareSid, res_id)) {
// ret = `file://${APP_PATH}/courseware_cache/${coursewareSid}/${res_id}`;
// } // }
// return ret; // return ret;
} }
......
.game-container {
width: 100%;
height: 100%;
background: #ffffff;
background-size: cover;
}
#canvas {
}
@font-face
{
font-family: 'BRLNSDB';
src: url("../../assets/font/BRLNSDB.TTF") ;
}
<div class="game-container" #wrap> <div
<canvas id="canvas" #canvas></canvas> class="p-courseware-video"
[style.background-image]="hasVideo ? '' : 'url(assets/player/video-not-upload.jpg)'"
>
<video *ngIf="hasVideo"
[src]="data.contentObj.video_url"
width="100%" height="100%"
#videoNode (click)="playVideo()">
</video>
<div class="video-cover" *ngIf="initCover && !isPlaying">
<img *ngIf="initCover" [src]="initCover"
style="width:100%;height:100%;"/>
</div>
<div class="k-play-btn" *ngIf="!isPlayStarted && hasVideo" (click)="playVideo()">
<i class="anticon anticon-play-circle-o"></i>
</div>
<div class="k-video-control" *ngIf="hasVideo">
<div class="k-video-control-bg"></div>
<div class="k-video-control-time-past">{{currentTime | time}}</div>
<div #videoProgress class="k-video-control-progress"
(mousedown)="onProgressMouseDown($event)"
(mousemove)="onProgressMouseMove($event)"
(mouseup)="onProgressMouseUp($event)">
<div class="i-bg"></div>
<div class="i-before-progress" [style.width]="progress + '%'"></div>
<div class="i-cursor" [style.left]="progress + '%'"></div>
</div>
<div class="k-video-control-time-total">{{duration | time}}</div>
<div class="k-video-control-tool">
<div class="i-tool-btn" (click)="onBtnQuickSeek(-10)">-10</div>
<div class="i-tool-btn" (click)="onBtnQuickSeek(-5)">-5</div>
<div class="i-tool-btn" (click)="onBtnQuickSeek(5)">+5</div>
<div class="i-tool-btn" (click)="onBtnQuickSeek(10)">+10</div>
</div>
</div>
</div> </div>
@import '../style/common_mixin.scss';
.p-courseware-video {
height: 100%;
overflow: hidden;
-webkit-transform-style: preserve-3d;
-moz-transform-style: preserve-3d;
transform-style: preserve-3d;
background-color: #333;
@include k-img-bg();
.video-cover{
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
}
video {
width: 100%;
height: 93%;
}
}
.k-play-btn {
position: absolute;
width: 15%;
top: 30%;
left: 40%;
font-size: 12vh;
color: #444;
background-color: white;
border-radius: 50%;
opacity: 0.9;
}
.k-video-control {
position: absolute;
bottom: 1vh;
left: 0;
width: 100%;
height: 7.5%;
display: flex;
color: white;
padding: 0 8vh;
align-items: center;
&-bg {
position: absolute;
//width: 100%;
height: 100%;
background-color: #333;
opacity: 0.5;
}
&-time-past {
width: 3.5rem;
}
&-progress {
position: relative;
flex: 1;
height: 100%;
display: flex;
align-items: center;
margin: 0 3%;
& > * {
pointer-events: none; // Ken video seek
}
.i-bg {
position: absolute;
background-color: #aaa;
width: 100%;
height: 10%;
border-radius: 1rem;
}
.i-before-progress {
position: absolute;
height: 10%;
background-color: white;
border-radius: 1rem;
}
.i-cursor {
position: absolute;
width: 1.5vh;
height: 1.5vh;
border-radius: 50%;
background-color: white;
}
}
&-time-total {
margin-right: 3vh;
}
&-tool {
display: flex;
.i-tool-btn {
width: 4vh;
height: 4vh;
background-color: white;
border-radius: 50%;
margin: 0 1vh;
color: black;
display: flex;
justify-content: center;
align-items: center;
font-size: 1.8vh;
font-weight: bold;
}
}
}
import {Component, ElementRef, ViewChild, OnInit, Input, OnDestroy, HostListener} from '@angular/core'; import { Component, ElementRef, ViewChild, OnInit, Input, OnDestroy, HostListener, OnChanges, AfterViewInit } from '@angular/core';
import * as _ from 'lodash';
import {
Label,
MySprite, tweenChange,
} from './Unit';
import {res, resAudio} from './resources';
import {Subject} from 'rxjs';
import {debounceTime} from 'rxjs/operators';
import TWEEN from '@tweenjs/tween.js';
...@@ -18,662 +7,125 @@ import TWEEN from '@tweenjs/tween.js'; ...@@ -18,662 +7,125 @@ import TWEEN from '@tweenjs/tween.js';
@Component({ @Component({
selector: 'app-play', selector: 'app-play',
templateUrl: './play.component.html', templateUrl: './play.component.html',
styleUrls: ['./play.component.css'] styleUrls: ['./play.component.scss'],
}) })
export class PlayComponent implements OnInit, OnDestroy { export class PlayComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
@ViewChild('canvas', {static: true }) canvas: ElementRef;
@ViewChild('wrap', {static: true }) wrap: ElementRef;
// 数据 // 数据
data; data;
saveKey = 'ww_video';
ctx;
canvasWidth = 1280; // canvas实际宽度
canvasHeight = 720; // canvas实际高度
canvasBaseW = 1280; // canvas 资源预设宽度
canvasBaseH = 720; // canvas 资源预设高度
mx; // 点击x坐标
my; // 点击y坐标
// 资源
rawImages = new Map(res);
rawAudios = new Map(resAudio);
images = new Map();
animationId: any;
winResizeEventStream = new Subject();
audioObj = {};
renderArr;
mapScale = 1;
canvasLeft;
canvasTop;
saveKey = 'test_0011';
btnLeft; @ViewChild('videoNode')
btnRight; videoNodeRef: ElementRef;
pic1; @ViewChild('videoProgress')
pic2; videoProgressRef: ElementRef;
player: any;
canTouch = true; progress = 0;
currentTime = 0;
curPic; duration = 0;
isPlaying = false;
@HostListener('window:resize', ['$event']) hasVideo = false;
onResize(event) { touchSeeking = false;
this.winResizeEventStream.next(); initCover = false;
isPlayStarted = false;
constructor() {
} }
ngOnInit() { ngOnInit() {
this.data = {
this.data = {}; contentObj: {
video_url: null
}
};
// 获取数据 // 获取数据
const getData = (<any> window).courseware.getData; const getData = ( window as any).courseware.getData;
getData((data) => { getData((data) => {
if (data && typeof data === 'object') {
if (data && typeof data == 'object') { console.log(this.data)
this.data = data; this.data = data;
} }
// console.log('data:' , data); this.hasVideo = !!this.data.contentObj.video_url
window["air"].hideAirClassLoading(this.saveKey, this.data);
// 初始化 各事件监听
this.initListener();
// 若无数据 则为预览模式 需要填充一些默认数据用来显示
this.initDefaultData();
// 初始化 音频资源
this.initAudio();
// 初始化 图片资源
this.initImg();
// 开始预加载资源
this.load();
}, this.saveKey); }, this.saveKey);
} }
ngOnDestroy() { ngOnDestroy() {
window['curCtx'] = null;
window.cancelAnimationFrame(this.animationId);
} }
ngOnChanges(data) {
load() {
// 预加载资源
this.loadResources().then(() => {
window["air"].hideAirClassLoading(this.saveKey, this.data);
this.init();
this.update();
});
}
init() {
this.initCtx();
this.initData();
this.initView();
} }
initCtx() { ngAfterViewInit() {
this.canvasWidth = this.wrap.nativeElement.clientWidth; if (!this.videoNodeRef) {
this.canvasHeight = this.wrap.nativeElement.clientHeight;
this.canvas.nativeElement.width = this.wrap.nativeElement.clientWidth;
this.canvas.nativeElement.height = this.wrap.nativeElement.clientHeight;
this.ctx = this.canvas.nativeElement.getContext('2d');
this.canvas.nativeElement.width = this.canvasWidth;
this.canvas.nativeElement.height = this.canvasHeight;
window['curCtx'] = this.ctx;
}
updateItem(item) {
if (item) {
item.update();
}
}
updateArr(arr) {
if (!arr) {
return; return;
} }
for (let i = 0; i < arr.length; i++) { this.player = this.videoNodeRef.nativeElement;
arr[i].update(this); this.player.ontimeupdate = (event) => {
} this.onTimeUpdate(event);
}
initListener() {
this.winResizeEventStream
.pipe(debounceTime(500))
.subscribe(data => {
this.renderAfterResize();
});
// ---------------------------------------------
const setParentOffset = () => {
const rect = this.canvas.nativeElement.getBoundingClientRect();
this.canvasLeft = rect.left;
this.canvasTop = rect.top;
};
const setMxMyByTouch = (event) => {
if (event.touches.length <= 0) {
return;
}
if (this.canvasLeft == null) {
setParentOffset();
}
this.mx = event.touches[0].pageX - this.canvasLeft;
this.my = event.touches[0].pageY - this.canvasTop;
}; };
this.player.onloadedmetadata = () => {
const setMxMyByMouse = (event) => { this.currentTime = 0;
this.mx = event.offsetX; this.duration = this.player.duration;
this.my = event.offsetY;
}; };
// --------------------------------------------- this.player.onseeking = () => {
this.player.pause();
let firstTouch = true;
const touchDownFunc = (e) => {
if (firstTouch) {
firstTouch = false;
removeMouseListener();
}
setMxMyByTouch(e);
this.mapDown(e);
}; };
const touchMoveFunc = (e) => { this.player.onseeked = () => {
setMxMyByTouch(e); this.player.play();
this.mapMove(e); this.touchSeeking = false;
};
const touchUpFunc = (e) => {
setMxMyByTouch(e);
this.mapUp(e);
};
const mouseDownFunc = (e) => {
if (firstTouch) {
firstTouch = false;
removeTouchListener();
}
setMxMyByMouse(e);
this.mapDown(e);
}; };
const mouseMoveFunc = (e) => { this.player.onplay = () => {
setMxMyByMouse(e); this.isPlaying = true;
this.mapMove(e);
};
const mouseUpFunc = (e) => {
setMxMyByMouse(e);
this.mapUp(e);
};
const element = this.canvas.nativeElement;
const addTouchListener = () => {
element.addEventListener('touchstart', touchDownFunc);
element.addEventListener('touchmove', touchMoveFunc);
element.addEventListener('touchend', touchUpFunc);
element.addEventListener('touchcancel', touchUpFunc);
};
const removeTouchListener = () => {
element.removeEventListener('touchstart', touchDownFunc);
element.removeEventListener('touchmove', touchMoveFunc);
element.removeEventListener('touchend', touchUpFunc);
element.removeEventListener('touchcancel', touchUpFunc);
};
const addMouseListener = () => {
element.addEventListener('mousedown', mouseDownFunc);
element.addEventListener('mousemove', mouseMoveFunc);
element.addEventListener('mouseup', mouseUpFunc);
}; };
const removeMouseListener = () => { this.player.onpause = () => {
element.removeEventListener('mousedown', mouseDownFunc); this.isPlaying = false;
element.removeEventListener('mousemove', mouseMoveFunc);
element.removeEventListener('mouseup', mouseUpFunc);
}; };
setTimeout(() => {
addMouseListener(); this.initCover = this.data.contentObj.pic_id;
addTouchListener();
}
playAudio(key, now = false, callback = null) {
const audio = this.audioObj[key];
if (audio) {
if (now) {
audio.pause();
audio.currentTime = 0;
}
if (callback) {
audio.onended = () => {
callback();
};
}
audio.play();
}
}
loadResources() {
const pr = [];
this.rawImages.forEach((value, key) => {// 预加载图片
const p = this.preload(value)
.then(img => {
this.images.set(key, img);
})
.catch(err => console.log(err));
pr.push(p);
}); });
this.rawAudios.forEach((value, key) => {// 预加载音频
const a = this.preloadAudio(value)
.then(() => {
// this.images.set(key, img);
})
.catch(err => console.log(err));
pr.push(a);
});
return Promise.all(pr);
}
preload(url) {
return new Promise((resolve, reject) => {
const img = new Image();
// img.crossOrigin = "anonymous";
img.onload = () => resolve(img);
img.onerror = reject;
img.src = url;
});
}
preloadAudio(url) {
return new Promise((resolve, reject) => {
const audio = new Audio();
audio.oncanplay = (a) => {
resolve();
};
audio.onerror = () => {
reject();
};
audio.src = url;
audio.load();
});
}
renderAfterResize() {
this.canvasWidth = this.wrap.nativeElement.clientWidth;
this.canvasHeight = this.wrap.nativeElement.clientHeight;
this.init();
}
checkClickTarget(target) {
const rect = target.getBoundingBox();
if (this.checkPointInRect(this.mx, this.my, rect)) {
return true;
}
return false;
}
getWorlRect(target) {
let rect = target.getBoundingBox();
if (target.parent) {
const pRect = this.getWorlRect(target.parent);
rect.x += pRect.x;
rect.y += pRect.y;
}
return rect;
}
checkPointInRect(x, y, rect) {
if (x >= rect.x && x <= rect.x + rect.width) {
if (y >= rect.y && y <= rect.y + rect.height) {
return true;
}
}
return false;
}
addUrlToAudioObj(key, url = null, vlomue = 1, loop = false, callback = null) {
const audioObj = this.audioObj;
if (url == null) {
url = key;
}
this.rawAudios.set(key, url);
const audio = new Audio();
audio.src = url;
audio.load();
audio.loop = loop;
audio.volume = vlomue;
audioObj[key] = audio;
if (callback) {
audio.onended = () => {
callback();
};
}
}
addUrlToImages(url) {
this.rawImages.set(url, url);
}
// ======================================================编写区域==========================================================================
/**
* 添加默认数据 便于无数据时的展示
*/
initDefaultData() {
if (!this.data.pic_url) {
this.data.pic_url = 'assets/play/default/pic.jpg';
this.data.pic_url_2 = 'assets/play/default/pic.jpg';
}
}
/**
* 添加预加载图片
*/
initImg() {
this.addUrlToImages(this.data.pic_url);
this.addUrlToImages(this.data.pic_url_2);
}
/**
* 添加预加载音频
*/
initAudio() {
// 音频资源
this.addUrlToAudioObj(this.data.audio_url);
this.addUrlToAudioObj(this.data.audio_url_2);
// 音效
this.addUrlToAudioObj('click', this.rawAudios.get('click'), 0.3);
}
/**
* 初始化数据
*/
initData() {
const sx = this.canvasWidth / this.canvasBaseW;
const sy = this.canvasHeight / this.canvasBaseH;
const s = Math.min(sx, sy);
this.mapScale = s;
// this.mapScale = sx;
// this.mapScale = sy;
this.renderArr = [];
}
/**
* 初始化试图
*/
initView() {
this.initPic();
this.initBottomPart();
}
initBottomPart() {
const btnLeft = new MySprite();
btnLeft.init(this.images.get('btn_left'));
btnLeft.x = this.canvasWidth - 150 * this.mapScale;
btnLeft.y = this.canvasHeight - 100 * this.mapScale;
btnLeft.setScaleXY(this.mapScale);
this.renderArr.push(btnLeft);
this.btnLeft = btnLeft;
const btnRight = new MySprite();
btnRight.init(this.images.get('btn_right'));
btnRight.x = this.canvasWidth - 50 * this.mapScale;
btnRight.y = this.canvasHeight - 100 * this.mapScale;
btnRight.setScaleXY(this.mapScale);
this.renderArr.push(btnRight);
this.btnRight = btnRight;
} }
initPic() { playVideo() {
if (!this.videoNodeRef) {
const maxW = this.canvasWidth * 0.7;
const pic1 = new MySprite();
pic1.init(this.images.get(this.data.pic_url));
pic1.x = this.canvasWidth / 2;
pic1.y = this.canvasHeight / 2;
pic1.setScaleXY(maxW / pic1.width);
this.renderArr.push(pic1);
this.pic1 = pic1;
const label1 = new Label();
label1.text = this.data.text;
label1.textAlign = 'center';
label1.fontSize = 50;
label1.fontName = 'BRLNSDB';
label1.fontColor = '#ffffff';
pic1.addChild(label1);
const pic2 = new MySprite();
pic2.init(this.images.get(this.data.pic_url_2));
pic2.x = this.canvasWidth / 2 + this.canvasWidth;
pic2.y = this.canvasHeight / 2;
pic2.setScaleXY(maxW / pic2.width);
this.renderArr.push(pic2);
this.pic2 = pic2;
this.curPic = pic1;
}
btnLeftClicked() {
this.lastPage();
}
btnRightClicked() {
this.nextPage();
}
lastPage() {
if (this.curPic == this.pic1) {
return; return;
} }
this.initCover = false;
this.canTouch = false; if (this.player.paused) {
this.player.play();
const moveLen = this.canvasWidth; this.isPlayStarted = true;
tweenChange(this.pic1, {x: this.pic1.x + moveLen}, 1); } else {
tweenChange(this.pic2, {x: this.pic2.x + moveLen}, 1, () => { this.player.pause();
this.canTouch = true;
this.curPic = this.pic1;
});
}
nextPage() {
if (this.curPic == this.pic2) {
return;
} }
this.canTouch = false;
const moveLen = this.canvasWidth;
tweenChange(this.pic1, {x: this.pic1.x - moveLen}, 1);
tweenChange(this.pic2, {x: this.pic2.x - moveLen}, 1, () => {
this.canTouch = true;
this.curPic = this.pic2;
});
} }
pic1Clicked() { onTimeUpdate(event) {
this.playAudio(this.data.audio_url); const player = event.target;
this.currentTime = player.currentTime;
this.progress = Math.floor((player.currentTime / player.duration) * 100);
} }
pic2Clicked() { onBtnQuickSeek(delta: number) {
this.playAudio(this.data.audio_url_2); this.player.currentTime += delta;
} }
onProgressMouseDown(event) {
this.player.pause();
this.touchSeeking = true;
mapDown(event) {
if (!this.canTouch) {
return;
}
if ( this.checkClickTarget(this.btnLeft) ) {
this.btnLeftClicked();
return;
}
if ( this.checkClickTarget(this.btnRight) ) {
this.btnRightClicked();
return;
}
if ( this.checkClickTarget(this.pic1) ) {
this.pic1Clicked();
return;
}
if ( this.checkClickTarget(this.pic2) ) {
this.pic2Clicked();
return;
}
}
mapMove(event) {
} }
mapUp(event) { onProgressMouseMove(event) {
if (this.touchSeeking) {
const {target, offsetX} = event;
this.progress = Math.floor(offsetX / target.clientWidth * 100);
}
} }
onProgressMouseUp(event) {
const {target, offsetX} = event;
update() { this.player.currentTime = this.player.duration * offsetX / target.clientWidth;
// ----------------------------------------------------------
this.animationId = window.requestAnimationFrame(this.update.bind(this));
// 清除画布内容
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
// tween 更新动画
TWEEN.update();
// ----------------------------------------------------------
this.updateArr(this.renderArr);
} }
} }
@mixin hide-overflow-text {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
@mixin k-no-select {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
@mixin k-img-bg {
background-repeat: no-repeat;
background-position: center;
background-size: contain;
}
@mixin hide-overflow-text {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
@mixin k-no-select {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
@mixin k-img-bg {
background-repeat: no-repeat;
background-position: center;
background-size: contain;
}
...@@ -3,6 +3,8 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; ...@@ -3,6 +3,8 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module'; import { AppModule } from './app/app.module';
import { environment } from './environments/environment'; import { environment } from './environments/environment';
import '../node_modules/bootstrap/dist/css/bootstrap.min.css';
if (environment.production) { if (environment.production) {
enableProdMode(); enableProdMode();
......
/* You can add global styles to this file, and also import other style files */
/* You can add global styles to this file, and also import other style files */
@mixin hide-overflow-text {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
@mixin k-no-select {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
@mixin k-img-bg {
background-repeat: no-repeat;
background-position: center;
background-size: contain;
}
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment