Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions app/components/VideoPlayer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<script setup lang="ts">
import Hls from 'hls.js'

const props = defineProps<{
src: string
autoplay?: boolean
}>()

const videoRef = useTemplateRef<HTMLVideoElement>('videoRef')

let hls: Hls | null = null

watch(
[videoRef, () => props.src],
([video, src]) => {
if (!video || !Hls.isSupported()) return

hls?.destroy()
hls = new Hls()
hls.loadSource(src)
hls.attachMedia(video)
hls.on(Hls.Events.ERROR, err => {
console.log(err)

Check warning on line 23 in app/components/VideoPlayer.vue

View workflow job for this annotation

GitHub Actions / 🤖 Autofix code

eslint(no-console)

Unexpected console statement.

Check warning on line 23 in app/components/VideoPlayer.vue

View workflow job for this annotation

GitHub Actions / 🔠 Lint project

eslint(no-console)

Unexpected console statement.
})
Comment thread
alexdln marked this conversation as resolved.
},
{ immediate: true, flush: 'post' },
)

onScopeDispose(() => {
hls?.destroy()
})

useIntersectionObserver(
videoRef,
([entry]) => {
const video = videoRef.value
if (!props.autoplay || !video || !entry) return

if (entry.isIntersecting) {
void video.play().catch(() => {})
} else {
video.pause()
}
},
{ threshold: 0.5 },
)
</script>

<template>
<video ref="videoRef" :src="src" />
</template>
61 changes: 33 additions & 28 deletions app/components/global/BlueskyPostEmbed.client.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ interface BlueskyPost {
images?: EmbedImage[]
external?: EmbedExternal
thumbnail?: string
playlist?: string
aspectRatio?: { width: number; height: number }
}
likeCount?: number
Expand Down Expand Up @@ -115,18 +116,20 @@ const postUrl = computed(() => {
<span class="i-svg-spinners:90-ring-with-bg h-5 w-5 inline-block" />
</div>

<a
v-else-if="post"
:href="postUrl ?? '#'"
target="_blank"
rel="noopener noreferrer"
class="not-prose block my-4 rounded-lg border border-border bg-bg-subtle p-4 sm:p-5 no-underline hover:border-border-hover transition-colors duration-200 relative group"
>
<!-- Bluesky icon -->
<span
class="i-simple-icons:bluesky w-5 h-5 text-fg-subtle group-hover:text-blue-500 absolute top-4 end-4 sm:top-5 sm:end-5"
aria-hidden="true"
/>
<div class="not-prose relative bg-bg-subtle p-4 sm:p-5 my-4 sm:my-5" v-else-if="post">
<a
:href="postUrl ?? '#'"
target="_blank"
rel="noopener noreferrer"
class="before:(absolute content-[''] inset-0 hover:border-border-hover border border-border transition-colors duration-200 rounded-lg) block no-underline group"
:title="$t('blog.atproto.view_on_bluesky')"
>
<!-- Bluesky icon -->
<span
class="i-simple-icons:bluesky w-5 h-5 text-fg-subtle group-hover:text-blue-500 absolute top-4 end-4 sm:top-5 sm:end-5"
aria-hidden="true"
/>
</a>

<!-- Author row -->
<div class="flex items-center gap-3 mb-3 pe-7">
Expand Down Expand Up @@ -169,7 +172,12 @@ const postUrl = computed(() => {

<!-- Embedded external embed -->
<template v-if="post.embed?.external && post.embed.external.uri">
<div class="block mb-3 p-0.5 bg-bg-muted rounded-lg">
<a
:href="post.embed.external.uri"
target="_blank"
rel="noopener noreferrer"
class="relative block mb-3 p-0.5 bg-bg-muted hover:bg-bg-elevated rounded-lg z-10 duration-300 transition-colors"
>
<img
v-if="post.embed.external.thumb"
:src="post.embed.external.thumb"
Expand All @@ -185,25 +193,22 @@ const postUrl = computed(() => {
{{ post.embed.external.description }}
</p>
</div>
</div>
</a>
</template>

<!-- Embedded video -->
<template v-if="post.embed?.thumbnail">
<template v-if="post.embed?.playlist">
<div class="relative block mb-3 p-0.5 bg-bg-muted rounded-lg">
<img
:src="post.embed.thumbnail"
alt=""
class="w-full rounded-lg object-cover"
:height="post.embed.aspectRatio?.height"
:width="post.embed.aspectRatio?.width"
loading="lazy"
<VideoPlayer
:poster="post.embed.thumbnail"
playsInline
controls
preload="none"
:src="post.embed.playlist"
muted
loop
class="block max-h-150 object-contain w-full rounded-lg"
/>
<div
class="absolute inset-0 bg-bg/60 light:bg-bg/80 flex items-center justify-center text-fg font-medium"
>
Click to watch video on Bluesky
</div>
</div>
</template>

Expand All @@ -223,5 +228,5 @@ const postUrl = computed(() => {
{{ post.replyCount }}
</span>
</div>
</a>
</div>
</template>
4 changes: 3 additions & 1 deletion modules/security-headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export default defineNuxtModule({
'https://registry.npmjs.org',
'https://api.npmjs.org',
'https://npm.antfu.dev',
'https://video.bsky.app',
'https://video.cdn.bsky.app',
BLUESKY_API,
...ALL_KNOWN_GIT_API_ORIGINS,
// Local CLI connector (npmx CLI communicates via localhost)
Expand All @@ -76,7 +78,7 @@ export default defineNuxtModule({
`script-src 'self' 'unsafe-inline'`,
`style-src 'self' 'unsafe-inline'`,
`img-src ${imgSrc}`,
`media-src 'self'`,
`media-src 'self' blob:`,
`font-src 'self'`,
`connect-src ${connectSrc}`,
`frame-src ${frameSrc}`,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"fast-npm-meta": "1.4.2",
"focus-trap": "^8.0.0",
"gray-matter": "4.0.3",
"hls.js": "1.6.16",
"ipaddr.js": "2.3.0",
"marked": "18.0.0",
"module-replacements": "3.0.0-beta.7",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/unit/a11y-component-coverage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const SKIPPED_COMPONENTS: Record<string, string> = {
'Changelog/Markdown.vue': 'Requires API call & only renders markdown html',
'Translation/StatusByFile.unused.vue': 'Unused component, might be needed in the future',
'ColorScheme/Img.vue': 'Image component, basic ui',
'VideoPlayer.vue': 'Atproto video component, basic ui',
}

function normalizeComponentPath(filePath: string): string {
Expand Down
Loading