mirror of
https://git.shipoclu.com/moon/plspace.git
synced 2026-08-13 02:42:30 +00:00
support flash
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
fetchQuotes,
|
||||
postStatus,
|
||||
setEmojiReaction,
|
||||
uploadMedia,
|
||||
updateProfileFields,
|
||||
updatePublicProfile,
|
||||
votePoll,
|
||||
@@ -97,6 +98,39 @@ describe('updatePublicProfile', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('media uploads', () => {
|
||||
it('preserves an SWF file and its MIME type in the multipart upload', async () => {
|
||||
const swf = new File(['flash bytes'], 'animation.swf', {
|
||||
type: 'application/x-shockwave-flash',
|
||||
})
|
||||
const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => {
|
||||
const form = init?.body as FormData
|
||||
expect(init?.method).toBe('POST')
|
||||
expect(form.get('file')).toBe(swf)
|
||||
expect((form.get('file') as File).name).toBe('animation.swf')
|
||||
expect((form.get('file') as File).type).toBe('application/x-shockwave-flash')
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: 'flash-1',
|
||||
type: 'unknown',
|
||||
url: 'https://media.example.test/animation.swf',
|
||||
preview_url: null,
|
||||
pleroma: { mime_type: 'application/x-shockwave-flash' },
|
||||
}),
|
||||
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
||||
)
|
||||
})
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
|
||||
await uploadMedia(new ApiClient('example.test', 'token'), swf)
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://example.test/api/v1/media',
|
||||
expect.any(Object),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('poll endpoints', () => {
|
||||
it('submits selected poll option indexes as JSON', async () => {
|
||||
const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => {
|
||||
|
||||
@@ -110,7 +110,7 @@ export interface CredentialAccount extends Account {
|
||||
|
||||
export interface MediaAttachment {
|
||||
id: string
|
||||
type: 'unknown' | 'image' | 'gifv' | 'video' | 'audio'
|
||||
type: 'unknown' | 'image' | 'gifv' | 'video' | 'audio' | 'flash'
|
||||
url: string
|
||||
preview_url: string | null
|
||||
remote_url?: string | null
|
||||
@@ -121,6 +121,12 @@ export interface MediaAttachment {
|
||||
small?: { width?: number; height?: number; aspect?: number }
|
||||
[key: string]: unknown
|
||||
}
|
||||
/** Pleroma/Akkoma preserve the original attachment MIME type here. */
|
||||
pleroma?: {
|
||||
mime_type?: string | null
|
||||
name?: string | null
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export interface StatusMention {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
export interface RufflePlayerApi {
|
||||
load(options: RuffleLoadOptions | string): Promise<void>
|
||||
}
|
||||
|
||||
export interface RuffleLoadOptions {
|
||||
url: string
|
||||
autoplay?: 'on' | 'off' | 'auto'
|
||||
letterbox?: 'on' | 'off' | 'fullscreen'
|
||||
allowScriptAccess?: boolean
|
||||
allowNetworking?: 'all' | 'internal' | 'none'
|
||||
openUrlMode?: 'allow' | 'confirm' | 'deny'
|
||||
}
|
||||
|
||||
export interface RufflePlayerElement extends HTMLElement {
|
||||
config: Partial<RuffleLoadOptions>
|
||||
ruffle(version?: 1): RufflePlayerApi
|
||||
}
|
||||
|
||||
export interface RuffleSource {
|
||||
createPlayer(): RufflePlayerElement
|
||||
}
|
||||
|
||||
export interface RufflePublicApi {
|
||||
config?: Record<string, unknown>
|
||||
newest(): RuffleSource
|
||||
}
|
||||
|
||||
export type RuffleLoader = () => Promise<RufflePublicApi>
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
RufflePlayer?: Partial<RufflePublicApi>
|
||||
}
|
||||
}
|
||||
|
||||
let loading: Promise<RufflePublicApi> | null = null
|
||||
|
||||
function installedRuffle(): RufflePublicApi | null {
|
||||
return typeof window.RufflePlayer?.newest === 'function'
|
||||
? (window.RufflePlayer as RufflePublicApi)
|
||||
: null
|
||||
}
|
||||
|
||||
/** Lazy-load the bundled self-hosted runtime once for every Flash attachment. */
|
||||
export const loadRuffle: RuffleLoader = async () => {
|
||||
const installed = installedRuffle()
|
||||
if (installed) return installed
|
||||
if (loading) return loading
|
||||
|
||||
loading = new Promise<RufflePublicApi>((resolve, reject) => {
|
||||
const publicPath = new URL(`${import.meta.env.BASE_URL}ruffle/`, document.baseURI).href
|
||||
window.RufflePlayer = {
|
||||
...(window.RufflePlayer ?? {}),
|
||||
config: {
|
||||
...(window.RufflePlayer?.config ?? {}),
|
||||
polyfills: false,
|
||||
publicPath,
|
||||
},
|
||||
}
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.src = new URL('ruffle.js', publicPath).href
|
||||
script.async = true
|
||||
script.dataset.plspaceRuffle = 'true'
|
||||
script.onload = () => {
|
||||
const api = installedRuffle()
|
||||
if (api) resolve(api)
|
||||
else reject(new Error('Ruffle loaded without installing its player API.'))
|
||||
}
|
||||
script.onerror = () => reject(new Error('Could not load the bundled Ruffle runtime.'))
|
||||
document.head.appendChild(script)
|
||||
}).catch((cause) => {
|
||||
loading = null
|
||||
throw cause
|
||||
})
|
||||
|
||||
return loading
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { MediaAttachment } from '$lib/api/types'
|
||||
import { flashAspectRatio, isFlashAttachment } from './flash'
|
||||
|
||||
function attachment(overrides: Partial<MediaAttachment> = {}): MediaAttachment {
|
||||
return {
|
||||
id: 'file-1',
|
||||
type: 'unknown',
|
||||
url: 'https://media.example.test/file.bin',
|
||||
preview_url: null,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('Flash attachment detection', () => {
|
||||
it('recognizes Pleroma MIME metadata and explicit Flash types', () => {
|
||||
expect(
|
||||
isFlashAttachment(
|
||||
attachment({ pleroma: { mime_type: 'application/x-shockwave-flash' } }),
|
||||
),
|
||||
).toBe(true)
|
||||
expect(isFlashAttachment(attachment({ type: 'flash' }))).toBe(true)
|
||||
})
|
||||
|
||||
it('recognizes case-insensitive SWF paths despite query strings or fragments', () => {
|
||||
expect(
|
||||
isFlashAttachment(
|
||||
attachment({ url: 'https://media.example.test/games/MOVIE.SWF?download=1#play' }),
|
||||
),
|
||||
).toBe(true)
|
||||
expect(
|
||||
isFlashAttachment(attachment({ url: 'https://media.example.test/movie.swf.png' })),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('uses bounded intrinsic dimensions and a safe fallback', () => {
|
||||
expect(
|
||||
flashAspectRatio(
|
||||
attachment({ meta: { original: { width: 1920, height: 1080 } } }),
|
||||
),
|
||||
).toBeCloseTo(16 / 9)
|
||||
expect(
|
||||
flashAspectRatio(attachment({ meta: { original: { width: 10000, height: 1 } } })),
|
||||
).toBeCloseTo(4 / 3)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { MediaAttachment } from '../api/types'
|
||||
|
||||
/** Detect Pleroma's Flash MIME extension and Mastodon-compatible `.swf` URLs. */
|
||||
export function isFlashAttachment(media: MediaAttachment): boolean {
|
||||
if (media.type === 'flash') return true
|
||||
if (/flash/i.test(media.pleroma?.mime_type ?? '')) return true
|
||||
|
||||
for (const value of [media.url, media.remote_url]) {
|
||||
if (!value) continue
|
||||
try {
|
||||
if (/\.swf$/i.test(new URL(value, window.location.href).pathname)) return true
|
||||
} catch {
|
||||
if (/\.swf(?:[?#]|$)/i.test(value)) return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** Use trustworthy server dimensions, while preventing pathological layouts. */
|
||||
export function flashAspectRatio(media: MediaAttachment): number {
|
||||
const width = media.meta?.original?.width
|
||||
const height = media.meta?.original?.height
|
||||
const ratio = width && height ? width / height : Number.NaN
|
||||
return Number.isFinite(ratio) && ratio >= 0.25 && ratio <= 4 ? ratio : 4 / 3
|
||||
}
|
||||
Reference in New Issue
Block a user