feat: display embedded media player (#2417)

zio/stable
Ayo Ayco 2023-11-07 10:57:44 +01:00 committed by GitHub
parent 0bd1209bee
commit 957f0d3b17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 146 additions and 5 deletions

View File

@ -17,7 +17,7 @@ const emojisObject = useEmojisFallback(() => status.emojis)
const vnode = $computed(() => { const vnode = $computed(() => {
if (!status.content) if (!status.content)
return null return null
const vnode = contentToVNode(status.content, { return contentToVNode(status.content, {
emojis: emojisObject.value, emojis: emojisObject.value,
mentions: 'mentions' in status ? status.mentions : undefined, mentions: 'mentions' in status ? status.mentions : undefined,
markdown: true, markdown: true,
@ -25,7 +25,6 @@ const vnode = $computed(() => {
status: 'id' in status ? status : undefined, status: 'id' in status ? status : undefined,
inReplyToStatus: newer, inReplyToStatus: newer,
}) })
return vnode
}) })
</script> </script>

View File

@ -26,9 +26,11 @@ const hasSpoilerOrSensitiveMedia = $computed(() => spoilerTextPresent || (status
const isSensitiveNonSpoiler = computed(() => status.sensitive && !status.spoilerText && !!status.mediaAttachments.length) const isSensitiveNonSpoiler = computed(() => status.sensitive && !status.spoilerText && !!status.mediaAttachments.length)
const hideAllMedia = computed( const hideAllMedia = computed(
() => { () => {
return currentUser.value ? (getHideMediaByDefault(currentUser.value.account) && !!status.mediaAttachments.length) : false return currentUser.value ? (getHideMediaByDefault(currentUser.value.account) && (!!status.mediaAttachments.length || !!status.card?.html)) : false
}, },
) )
const embeddedMediaPreference = $(usePreferences('experimentalEmbeddedMedia'))
const allowEmbeddedMedia = $computed(() => status.card?.html && embeddedMediaPreference)
</script> </script>
<template> <template>
@ -56,10 +58,11 @@ const hideAllMedia = computed(
:is-preview="isPreview" :is-preview="isPreview"
/> />
<StatusPreviewCard <StatusPreviewCard
v-if="status.card" v-if="status.card && !allowEmbeddedMedia"
:card="status.card" :card="status.card"
:small-picture-only="status.mediaAttachments?.length > 0" :small-picture-only="status.mediaAttachments?.length > 0"
/> />
<StatusEmbeddedMedia v-if="allowEmbeddedMedia" :status="status" />
<StatusCard <StatusCard
v-if="status.reblog" v-if="status.reblog"
:status="status.reblog" border="~ rounded" :status="status.reblog" border="~ rounded"

View File

@ -0,0 +1,105 @@
<script setup lang="ts">
import type { mastodon } from 'masto'
const { status } = defineProps<{
status: mastodon.v1.Status
}>()
const vnode = $computed(() => {
if (!status.card?.html)
return null
const node = sanitizeEmbeddedIframe(status.card?.html)?.children[0]
return node ? nodeToVNode(node) : null
})
const overlayToggle = ref(true)
const card = ref(status.card)
</script>
<template>
<div v-if="card">
<div
v-if="overlayToggle"
h-80
cursor-pointer
relative
>
<div
p-3
absolute
w-full
h-full
z-100
rounded-lg
style="background: linear-gradient(black, rgba(0,0,0,0.5), transparent, transparent, rgba(0,0,0,0.20))"
>
<NuxtLink flex flex-col gap-1 hover:underline text-xs text-light font-light target="_blank" :href="card?.url">
<div flex gap-0.5>
<p flex-row line-clamp-1>
{{ card?.providerName }}<span v-if="card?.authorName"> {{ card?.authorName }}</span>
</p>
<span
flex-row
w-4 h-4
pointer-events-none
i-ri:arrow-right-up-line
/>
</div>
<p font-bold line-clamp-1 text-size-base>
{{ card?.title }}
</p>
<p line-clamp-1>
{{ $t('status.embedded_warning') }}
</p>
</NuxtLink>
<div
flex
h-50
mt-1
justify-center
flex-items-center
>
<button
absolute
bg-primary
opacity-85
rounded-full
hover:bg-primary-active
hover:opacity-95
transition-all
box-shadow-outline
@click.stop.prevent="() => overlayToggle = !overlayToggle"
>
<span
text-light
flex flex-col
gap-3
w-27 h-27
pointer-events-none
i-ri:play-circle-line
/>
</button>
</div>
</div>
<CommonBlurhash
v-if="card?.image"
:blurhash="card.blurhash"
:src="card.image"
w-full
h-full
object-cover
rounded-lg
/>
</div>
<div v-else>
<!-- this inserts the iframe -->
<component :is="vnode" v-if="vnode" rounded-lg h-80 />
</div>
</div>
</template>
<style>
iframe {
width: 100%;
height: 100%;
}
</style>

View File

@ -150,6 +150,25 @@ export function convertMastodonHTML(html: string, customEmojis: Record<string, m
return render(tree) return render(tree)
} }
export function sanitizeEmbeddedIframe(html: string): Node {
const transforms: Transform[] = [
sanitize({
iframe: {
src: (src) => {
if (typeof src !== 'string')
return undefined
const url = new URL(src)
return url.protocol === 'https:' ? src : undefined
},
allowfullscreen: set('true'),
},
}),
]
return transformSync(parse(html), transforms)
}
export function htmlToText(html: string) { export function htmlToText(html: string) {
try { try {
const tree = parse(html) const tree = parse(html)

View File

@ -36,7 +36,7 @@ export function contentToVNode(
return h(Fragment, (tree.children as Node[] || []).map(n => treeToVNode(n))) return h(Fragment, (tree.children as Node[] || []).map(n => treeToVNode(n)))
} }
function nodeToVNode(node: Node): VNode | string | null { export function nodeToVNode(node: Node): VNode | string | null {
if (node.type === TEXT_NODE) if (node.type === TEXT_NODE)
return node.value return node.value

View File

@ -26,6 +26,7 @@ export interface PreferencesSettings {
experimentalVirtualScroller: boolean experimentalVirtualScroller: boolean
experimentalGitHubCards: boolean experimentalGitHubCards: boolean
experimentalUserPicker: boolean experimentalUserPicker: boolean
experimentalEmbeddedMedia: boolean
} }
export interface UserSettings { export interface UserSettings {
@ -78,6 +79,7 @@ export const DEFAULT__PREFERENCES_SETTINGS: PreferencesSettings = {
experimentalVirtualScroller: true, experimentalVirtualScroller: true,
experimentalGitHubCards: true, experimentalGitHubCards: true,
experimentalUserPicker: true, experimentalUserPicker: true,
experimentalEmbeddedMedia: false,
} }
export function getDefaultUserSettings(locales: string[]): UserSettings { export function getDefaultUserSettings(locales: string[]): UserSettings {

View File

@ -500,6 +500,8 @@
}, },
"notifications_settings": "Notifications", "notifications_settings": "Notifications",
"preferences": { "preferences": {
"embedded_media": "Embedded Media Player",
"embedded_media_description": "Display an embedded player instead of the normal preview card when expanding shared media streaming links.",
"enable_autoplay": "Enable Autoplay", "enable_autoplay": "Enable Autoplay",
"enable_data_saving": "Enable data saving", "enable_data_saving": "Enable data saving",
"enable_data_saving_description": "Save data by preventing attachments from automatically loading.", "enable_data_saving_description": "Save data by preventing attachments from automatically loading.",
@ -577,6 +579,7 @@
}, },
"boosted_by": "Boosted By", "boosted_by": "Boosted By",
"edited": "Edited {0}", "edited": "Edited {0}",
"embedded_warning": "Playing this may reveal your IP address to others.",
"favourited_by": "Favorited By", "favourited_by": "Favorited By",
"filter_hidden_phrase": "Filtered by", "filter_hidden_phrase": "Filtered by",
"filter_show_anyway": "Show anyway", "filter_show_anyway": "Show anyway",

View File

@ -117,6 +117,16 @@ const userSettings = useUserSettings()
<div i-ri-flask-line /> <div i-ri-flask-line />
{{ $t('settings.preferences.title') }} {{ $t('settings.preferences.title') }}
</h2> </h2>
<!-- Embedded Media -->
<SettingsToggleItem
:checked="getPreferences(userSettings, 'experimentalEmbeddedMedia')"
@click="togglePreferences('experimentalEmbeddedMedia')"
>
{{ $t('settings.preferences.embedded_media') }}
<template #description>
{{ $t('settings.preferences.embedded_media_description') }}
</template>
</SettingsToggleItem>
<SettingsToggleItem <SettingsToggleItem
:checked="getPreferences(userSettings, 'experimentalVirtualScroller')" :checked="getPreferences(userSettings, 'experimentalVirtualScroller')"
@click="togglePreferences('experimentalVirtualScroller')" @click="togglePreferences('experimentalVirtualScroller')"