Started implementing video support.
This commit is contained in:
8
package-lock.json
generated
8
package-lock.json
generated
@@ -15,7 +15,7 @@
|
|||||||
"@angular/forms": "^21.2.0",
|
"@angular/forms": "^21.2.0",
|
||||||
"@angular/platform-browser": "^21.2.0",
|
"@angular/platform-browser": "^21.2.0",
|
||||||
"@angular/router": "^21.2.0",
|
"@angular/router": "^21.2.0",
|
||||||
"@chatenium/chatenium-sdk": "^1.0.11",
|
"@chatenium/chatenium-sdk": "^1.1.2",
|
||||||
"@ngx-translate/core": "^17.0.0",
|
"@ngx-translate/core": "^17.0.0",
|
||||||
"@ngx-translate/http-loader": "^17.0.0",
|
"@ngx-translate/http-loader": "^17.0.0",
|
||||||
"@taiga-ui/addon-charts": "^5.1.0",
|
"@taiga-ui/addon-charts": "^5.1.0",
|
||||||
@@ -988,9 +988,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@chatenium/chatenium-sdk": {
|
"node_modules/@chatenium/chatenium-sdk": {
|
||||||
"version": "1.0.11",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@chatenium/chatenium-sdk/-/chatenium-sdk-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@chatenium/chatenium-sdk/-/chatenium-sdk-1.1.2.tgz",
|
||||||
"integrity": "sha512-2vkN+W541bMEdWTStrXorsEmbQ9mva6drKOU11yFdVMzpPEsMJr9bvk5Lwc5COpOgNTIbnFSAkzPg+0USFruVQ==",
|
"integrity": "sha512-MYUdi1zxcsSUlf1JADU7HNU6zxPejNuspbt+9P3iUBI2ecHWzhqSdcQRR+OMEe0UThl7QNIlZrt0yl15/4fjYQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@faker-js/faker": "^10.4.0",
|
"@faker-js/faker": "^10.4.0",
|
||||||
"axios": "^1.14.0",
|
"axios": "^1.14.0",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"@angular/forms": "^21.2.0",
|
"@angular/forms": "^21.2.0",
|
||||||
"@angular/platform-browser": "^21.2.0",
|
"@angular/platform-browser": "^21.2.0",
|
||||||
"@angular/router": "^21.2.0",
|
"@angular/router": "^21.2.0",
|
||||||
"@chatenium/chatenium-sdk": "^1.0.11",
|
"@chatenium/chatenium-sdk": "^1.1.2",
|
||||||
"@ngx-translate/core": "^17.0.0",
|
"@ngx-translate/core": "^17.0.0",
|
||||||
"@ngx-translate/http-loader": "^17.0.0",
|
"@ngx-translate/http-loader": "^17.0.0",
|
||||||
"@taiga-ui/addon-charts": "^5.1.0",
|
"@taiga-ui/addon-charts": "^5.1.0",
|
||||||
|
|||||||
@@ -49,9 +49,10 @@ export class Dm implements OnInit {
|
|||||||
fileId: file.fileId,
|
fileId: file.fileId,
|
||||||
type: file.type,
|
type: file.type,
|
||||||
format: file.extension,
|
format: file.extension,
|
||||||
path: file.preview,
|
path: file.blob,
|
||||||
height: file.height,
|
height: file.height,
|
||||||
width: file.width,
|
width: file.width,
|
||||||
|
localVideoThumbnail: file.videoThumbnail
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -82,46 +83,58 @@ export class Dm implements OnInit {
|
|||||||
console.log(fileId, allChunks, chunksDone)
|
console.log(fileId, allChunks, chunksDone)
|
||||||
}
|
}
|
||||||
|
|
||||||
onWsListen(action: string, message: string) {
|
// The chatid parameter ensures isolation
|
||||||
console.log(action, message)
|
onWsListen(action: string, message: string, chatid: string) {
|
||||||
switch (action) {
|
const data = JSON.parse(message);
|
||||||
case "newMessage": {
|
if (data.chatid === chatid) {
|
||||||
this.store.messages.update(messages => [...messages, JSON.parse(message)])
|
const targetStore = this.serviceManager.dmServices()[chatid];
|
||||||
|
if (targetStore) {
|
||||||
|
switch (action) {
|
||||||
|
case "newMessage":
|
||||||
|
targetStore.messages.update(messages => [...messages, data]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.route.params.subscribe(async params => {
|
this.route.params.subscribe(async params => {
|
||||||
const chatid = params['chatid']
|
const chatid = params['chatid'];
|
||||||
this.chatid = chatid
|
this.chatid = chatid;
|
||||||
console.log(this.serviceManager.chats())
|
|
||||||
const session = this.serviceManager.currentSession();
|
|
||||||
const chatData = this.serviceManager.chats().find(chat => chat.chatid == chatid)
|
|
||||||
|
|
||||||
// Setup storage
|
const session = this.serviceManager.currentSession();
|
||||||
if (!this.serviceManager.dmServices()[chatid] && session != null && chatData != null) {
|
const chatData = this.serviceManager.chats().find(c => c.chatid === chatid);
|
||||||
this.serviceManager.dmServices()[chatid] = {
|
|
||||||
service: new DMService(
|
if (!session || !chatData) {
|
||||||
session.userData.userid,
|
console.warn(`Initialization deferred for ${chatid}: Session or ChatData missing.`);
|
||||||
session.token,
|
return;
|
||||||
chatid,
|
|
||||||
this.indexedDb.getApi(),
|
|
||||||
(action, data) => {
|
|
||||||
this.onWsListen(action, data)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
chatData: signal<Chat>(chatData),
|
|
||||||
messages: signal<Message[]>([]),
|
|
||||||
messageBox: new MessageBoxViewModel(
|
|
||||||
(msg, files) => this.sendMessage(msg, files),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.store.messages.set(await this.serviceManager.dmServices()[chatid].service.get())
|
if (!this.serviceManager.dmServices()[chatid]) {
|
||||||
console.log(WebSocketHandler.getInstance().connId)
|
const newStore = {
|
||||||
await this.store.service.joinWebSocketRoom()
|
chatData: signal<Chat>(chatData),
|
||||||
})
|
messages: signal<Message[]>([]),
|
||||||
|
messageBox: new MessageBoxViewModel((msg, files) => this.sendMessage(msg, files)),
|
||||||
|
wsListener: (action, data) => this.onWsListen(action, data, chatid),
|
||||||
|
} as DmStorage;
|
||||||
|
|
||||||
|
newStore.service = new DMService(
|
||||||
|
session.userData.userid,
|
||||||
|
session.token,
|
||||||
|
chatid,
|
||||||
|
this.indexedDb.getApi(),
|
||||||
|
(action, data) => newStore.wsListener(action, data)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.serviceManager.dmServices()[chatid] = newStore;
|
||||||
|
|
||||||
|
const currentStore = this.serviceManager.dmServices()[chatid];
|
||||||
|
const history = await currentStore.service.get();
|
||||||
|
currentStore.messages.set(history);
|
||||||
|
|
||||||
|
await currentStore.service.joinWebSocketRoom();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<masonry [maxColSize]="2" style="overflow-y: scroll; height: 500px">
|
<masonry [maxColSize]="2" style="overflow-y: scroll; height: 500px">
|
||||||
@for (file of viewModel().files(); track file) {
|
@for (file of viewModel().files(); track file) {
|
||||||
@if (file.type == "image") {
|
@if (file.type == "image") {
|
||||||
<img [src]="file.preview" style="width: 100%; height: 100%; object-fit: fill;"/>
|
<img [src]="file.blob" style="width: 100%; height: 100%; object-fit: fill;"/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</masonry>
|
</masonry>
|
||||||
|
|||||||
@@ -107,22 +107,24 @@ export class MessageBox {
|
|||||||
for (let i = 0; i < fileList.length; i++) {
|
for (let i = 0; i < fileList.length; i++) {
|
||||||
const file = fileList[i];
|
const file = fileList[i];
|
||||||
const type = file.type.split("/").shift() ?? ""
|
const type = file.type.split("/").shift() ?? ""
|
||||||
let preview = ""
|
let blob = ""
|
||||||
|
let thumbnailBlob = undefined
|
||||||
let height = 0
|
let height = 0
|
||||||
let width = 0
|
let width = 0
|
||||||
|
|
||||||
console.log(type)
|
console.log(type)
|
||||||
|
|
||||||
|
blob = URL.createObjectURL(file)
|
||||||
if (type == "image") {
|
if (type == "image") {
|
||||||
try {
|
const imgData = await fetchImageDimensions(file)
|
||||||
const imgData = await makePicturePreview(file)
|
console.log(imgData)
|
||||||
console.log(imgData)
|
height = imgData.height
|
||||||
preview = imgData.preview
|
width = imgData.width
|
||||||
height = imgData.height
|
} else if (type == "video") {
|
||||||
width = imgData.width
|
const videoData = await processVideo(file)
|
||||||
} catch (error) {
|
thumbnailBlob = videoData.thumbnailBlob
|
||||||
console.error(error)
|
height = videoData.height
|
||||||
}
|
width = videoData.width
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("push")
|
console.log("push")
|
||||||
@@ -132,14 +134,15 @@ export class MessageBox {
|
|||||||
name: file.name,
|
name: file.name,
|
||||||
type: type,
|
type: type,
|
||||||
extension: file.name.split(".").pop() ?? "",
|
extension: file.name.split(".").pop() ?? "",
|
||||||
preview: preview,
|
blob: blob,
|
||||||
height: height,
|
height: height,
|
||||||
width: width,
|
width: width,
|
||||||
|
videoThumbnail: thumbnailBlob,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.viewModel().dialogOpen.set(true)
|
this.viewModel().dialogOpen.set(true)
|
||||||
|
|
||||||
function makePicturePreview(file: File): Promise<{ preview: string, height: number, width: number }> {
|
function fetchImageDimensions(file: File): Promise<{ height: number, width: number }> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
const objectUrl = URL.createObjectURL(file);
|
const objectUrl = URL.createObjectURL(file);
|
||||||
@@ -147,7 +150,6 @@ export class MessageBox {
|
|||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
console.log("Loaded image")
|
console.log("Loaded image")
|
||||||
resolve({
|
resolve({
|
||||||
preview: objectUrl,
|
|
||||||
width: img.naturalWidth,
|
width: img.naturalWidth,
|
||||||
height: img.naturalHeight,
|
height: img.naturalHeight,
|
||||||
});
|
});
|
||||||
@@ -162,6 +164,37 @@ export class MessageBox {
|
|||||||
img.src = objectUrl;
|
img.src = objectUrl;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function processVideo(file: File): Promise<{ thumbnailBlob: string, height: number, width: number }> {
|
||||||
|
const objectUrl = URL.createObjectURL(file);
|
||||||
|
const video = document.createElement('video');
|
||||||
|
video.src = objectUrl;
|
||||||
|
video.crossOrigin = 'anonymous';
|
||||||
|
video.muted = true;
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
video.addEventListener('loadeddata', async () => {
|
||||||
|
video.currentTime = 0;
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = video.videoWidth;
|
||||||
|
canvas.height = video.videoHeight;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx!.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||||
|
const thumbnail = canvas.toDataURL('image/jpeg')
|
||||||
|
const thumbnailResp = await fetch(thumbnail);
|
||||||
|
const thumbnailBlob = await thumbnailResp.blob();
|
||||||
|
const thumbnailBlobUrl = URL.createObjectURL(thumbnailBlob);
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
thumbnailBlob: thumbnailBlobUrl,
|
||||||
|
height: video.videoHeight,
|
||||||
|
width: video.videoWidth,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readonly console = console;
|
protected readonly console = console;
|
||||||
@@ -171,7 +204,8 @@ export class MessageBox {
|
|||||||
* All the extra data for client-side rendering. (With finished messages these are generated on the server)
|
* All the extra data for client-side rendering. (With finished messages these are generated on the server)
|
||||||
*/
|
*/
|
||||||
export interface FileDataWithPreview extends FileData {
|
export interface FileDataWithPreview extends FileData {
|
||||||
preview: string;
|
blob: string;
|
||||||
|
videoThumbnail?: string;
|
||||||
height: number;
|
height: number;
|
||||||
width: number;
|
width: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
@for (file of message.files; track file) {
|
@for (file of message.files; track file) {
|
||||||
@if (file.type == "image") {
|
@if (file.type == "image") {
|
||||||
<img [src]="file.path" style="width: 100%; height: 100%; max-height: 300px; object-fit: cover; border-radius: 25px"/>
|
<img [src]="file.path" style="width: 100%; height: 100%; max-height: 300px; object-fit: cover; border-radius: 25px"/>
|
||||||
|
} @else if (file.type == "video") {
|
||||||
|
<video-player [src]="file.path"></video-player>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</masonry>
|
</masonry>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
|
|
||||||
.bubble {
|
.bubble {
|
||||||
background: var(--tui-background-neutral-2);
|
background: var(--tui-background-neutral-2);
|
||||||
max-width: 350px;
|
max-width: 50%;
|
||||||
min-width: 250px;
|
min-width: 250px;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import {Message as NetworkMessage} from '@chatenium/chatenium-sdk/domain/textCha
|
|||||||
import {ServiceManager} from '../../../service-manager';
|
import {ServiceManager} from '../../../service-manager';
|
||||||
import {DatePipe} from '@angular/common';
|
import {DatePipe} from '@angular/common';
|
||||||
import {Masonry} from '../masonry/masonry';
|
import {Masonry} from '../masonry/masonry';
|
||||||
|
import {VideoPlayer} from '../video-player/video-player';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'messages',
|
selector: 'messages',
|
||||||
imports: [
|
imports: [
|
||||||
DatePipe,
|
DatePipe,
|
||||||
Masonry
|
Masonry,
|
||||||
|
VideoPlayer
|
||||||
],
|
],
|
||||||
templateUrl: './messages.html',
|
templateUrl: './messages.html',
|
||||||
styleUrl: './messages.scss',
|
styleUrl: './messages.scss',
|
||||||
|
|||||||
88
src/app/chat/elements/video-player/video-player.html
Normal file
88
src/app/chat/elements/video-player/video-player.html
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
@if (playerActive) {
|
||||||
|
<div id="player" [style]="'max-width:'+maxWidth+';max-height:'+maxHeight" (mouseover)="controlShowed = true"
|
||||||
|
(mouseleave)="controlShowed = false" #player>
|
||||||
|
<video (mouseout)="showVolRange = false" [class.upScale]="videoFullscreen"
|
||||||
|
[style]="'max-width:'+maxWidth+';height:'+maxHeight" (pause)="videoPlaying = false"
|
||||||
|
(play)="videoPlaying = true" #video (timeupdate)="watched = videoHMSFormat(video.currentTime)"
|
||||||
|
(loadedmetadata)="videoPlayer = video; video.style.display = 'block'; videoLoaded = true"
|
||||||
|
(click)="videoPlaying ? video.pause() : video.play(); videoPlaying = !videoPlaying"
|
||||||
|
(dblclick)="videoFullscreen ? exitFullScreen() : player.requestFullscreen(); videoFullscreen = !videoFullscreen"
|
||||||
|
[src]="src"></video>
|
||||||
|
|
||||||
|
<button tuiButton appearance="icon" class="picInPic" (click)="video.requestPictureInPicture();">
|
||||||
|
<tui-icon icon="@tui.picture-in-picture"></tui-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="controlsHolder">
|
||||||
|
<div id="controls">
|
||||||
|
<button tuiButton appearance="icon"
|
||||||
|
(click)="videoPlaying ? video.pause() : video.play();">
|
||||||
|
@if (videoPlaying) {
|
||||||
|
<tui-icon icon="@tui.pause"></tui-icon>
|
||||||
|
} @else {
|
||||||
|
<tui-icon icon="@tui.play"></tui-icon>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
<span>{{ watched }}</span>
|
||||||
|
<progress class="timeProgress" (click)="jump($event)" [max]="video.duration" [value]="video.currentTime"
|
||||||
|
tuiProgressBar size="xs"></progress>
|
||||||
|
<div>
|
||||||
|
<!-- @if (showVolRange) { -->
|
||||||
|
|
||||||
|
<!-- } -->
|
||||||
|
|
||||||
|
<div class="volumeSetter" (mouseover)="showVolRange = true">
|
||||||
|
<button
|
||||||
|
tuiButton appearance="icon"
|
||||||
|
tuiDropdown
|
||||||
|
tuiDropdownAppearance="neutral"
|
||||||
|
tuiDropdownAuto
|
||||||
|
tuiDropdownDirection="top"
|
||||||
|
tuiDropdownLimitWidth="fixed"
|
||||||
|
tuiIconButton
|
||||||
|
>
|
||||||
|
@if (video.volume == 0) {
|
||||||
|
<tui-icon icon="@tui.volume-off"></tui-icon>
|
||||||
|
} @else if (video.volume > .75) {
|
||||||
|
<tui-icon icon="@tui.volume-2"></tui-icon>
|
||||||
|
} @else if (video.volume >= .50 && video.volume < .74) {
|
||||||
|
<tui-icon icon="@tui.volume-1"></tui-icon>
|
||||||
|
} @else {
|
||||||
|
<tui-icon icon="@tui.volume"></tui-icon>
|
||||||
|
}
|
||||||
|
|
||||||
|
<input
|
||||||
|
*tuiDropdown
|
||||||
|
tuiSlider
|
||||||
|
type="range"
|
||||||
|
value="100"
|
||||||
|
(input)="setVolume(volume)"
|
||||||
|
[(ngModel)]="volume"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button tuiButton appearance="icon"
|
||||||
|
(click)="videoFullscreen ? exitFullScreen() : player.requestFullscreen(); videoFullscreen = !videoFullscreen">
|
||||||
|
@if (videoFullscreen) {
|
||||||
|
<tui-icon icon="@tui.minimize"></tui-icon>
|
||||||
|
} @else {
|
||||||
|
<tui-icon icon="@tui.maximize"></tui-icon>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
} @else {
|
||||||
|
<div class="player_preview">
|
||||||
|
@if (thumbnailOverwrite) {
|
||||||
|
<img [src]="thumbnailOverwrite" />
|
||||||
|
} @else {
|
||||||
|
<oimg [src]="src+'_thumbnail.png'"></oimg>
|
||||||
|
}
|
||||||
|
<button tuiButton (click)="playVideo()">
|
||||||
|
<tui-icon icon="@tui.play"></tui-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
110
src/app/chat/elements/video-player/video-player.scss
Normal file
110
src/app/chat/elements/video-player/video-player.scss
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
#player {
|
||||||
|
max-width: 400px;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
tui-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picInPic {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upScale {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#controlsHolder {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
#controls {
|
||||||
|
width: 95%;
|
||||||
|
height: 50px;
|
||||||
|
background: var(--tui-background-base-alt);
|
||||||
|
border: 2px solid var(--tui-border-normal);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
gap: 5px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 50px 50px 1fr 50px 50px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
button {
|
||||||
|
height: 25px;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
font-size: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volumeSetter {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.player_preview {
|
||||||
|
max-width: 400px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tui-dropdown[data-appearance] {
|
||||||
|
min-block-size: 8rem;
|
||||||
|
background: var(--tui-background-base);
|
||||||
|
box-shadow: none;
|
||||||
|
border: none;
|
||||||
|
backdrop-filter: blur(1rem);
|
||||||
|
|
||||||
|
[tuiSlider] {
|
||||||
|
position: absolute;
|
||||||
|
inline-size: 7rem;
|
||||||
|
transform-origin: left;
|
||||||
|
transform: rotate(-90deg) translate(-100%, 1rem) translateY(12px);
|
||||||
|
|
||||||
|
&::-webkit-slider-thumb {
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-range-thumb {
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/app/chat/elements/video-player/video-player.spec.ts
Normal file
22
src/app/chat/elements/video-player/video-player.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { VideoPlayer } from './video-player';
|
||||||
|
|
||||||
|
describe('VideoPlayer', () => {
|
||||||
|
let component: VideoPlayer;
|
||||||
|
let fixture: ComponentFixture<VideoPlayer>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [VideoPlayer],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(VideoPlayer);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
await fixture.whenStable();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
102
src/app/chat/elements/video-player/video-player.ts
Normal file
102
src/app/chat/elements/video-player/video-player.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import {Component, Input, ViewEncapsulation} from '@angular/core';
|
||||||
|
import {
|
||||||
|
TuiButton,
|
||||||
|
TuiDropdownDirective,
|
||||||
|
TuiDropdownOpen,
|
||||||
|
TuiDropdownOptionsDirective,
|
||||||
|
TuiIcon,
|
||||||
|
TuiSlider
|
||||||
|
} from '@taiga-ui/core';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {Oimg} from '../oimg/oimg';
|
||||||
|
import {TuiInputDateRange, TuiProgressBar} from '@taiga-ui/kit';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'video-player',
|
||||||
|
imports: [
|
||||||
|
TuiIcon,
|
||||||
|
FormsModule,
|
||||||
|
TuiButton,
|
||||||
|
Oimg,
|
||||||
|
TuiSlider,
|
||||||
|
TuiProgressBar,
|
||||||
|
TuiDropdownOptionsDirective,
|
||||||
|
TuiDropdownDirective,
|
||||||
|
TuiDropdownOpen,
|
||||||
|
TuiInputDateRange
|
||||||
|
],
|
||||||
|
templateUrl: './video-player.html',
|
||||||
|
styleUrl: './video-player.scss',
|
||||||
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
})
|
||||||
|
export class VideoPlayer {
|
||||||
|
@Input() src: string = "";
|
||||||
|
@Input() maxWidth: string = "";
|
||||||
|
@Input() maxHeight: string = "";
|
||||||
|
@Input() thumbnailOverwrite: string | undefined = undefined;
|
||||||
|
|
||||||
|
playerActive = false
|
||||||
|
|
||||||
|
videoLoaded = false
|
||||||
|
|
||||||
|
videoPlayer: HTMLVideoElement | any = "";
|
||||||
|
videoPlaying: boolean = false;
|
||||||
|
videoFullscreen: boolean = false;
|
||||||
|
watched: string = "00:00";
|
||||||
|
controlShowed: boolean = false;
|
||||||
|
videoMuted: boolean = false;
|
||||||
|
videoVolBefMute: number = 0;
|
||||||
|
|
||||||
|
videoHMSFormat(time: any) {
|
||||||
|
let hours: any = Math.round(time / 3600)
|
||||||
|
let minutes: any = Math.round(time / 60 - (hours * 60));
|
||||||
|
let seconds: any = Math.round(time - (minutes * 60));
|
||||||
|
if (seconds < 0) seconds += 60;
|
||||||
|
|
||||||
|
if (hours < 10) hours = `0${hours}`;
|
||||||
|
if (minutes < 10) minutes = `0${minutes}`;
|
||||||
|
if (seconds < 10) seconds = `0${seconds}`;
|
||||||
|
|
||||||
|
return hours == 0 ? `${minutes}:${seconds}` : `${hours}:${minutes}:${seconds}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
jump(e: any) {
|
||||||
|
let progressBar = e.target;
|
||||||
|
const clickX = e.clientX - progressBar.getBoundingClientRect().left;
|
||||||
|
const progressBarWidth = progressBar.clientWidth;
|
||||||
|
const progressValue = (clickX / progressBarWidth) * progressBar.max;
|
||||||
|
this.videoPlayer.currentTime = progressValue;
|
||||||
|
progressBar.value = progressValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
volume = 50
|
||||||
|
showVolRange = false
|
||||||
|
setVolume(volume: number) {
|
||||||
|
this.videoPlayer.volume = volume / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
exitFullScreen() {
|
||||||
|
document.exitFullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMute(){
|
||||||
|
if(this.videoMuted) {
|
||||||
|
this.videoPlayer.volume = this.videoVolBefMute / 100;
|
||||||
|
this.videoMuted = false;
|
||||||
|
this.volume = this.videoVolBefMute
|
||||||
|
}else{
|
||||||
|
this.videoPlayer.volume = 0;
|
||||||
|
this.videoMuted = true;
|
||||||
|
this.videoVolBefMute = this.volume;
|
||||||
|
this.volume = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playVideo() {
|
||||||
|
this.playerActive = true
|
||||||
|
this.videoPlaying = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.videoPlayer.play();
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,4 +40,5 @@ export interface DmStorage {
|
|||||||
messages: WritableSignal<Message[]>
|
messages: WritableSignal<Message[]>
|
||||||
chatData: WritableSignal<Chat>
|
chatData: WritableSignal<Chat>
|
||||||
messageBox: MessageBoxViewModel
|
messageBox: MessageBoxViewModel
|
||||||
|
wsListener: (action: string, message: string) => void
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user