feat: display embedded media player (#2417)
This commit is contained in:
parent
0bd1209bee
commit
957f0d3b17
|
@ -17,7 +17,7 @@ const emojisObject = useEmojisFallback(() => status.emojis)
|
|||
const vnode = $computed(() => {
|
||||
if (!status.content)
|
||||
return null
|
||||
const vnode = contentToVNode(status.content, {
|
||||
return contentToVNode(status.content, {
|
||||
emojis: emojisObject.value,
|
||||
mentions: 'mentions' in status ? status.mentions : undefined,
|
||||
markdown: true,
|
||||
|
@ -25,7 +25,6 @@ const vnode = $computed(() => {
|
|||
status: 'id' in status ? status : undefined,
|
||||
inReplyToStatus: newer,
|
||||
})
|
||||
return vnode
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
@ -26,9 +26,11 @@ const hasSpoilerOrSensitiveMedia = $computed(() => spoilerTextPresent || (status
|
|||
const isSensitiveNonSpoiler = computed(() => status.sensitive && !status.spoilerText && !!status.mediaAttachments.length)
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
@ -56,10 +58,11 @@ const hideAllMedia = computed(
|
|||
:is-preview="isPreview"
|
||||
/>
|
||||
<StatusPreviewCard
|
||||
v-if="status.card"
|
||||
v-if="status.card && !allowEmbeddedMedia"
|
||||
:card="status.card"
|
||||
:small-picture-only="status.mediaAttachments?.length > 0"
|
||||
/>
|
||||
<StatusEmbeddedMedia v-if="allowEmbeddedMedia" :status="status" />
|
||||
<StatusCard
|
||||
v-if="status.reblog"
|
||||
:status="status.reblog" border="~ rounded"
|
||||
|
|
105
components/status/StatusEmbeddedMedia.vue
Normal file
105
components/status/StatusEmbeddedMedia.vue
Normal 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>
|
|
@ -150,6 +150,25 @@ export function convertMastodonHTML(html: string, customEmojis: Record<string, m
|
|||
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) {
|
||||
try {
|
||||
const tree = parse(html)
|
||||
|
|
|
@ -36,7 +36,7 @@ export function contentToVNode(
|
|||
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)
|
||||
return node.value
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ export interface PreferencesSettings {
|
|||
experimentalVirtualScroller: boolean
|
||||
experimentalGitHubCards: boolean
|
||||
experimentalUserPicker: boolean
|
||||
experimentalEmbeddedMedia: boolean
|
||||
}
|
||||
|
||||
export interface UserSettings {
|
||||
|
@ -78,6 +79,7 @@ export const DEFAULT__PREFERENCES_SETTINGS: PreferencesSettings = {
|
|||
experimentalVirtualScroller: true,
|
||||
experimentalGitHubCards: true,
|
||||
experimentalUserPicker: true,
|
||||
experimentalEmbeddedMedia: false,
|
||||
}
|
||||
|
||||
export function getDefaultUserSettings(locales: string[]): UserSettings {
|
||||
|
|
|
@ -500,6 +500,8 @@
|
|||
},
|
||||
"notifications_settings": "Notifications",
|
||||
"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_data_saving": "Enable data saving",
|
||||
"enable_data_saving_description": "Save data by preventing attachments from automatically loading.",
|
||||
|
@ -577,6 +579,7 @@
|
|||
},
|
||||
"boosted_by": "Boosted By",
|
||||
"edited": "Edited {0}",
|
||||
"embedded_warning": "Playing this may reveal your IP address to others.",
|
||||
"favourited_by": "Favorited By",
|
||||
"filter_hidden_phrase": "Filtered by",
|
||||
"filter_show_anyway": "Show anyway",
|
||||
|
|
|
@ -117,6 +117,16 @@ const userSettings = useUserSettings()
|
|||
<div i-ri-flask-line />
|
||||
{{ $t('settings.preferences.title') }}
|
||||
</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
|
||||
:checked="getPreferences(userSettings, 'experimentalVirtualScroller')"
|
||||
@click="togglePreferences('experimentalVirtualScroller')"
|
||||
|
|
Loading…
Reference in a new issue