import { useEffect, useMemo, useRef, useState } from 'react' import { createPortal } from 'react-dom' import { KEY_PREFIX_UPLOAD, routeIntercept, stateRecover, setTitle, routeTo } from '../../functions/Route' import { formatTagTextContent, formatTagUsage } from '../../functions/Format' import type { BackendArt } from '../../functions/BackendTypes' import { API_BASE, CDN_BASE, BackendFetch } from '../../functions/Backend' import { wtEvent } from '../../functions/Watchtower' import { toast } from '../../functions/Context' import './styles/Animation.css' import MediaCanvas from '../layout/MediaCanvas' import ModalEmbed from '../layout/ModalEmbed' import HeaderLoading from '../layout/HeaderLoading' import HeaderMessage from '../layout/HeaderMessage' import HeaderError from '../layout/HeaderError' import InputButtonRow from '../inputs/ButtonRow' import InputButton from '../inputs/Button' import InputBack from '../inputs/Back' interface PropsForViewAnimation { id: string } export default function ViewAnimation({ id }: PropsForViewAnimation) { wtEvent('view_animation', { id }) const embedRef = useRef(null) const [data, setData] = useState(stateRecover()) const [error, setError] = useState() const [isDownloading, setDownloading] = useState(false) const [isFullscreen, setFullscreen] = useState(false) const [isLoading, setLoading] = useState(!data) const isOwner = useMemo(() => Object.keys(localStorage).find((k) => k === `${KEY_PREFIX_UPLOAD}${id}`), []) const titleTags = data?.tags.map((t) => formatTagTextContent(t.label)).join(', ') ?? '' const titlePrefix = data ? (data.sticker ? 'Sticker' : 'Animation') : 'Item' const titleMessage = `View: ${titlePrefix} ` useEffect(() => { const start = Date.now() return () => { wtEvent('view_animation_duration', { id, ms: Date.now() - start, }) } }, []) useEffect(() => { if (data) return BackendFetch(`/art/${id}`) .then((resp) => { if (!resp.success) { setError(resp.error) return } setData(resp.json) }) .catch((err) => { setError('Network Error') console.error(err) }) .finally(() => setLoading(false)) }, []) function onFullscreenClose() { wtEvent('action_animation_fullscreen_close', { id }) setFullscreen(false) } function onFullscreenOpen() { wtEvent('action_animation_fullscreen_open', { id }) setFullscreen(true) } function onEmbedClose() { wtEvent('action_animation_embed_close', { id }) embedRef.current?.close() } function onEmbedOpen() { embedRef.current?.showModal() wtEvent('action_animation_embed_open', { id }) } function onShareOpen() { const mobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) const url = window.location.href if (mobile) { navigator.share({ url, title: `${data?.title} ${titlePrefix} - gifuu` }) } else { navigator.clipboard.writeText(url) toast('action-share', 'Copied URL to Clipboard!') } wtEvent('action_animation_share_open', { id, mobile }) } function onDownload() { setDownloading(true) function save(source: Blob, extension: string) { const blob = URL.createObjectURL(source) const a = document.createElement('a') a.href = blob a.download = `${data?.title} [gifuu-${data?.id}].${extension}` a.click() URL.revokeObjectURL(blob) } try { if (!data) throw 'Missing Data' // We can use the already decoded frame for stickers if (data.sticker) { const renderer = document.querySelector('canvas.render') if (!renderer) { throw 'Failed to find renderer' } renderer.toBlob( (blob) => { if (!blob) { throw 'Failed to retrieve frame' } save(blob!, 'png') }, 'image/png', 0.9, ) } else { // Download Standard Animation const url = `${CDN_BASE}/${data.id}/standard.avif` + `?utm_source=${window.location.hostname}&utm_medium=download&utm_term=${data.id}` fetch(url, { cache: 'force-cache' }) .then((resp) => resp.blob()) .then((resp) => save(resp, 'avif')) } wtEvent('action_animation_download', { id, sticker: data?.sticker }) } catch (error) { toast('action-download', String(error)) wtEvent('action_animation_download_failed', { id, reason: String(error) }) } finally { setDownloading(false) } } function onDelete() { const accept = confirm('Delete this animation?') const upload_key = `${KEY_PREFIX_UPLOAD}${id}` const upload_token = localStorage.getItem(upload_key) wtEvent('action_animation_delete', { id, cancel: !accept }) if (!upload_token) return if (!accept) return fetch(`${API_BASE}/art/${id}?token=${upload_token}`, { method: 'DELETE' }) .then(async (res) => { if (!res.ok) { alert(`Request Failed: ${await res.text()}`) return } // Clear Local Data localStorage.removeItem(upload_key) routeTo('/personal') }) .catch((error) => { wtEvent('action_animation_delete_failed', { id, reason: String(error) }) console.error(error) alert('Network Error') }) } if (isLoading) { return ( <> ) } if (error || !data) { return ( <> ) } setTitle(`${titlePrefix}: ${data.title} (${titleTags})`) return ( <> {createPortal(, document.body)} {isFullscreen && createPortal(
, document.body, )}

{data.title}

{data.width} × {data.height} • {new Date(data.created).toLocaleString()} •{' '} {data.rating < 0.8 ? 'Public' : 'Unlisted'} ({(data.rating * 100) | 0}%)

) }