Multicolumn Bluesky client powered by Angular
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}