This commit is contained in:
2025-05-25 14:53:37 +09:00
parent fa547bc62f
commit 41668fa894
8 changed files with 209 additions and 141 deletions
+30 -27
View File
@@ -15,23 +15,23 @@ const NicoViewer: React.FC = (props: Props) => {
const iframeRef = useRef<HTMLIFrameElement> (null)
const [screenWidth, setScreenWidth] = useState<CSSProperties['width']> ()
const [screenHeight, setScreenHeight] = useState<CSSProperties['height']> ()
const [isLandscape, setIsLandScape] = useState<boolean> (false)
const [isFullScreen, setIsFullScreen] = useState<boolean> (false)
const [landscape, setLandscape] = useState<boolean> (false)
const [fullScreen, setFullScreen] = useState<boolean> (false)
const src = `https://embed.nicovideo.jp/watch/${id}?persistence=1&oldScript=1&referer=&from=0&allowProgrammaticFullScreen=1`;
const styleFullScreen: CSSProperties = isFullScreen ? {
const styleFullScreen: CSSProperties = fullScreen ? {
top: 0,
left: isLandscape ? 0 : '100%',
left: landscape ? 0 : '100%',
position: 'fixed',
width: screenWidth,
height: screenHeight,
zIndex: 2147483647,
maxWidth: 'none',
transformOrigin: '0% 0%',
transform: isLandscape ? 'none' : 'rotate(90deg)',
transform: landscape ? 'none' : 'rotate(90deg)',
WebkitTransformOrigin: '0% 0%',
WebkitTransform: isLandscape ? 'none' : 'rotate(90deg)',
WebkitTransform: landscape ? 'none' : 'rotate(90deg)',
} : {};
const margedStyle = {
@@ -45,38 +45,40 @@ const NicoViewer: React.FC = (props: Props) => {
const onMessage = (event: MessageEvent<any>) => {
if (!iframeRef.current || event.source !== iframeRef.current.contentWindow) return;
if (event.data.eventName === 'enterProgrammaticFullScreen') {
setIsFullScreen(true);
setFullScreen(true);
} else if (event.data.eventName === 'exitProgrammaticFullScreen') {
setIsFullScreen(false);
setFullScreen(false);
}
};
window.addEventListener('message', onMessage);
addEventListener('message', onMessage);
return () => {
window.removeEventListener('message', onMessage);
removeEventListener('message', onMessage);
};
}, []);
useLayoutEffect(() => {
if (!isFullScreen) return;
if (!(fullScreen))
return
const initialScrollX = window.scrollX;
const initialScrollY = window.scrollY;
let timer: NodeJS.Timeout;
let ended = false;
const initialScrollX = window.scrollX
const initialScrollY = window.scrollY
let timer: NodeJS.Timeout
let ended = false
const pollingResize = () => {
if (ended) return;
if (ended)
return
const isLandscape = window.innerWidth >= window.innerHeight;
const windowWidth = `${isLandscape ? window.innerWidth : window.innerHeight}px`;
const windowHeight = `${isLandscape ? window.innerHeight : window.innerWidth}px`;
const landscape = window.innerWidth >= window.innerHeight
const windowWidth = `${landscape ? window.innerWidth : window.innerHeight}px`
const windowHeight = `${landscape ? window.innerHeight : window.innerWidth}px`
setIsLandScape(isLandscape);
setScreenWidth(windowWidth);
setScreenHeight(windowHeight);
timer = setTimeout(startPollingResize, 200);
setLandScape (Landscape)
setScreenWidth (windowWidth)
setScreenHeight (windowHeight)
timer = setTimeout (startPollingResize, 200)
}
const startPollingResize = () => {
@@ -94,12 +96,13 @@ const NicoViewer: React.FC = (props: Props) => {
ended = true;
window.scrollTo(initialScrollX, initialScrollY);
};
}, [isFullScreen]);
}, [fullScreen]);
useEffect(() => {
if (!isFullScreen) return;
window.scrollTo(0, 0);
}, [screenWidth, screenHeight, isFullScreen]);
if (!(fullScreen))
return
scrollTo (0, 0)
}, [screenWidth, screenHeight, fullScreen])
return <iframe ref={iframeRef}
src={src}
+7 -3
View File
@@ -7,10 +7,8 @@ import { API_BASE_URL } from '../config'
const TagSearch: React.FC = () => {
const navigate = useNavigate ()
const location = useLocation ()
const query = new URLSearchParams (location.search)
const tagsQuery = query.get ('tags') ?? ''
const [search, setSearch] = useState (tagsQuery)
const [search, setSearch] = useState ('')
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && search.length > 0)
@@ -18,6 +16,12 @@ const TagSearch: React.FC = () => {
{ replace: true })
}
useEffect (() => {
const query = new URLSearchParams (location.search)
const tagsQuery = query.get ('tags') ?? ''
setSearch (tagsQuery)
}, [location.search])
return (
<input
type="text"
+61 -40
View File
@@ -4,65 +4,86 @@ import { Link, useParams } from 'react-router-dom'
import { API_BASE_URL } from '../config'
import TagSearch from './TagSearch'
type Tag = { id: number
name: string }
type Tag = { id: number
name: string
category: string }
type TagByCategory = { [key: string]: Tag[] }
type OriginalTag = { id: number
name: string
category: string }
type OriginalTag = { id: number
name: string
category: string }
type Post = { id: number
url: string
title: string
thumbnail: string
tags: Tag[] }
type Props = { posts: Post[]
setPosts: (posts: Post[]) => void }
const tagNameMap: { [key: string]: string } = {
general: '一般',
deerjikist: 'ニジラー',
nico: 'ニコニコタグ' }
const TagSidebar: React.FC = () => {
const { postId } = useParams<{ postId?: number }> ()
const [tags, setTags] = useState<TagByCategory> ({ })
const TagSidebar: React.FC = (props: Props) => {
const { posts, setPosts } = props
useEffect(() => {
const [tags, setTags] = useState<TagByCategory> ({ })
const [tagsCounts, setTagsCounts] = useState<{ [key: id]: number }> ({ })
useEffect (() => {
const fetchTags = async () => {
try {
const response = await axios.get (`${API_BASE_URL}/tags`
+ (postId ? `?post=${ postId }` : ''))
const tagsTmp: TagByCategory = { }
for (const tag of response.data)
try
{
let tagsTmp: TagByCategory = { }
let tagsCountsTmp: { [key: id]: number } = { }
for (const post of posts)
{
if (!(tag.category in tagsTmp))
tagsTmp[tag.category] = []
tagsTmp[tag.category].push ({ id: tag.id, name: tag.name })
for (const tag of post.tags)
{
if (!(tag.category in tagsTmp))
tagsTmp[tag.category] = []
tagsTmp[tag.category].push (tag)
if (!(tag.id in tagsCountsTmp))
tagsCountsTmp[tag.id] = 0
++tagsCountsTmp[tag.id]
}
}
for (const cat of Object.keys (tagsTmp))
tagsTmp[cat].sort ((tagA, tagB) => tagA.name < tagB.name ? -1 : 1)
setTags (tagsTmp)
} catch (error) {
console.error('Failed to fetch tags:', error)
setTagsCounts (tagsCountsTmp)
}
catch (error)
{
console.error ('Failed to fetch tags:', error)
}
}
fetchTags()
}, [postId])
fetchTags ()
}, [posts])
return (
<div className="w-64 bg-gray-100 p-4 border-r border-gray-200 h-full">
<TagSearch />
{['general', 'deerjikist', 'nico'].map (cat => (cat in tags) ? (
<>
<h2>{tagNameMap[cat]}</h2>
<ul>
{tags[cat].map (tag => (
<li key={tag.id} className="mb-2">
<Link
to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}
className="text-blue-600 hover:underline"
>
{tag.name}
</Link>
</li>
))}
</ul>
</>
) : <></>)}
</div>
)
{['general', 'deerjikist', 'nico'].map (cat => cat in tags && <>
<h2>{tagNameMap[cat]}</h2>
<ul>
{tags[cat].map (tag => (
<li key={tag.id} className="mb-2">
<Link to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}
className="text-blue-600 hover:underline">
{tag.name}
</Link>
{posts.length > 1 && <span className="ml-1">{tagsCounts[tag.id]}</span>}
</li>))}
</ul>
</>)}
</div>)
}
export default TagSidebar
+9 -11
View File
@@ -1,20 +1,18 @@
import React from "react"
import { Link } from 'react-router-dom'
const TopNav: React.FC = () => {
return (
const TopNav: React.FC = () => (
<nav className="bg-gray-800 text-white p-3 flex justify-between items-center w-full">
<div className="flex items-center gap-4">
<a href="/" className="text-xl font-bold text-orange-500"> </a>
<a href="/" className="hover:text-orange-500"></a>
<a href="/deerjikists" className="hover:text-orange-500"></a>
<a href="/tags" className="hover:text-orange-500"></a>
<a href="/wiki" className="hover:text-orange-500">Wiki</a>
<Link to="/" className="text-xl font-bold text-orange-500"> </Link>
<Link to="/posts" className="hover:text-orange-500"></Link>
<Link to="/deerjikists" className="hover:text-orange-500"></Link>
<Link to="/tags" className="hover:text-orange-500"></Link>
<Link to="/wiki" className="hover:text-orange-500">Wiki</Link>
</div>
<div className="ml-auto pr-4">
<a href="/login" className="hover:text-orange-500"></a>
<Link to="/login" className="hover:text-orange-500"></Link>
</div>
</nav>
)
}
</nav>)
export default TopNav