Multicolumn Bluesky client powered by Angular
at master 108 lines 2.9 kB view raw
1import { 2 AfterViewInit, 3 ChangeDetectionStrategy, 4 Component, 5 ElementRef, 6 input, 7 OnDestroy, 8 OnInit, 9 viewChildren 10} from '@angular/core'; 11import {AppBskyEmbedExternal} from "@atproto/api"; 12import {DomSanitizer, SafeResourceUrl} from "@angular/platform-browser"; 13import videojs from "video.js"; 14import type Player from "video.js/dist/types/player"; 15import {NgOptimizedImage} from "@angular/common"; 16import {BlueskyGifSnippet, IframeSnippet, LinkSnippet, SnippetSource, SnippetType} from '@models/snippet'; 17import {SnippetUtils} from '@shared/utils/snippet-utils'; 18import {YouTubePlayer} from '@angular/youtube-player'; 19 20type Options = typeof videojs.options; 21 22@Component({ 23 selector: 'external-embed', 24 imports: [ 25 YouTubePlayer, 26 NgOptimizedImage 27 ], 28 templateUrl: './external-embed.component.html', 29 styles: ` 30 .video-js { 31 height: auto; 32 width: 100%; 33 } 34 :host ::ng-deep youtube-player iframe { 35 display: flex; 36 min-width: 0; 37 min-height: 0; 38 height: 100%; 39 width: 100%; 40 } 41 :host ::ng-deep youtube-player > div { 42 display: contents; 43 } 44 :host ::ng-deep youtube-player youtube-player-placeholder { 45 width: 100% !important; 46 height: auto !important; 47 aspect-ratio: 16 / 9; 48 } 49 :host ::ng-deep youtube-player youtube-player-placeholder .youtube-player-placeholder-button { 50 cursor: pointer; 51 } 52 `, 53 changeDetection: ChangeDetectionStrategy.OnPush 54}) 55export class ExternalEmbedComponent implements OnInit, OnDestroy, AfterViewInit { 56 external = input<AppBskyEmbedExternal.ViewExternal>(); 57 target = viewChildren<ElementRef<HTMLVideoElement>>('target'); 58 59 player: Player; 60 options: Options; 61 snippet: LinkSnippet | BlueskyGifSnippet | IframeSnippet; 62 safeURL: SafeResourceUrl; 63 64 protected readonly LinkSnippetType = SnippetType.LINK; 65 protected readonly BlueskyGifSnippetType = SnippetType.BLUESKY_GIF; 66 protected readonly IframeSnippetType = SnippetType.IFRAME; 67 protected readonly YoutubeSnippetSource = SnippetSource.YOUTUBE; 68 69 constructor( 70 private sanitizer: DomSanitizer, 71 ) {} 72 73 ngOnInit() { 74 this.snippet = SnippetUtils.detectSnippet(this.external()); 75 76 if (this.snippet.type === SnippetType.IFRAME) { 77 this.safeURL = this.sanitizer.bypassSecurityTrustResourceUrl(this.snippet.url); 78 } 79 } 80 81 ngAfterViewInit() { 82 if (this.snippet.type === SnippetType.BLUESKY_GIF) { 83 this.options = { 84 fluid: true, 85 aspectRatio: this.snippet.ratio, 86 autoplay: true, 87 loop: true, 88 sources: { 89 src: this.snippet.url, 90 type: 'video/webm' 91 }, 92 controls: true, 93 muted: true, 94 playsinline: true, 95 preload: 'none', 96 bigPlayButton: true, 97 controlBar: false, 98 }; 99 100 this.player = videojs(this.target()[0].nativeElement, this.options); 101 } 102 } 103 104 ngOnDestroy() { 105 this.player?.dispose(); 106 } 107 108}