ぼざクリタグ広場 https://hub.nizika.monster
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

115 lines
3.0 KiB

  1. import { useEffect, useRef, useState } from 'react'
  2. import { Helmet } from 'react-helmet-async'
  3. import { useParams } from 'react-router-dom'
  4. import PostEmbed from '@/components/PostEmbed'
  5. import MainArea from '@/components/layout/MainArea'
  6. import { SITE_TITLE } from '@/config'
  7. import { apiGet, apiPatch, apiPut } from '@/lib/api'
  8. import { fetchPost } from '@/lib/posts'
  9. import type { FC } from 'react'
  10. import type { NiconicoMetadata, NiconicoViewerHandle, Post, Theatre } from '@/types'
  11. type TheatreInfo = {
  12. hostFlg: boolean
  13. postId: number | null
  14. postStartedAt: string | null }
  15. export default (() => {
  16. const { id } = useParams ()
  17. const embedRef = useRef<NiconicoViewerHandle> (null)
  18. const [loading, setLoading] = useState (false)
  19. const [theatre, setTheatre] = useState<Theatre | null> (null)
  20. const [theatreInfo, setTheatreInfo] =
  21. useState<TheatreInfo> ({ hostFlg: false, postId: null, postStartedAt: null })
  22. const [post, setPost] = useState<Post | null> (null)
  23. const [videoLength, setVideoLength] = useState (9_999_999_999)
  24. useEffect (() => {
  25. if (!(id))
  26. return
  27. void (async () => {
  28. setTheatre (await apiGet<Theatre> (`/theatres/${ id }`))
  29. }) ()
  30. const interval = setInterval (async () => {
  31. if (theatreInfo.hostFlg
  32. && theatreInfo.postStartedAt
  33. && ((new Date).getTime () - (new Date (theatreInfo.postStartedAt)).getTime ()
  34. > videoLength))
  35. setTheatreInfo ({ hostFlg: true, postId: null, postStartedAt: null })
  36. else
  37. setTheatreInfo (await apiPut<TheatreInfo> (`/theatres/${ id }/watching`))
  38. }, 1_000)
  39. return () => clearInterval (interval)
  40. }, [id, theatreInfo.hostFlg, theatreInfo.postStartedAt, videoLength])
  41. useEffect (() => {
  42. if (!(theatreInfo.hostFlg) || loading)
  43. return
  44. if (theatreInfo.postId == null)
  45. {
  46. void (async () => {
  47. setLoading (true)
  48. await apiPatch<void> (`/theatres/${ id }/next_post`)
  49. setLoading (false)
  50. }) ()
  51. return
  52. }
  53. }, [id, loading, theatreInfo.hostFlg, theatreInfo.postId])
  54. useEffect (() => {
  55. if (theatreInfo.postId == null)
  56. return
  57. void (async () => {
  58. setPost (await fetchPost (String (theatreInfo.postId)))
  59. }) ()
  60. }, [theatreInfo.postId, theatreInfo.postStartedAt])
  61. const syncPlayback = (meta: NiconicoMetadata) => {
  62. if (!(theatreInfo.postStartedAt))
  63. return
  64. const targetTime =
  65. ((new Date).getTime () - (new Date (theatreInfo.postStartedAt)).getTime ())
  66. const drift = Math.abs (meta.currentTime - targetTime)
  67. if (drift > 5_000)
  68. embedRef.current?.seek (targetTime)
  69. }
  70. return (
  71. <MainArea>
  72. <Helmet>
  73. {theatre && (
  74. <title>
  75. {'上映会場'
  76. + (theatre.name ? `『${ theatre.name }』` : ` #${ theatre.id }`)
  77. + ` | ${ SITE_TITLE }`}
  78. </title>)}
  79. </Helmet>
  80. {post && (
  81. <PostEmbed
  82. ref={embedRef}
  83. post={post}
  84. onLoadComplete={info => {
  85. embedRef.current?.play ()
  86. setVideoLength (info.lengthInSeconds * 1_000)
  87. }}
  88. onMetadataChange={meta => {
  89. syncPlayback (meta)
  90. }}/>)}
  91. </MainArea>)
  92. }) satisfies FC