html5 media

This commit is contained in:
Daniel Ponte 2025-02-03 21:34:08 -05:00
parent ccd1830da9
commit d8a1f2b85e
10 changed files with 142 additions and 17 deletions

View file

@ -0,0 +1,7 @@
<mat-card class="callInfo" appearance="outlined">
<div class="inc-heading">
<div class="field field-label">Time</div>
<div class="field field-data">{{ call.call_date }}</div>
</div>
<audio controls [src]="call.audioURL! | safe: 'resourceUrl'"></audio>
</mat-card>

View file

@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CallInfoComponent } from './call-info.component';
describe('CallInfoComponent', () => {
let component: CallInfoComponent;
let fixture: ComponentFixture<CallInfoComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CallInfoComponent],
}).compileComponents();
fixture = TestBed.createComponent(CallInfoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,20 @@
import { Component, Input } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { CallRecord } from '../../calls';
import { Share } from '../../shares';
import { SafePipe } from '../calls.service';
@Component({
selector: 'app-call-info',
imports: [MatCardModule, SafePipe],
templateUrl: './call-info.component.html',
styleUrl: './call-info.component.scss',
})
export class CallInfoComponent {
@Input() share!: Share;
call!: CallRecord;
ngOnInit() {
this.call = this.share.sharedItem as CallRecord;
}
}

View file

@ -6,6 +6,14 @@ import { environment } from '.././../environments/environment';
import { TalkgroupService } from '../talkgroups/talkgroups.service';
import { Talkgroup } from '../talkgroup';
import { Share } from '../shares';
import {
DomSanitizer,
SafeHtml,
SafeResourceUrl,
SafeScript,
SafeStyle,
SafeUrl,
} from '@angular/platform-browser';
@Pipe({
name: 'grabDate',
@ -43,7 +51,11 @@ export class TimePipe implements PipeTransform {
export class TalkgroupPipe implements PipeTransform {
constructor(private tgService: TalkgroupService) {}
transform(call: CallRecord, field: string, share: Share|null = null): Observable<string> {
transform(
call: CallRecord,
field: string,
share: Share | null = null,
): Observable<string> {
return this.tgService.getTalkgroup(call.system_id, call.tgid).pipe(
map((tg: Talkgroup) => {
switch (field) {
@ -82,6 +94,50 @@ export class FixedPointPipe implements PipeTransform {
}
}
/**
* Sanitize HTML
*/
@Pipe({
name: 'safe',
})
export class SafePipe implements PipeTransform {
/**
* Pipe Constructor
*
* @param _sanitizer: DomSanitezer
*/
// tslint:disable-next-line
constructor(protected _sanitizer: DomSanitizer) {}
/**
* Transform
*
* @param value: string
* @param type: string
*/
transform(
value: string,
type: string,
): SafeHtml | SafeStyle | SafeScript | SafeUrl | SafeResourceUrl {
switch (type) {
case 'html':
return this._sanitizer.bypassSecurityTrustHtml(value);
case 'style':
return this._sanitizer.bypassSecurityTrustStyle(value);
case 'script':
return this._sanitizer.bypassSecurityTrustScript(value);
case 'url':
return this._sanitizer.bypassSecurityTrustUrl(value);
case 'resourceUrl':
let res = this._sanitizer.bypassSecurityTrustResourceUrl(value);
console.log(res);
return res;
default:
return this._sanitizer.bypassSecurityTrustHtml(value);
}
}
}
@Pipe({
name: 'audioDownloadURL',
standalone: true,

View file

@ -76,13 +76,13 @@
<ng-container matColumnDef="system">
<th mat-header-cell *matHeaderCellDef>System</th>
<td mat-cell *matCellDef="let call">
{{ call | talkgroup: "system":incident | async }}
{{ call | talkgroup: "system" : share | async }}
</td>
</ng-container>
<ng-container matColumnDef="group">
<th mat-header-cell *matHeaderCellDef>Group</th>
<td mat-cell *matCellDef="let call">
{{ call | talkgroup: "group":incident | async }}
{{ call | talkgroup: "group" : share | async }}
</td>
</ng-container>
<ng-container matColumnDef="talkgroup">

View file

@ -154,7 +154,7 @@ export class IncidentEditDialogComponent {
export class IncidentComponent {
incPrime = new Subject<IncidentRecord>();
inc$!: Observable<IncidentRecord>;
@Input() incident?: Share;
@Input() share?: Share;
subscriptions: Subscription = new Subscription();
dialog = inject(MatDialog);
incID!: string;
@ -187,11 +187,11 @@ export class IncidentComponent {
this.incID = this.route.snapshot.paramMap.get('id')!;
incOb = this.incSvc.getIncident(this.incID);
} else {
if (!this.incident) {
if (!this.share) {
return;
}
this.incID = (this.incident.sharedItem as IncidentRecord).id;
incOb = new BehaviorSubject(this.incident.sharedItem as IncidentRecord);
this.incID = (this.share.sharedItem as IncidentRecord).id;
incOb = new BehaviorSubject(this.share.sharedItem as IncidentRecord);
}
this.inc$ = merge(incOb, this.incPrime).pipe(
tap((inc) => {

View file

@ -1,9 +1,17 @@
@let sh = share | async;
@if (sh == null) {
<h1 class="error">Share invalid!</h1>
} @else if (sh.type == "incident") {
<app-incident [incident]="sh"></app-incident>
} @else if (sh.type == "call") {
} @else {
<h1 class="error">Share type {{ sh.type }} unknown</h1>
@switch (sh?.type) {
@case ("incident") {
<app-incident [share]="sh!"></app-incident>
}
@case ("call") {
<app-call-info [share]="sh!" player="true"></app-call-info>
}
@case (null) {
<div class="spinner">
<mat-spinner></mat-spinner>
</div>
}
@default {
<h1 class="error">Share type {{ sh?.type }} unknown</h1>
}
}

View file

@ -5,16 +5,23 @@ import { ActivatedRoute } from '@angular/router';
import { Observable, Subscription, switchMap } from 'rxjs';
import { IncidentComponent } from '../incidents/incident/incident.component';
import { AsyncPipe } from '@angular/common';
import { CallInfoComponent } from '../calls/call-info/call-info.component';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
@Component({
selector: 'app-share',
imports: [AsyncPipe, IncidentComponent],
imports: [
AsyncPipe,
IncidentComponent,
CallInfoComponent,
MatProgressSpinnerModule,
],
templateUrl: './share.component.html',
styleUrl: './share.component.scss',
})
export class ShareComponent {
shareID!: string;
share!: Observable<Share | null>;
share!: Observable<Share>;
constructor(
private route: ActivatedRoute,
private shareSvc: ShareService,

View file

@ -9,6 +9,7 @@ import {
switchMap,
} from 'rxjs';
import { Talkgroup, TalkgroupUpdate, TGID } from '../talkgroup';
import { Share } from '../shares';
export interface Pagination {
page: number;
@ -54,7 +55,11 @@ export class TalkgroupService {
return this.http.get<Talkgroup[]>('/api/talkgroup/');
}
getTalkgroup(sys: number, tg: number): Observable<Talkgroup> {
getTalkgroup(
sys: number,
tg: number,
share: Share | null = null,
): Observable<Talkgroup> {
const key = this.tgKey(sys, tg);
if (!this._getTalkgroup.get(key)) {
let rs = new ReplaySubject<Talkgroup>();