diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 75b76da..2847686 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -13,13 +13,11 @@ import DialogueProvider from '@/components/dialogues/DialogueProvider' import { Toaster } from '@/components/ui/toaster' import { apiPost, isApiError } from '@/lib/api' import { applyClientAppearance, - CLIENT_SETTINGS_EVENT, fetchUserSettings, - getClientAnimationMode, getClientThemeMode, seedClientThemeMode } from '@/lib/settings' +import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' -import type { ClientAnimationMode } from '@/lib/settings' import { KeyboardShortcutsProvider } from '@/lib/useKeyboardShortcuts' import DeerjikistDetailPage from '@/pages/deerjikists/DeerjikistDetailPage' import MaterialBasePage from '@/pages/materials/MaterialBasePage' @@ -112,8 +110,8 @@ const PostDetailRoute = ({ user }: { user: User | null }) => { const App: FC = () => { const [user, setUser] = useState (null) const [status, setStatus] = useState (200) - const [animationMode, setAnimationMode] = - useState (() => getClientAnimationMode ()) + const behaviourSettings = useClientBehaviourSettings () + const animationMode = behaviourSettings.animation ?? 'normal' const appLayoutTransition = useMemo ( () => ( @@ -139,16 +137,6 @@ const App: FC = () => { return () => mediaQuery.removeEventListener ('change', handleThemeChange) }, []) - useEffect (() => { - const handleClientSettingsChange = () => { - setAnimationMode (getClientAnimationMode ()) - } - - window.addEventListener (CLIENT_SETTINGS_EVENT, handleClientSettingsChange) - return () => - window.removeEventListener (CLIENT_SETTINGS_EVENT, handleClientSettingsChange) - }, []) - useEffect (() => { const createUser = async () => { const data = await apiPost<{ code: string; user: User }> ('/users') diff --git a/frontend/src/components/PostEmbed.tsx b/frontend/src/components/PostEmbed.tsx index 7ff3e14..9933ada 100644 --- a/frontend/src/components/PostEmbed.tsx +++ b/frontend/src/components/PostEmbed.tsx @@ -5,7 +5,7 @@ import NicoViewer from '@/components/NicoViewer' import TwitterEmbed from '@/components/TwitterEmbed' import { useDialogue } from '@/components/dialogues/DialogueProvider' import { Button } from '@/components/ui/button' -import { getClientEmbedAutoLoadMode } from '@/lib/settings' +import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' import type { FC, RefObject } from 'react' @@ -36,7 +36,8 @@ const PostEmbed: FC = ( onError }, ) => { const dialogue = useDialogue () - const embedAutoLoadMode = getClientEmbedAutoLoadMode () + const behaviourSettings = useClientBehaviourSettings () + const embedAutoLoadMode = behaviourSettings.embedAutoLoad ?? 'auto' const [manualLoadRequested, setManualLoadRequested] = useState (false) const [framed, setFramed] = useState (false) const [youtubePlayer, setYoutubePlayer] = useState (null) diff --git a/frontend/src/components/PostList.tsx b/frontend/src/components/PostList.tsx index e7cced5..8d50c66 100644 --- a/frontend/src/components/PostList.tsx +++ b/frontend/src/components/PostList.tsx @@ -3,7 +3,7 @@ import { useRef } from 'react' import { useLocation } from 'react-router-dom' import PrefetchLink from '@/components/PrefetchLink' -import { getClientThumbnailMode } from '@/lib/settings' +import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' import { cn } from '@/lib/utils' import { useSharedTransitionStore } from '@/stores/sharedTransitionStore' @@ -17,7 +17,15 @@ type Props = { posts: Post[] const PostList: FC = ({ posts, onClick }) => { const location = useLocation () - const thumbnailMode = getClientThumbnailMode () + const behaviourSettings = useClientBehaviourSettings () + const thumbnailMode = behaviourSettings.thumbnailMode ?? 'normal' + const animationMode = behaviourSettings.animation ?? 'normal' + const cardLayoutTransition = + animationMode === 'off' + ? { duration: 0 } + : animationMode === 'reduced' + ? { duration: .08, ease: 'linear' as const } + : { duration: .2, ease: 'easeOut' as const } const setForLocationKey = useSharedTransitionStore (s => s.setForLocationKey) @@ -41,7 +49,8 @@ const PostList: FC = ({ posts, onClick }) => { }}> 0 && 'ring-4 ring-green-500', @@ -61,7 +70,7 @@ const PostList: FC = ({ posts, onClick }) => { cardRef.current.style.zIndex = '' cardRef.current.style.position = '' }} - transition={{ layout: { duration: .2, ease: 'easeOut' } }}> + transition={{ layout: cardLayoutTransition }}> {thumbnailMode === 'off' ? (
= ({ user }) => { if (moreVsbl) setMoreVsbl (false) }} - transition={{ layout: { duration: .2, ease: 'easeOut' } }} + transition={{ layout: topNavTransition }} onAnimationComplete={() => { measure (moreVsbl ? -1 : activeIdx) }}> @@ -282,12 +304,14 @@ const TopNav: FC = ({ user }) => {

{item.name}

@@ -298,25 +322,29 @@ const TopNav: FC = ({ user }) => { ? ( + exit: { y: -40, opacity: 0 } }))}> {subItem.component} ) : ( + exit: { y: -40, opacity: 0 } }))}> = ({ user }) => { : ((visibleMenu[activeIdx]?.subMenu ?? []).length > 0 && (
- - ({ y: d * 24, opacity: 0 }), - centre: { y: 0, opacity: 1 }, - exit: (d: -1 | 1) => ({ y: (-d) * 24, opacity: 0 }) }} - className="absolute inset-0 flex items-center px-3" - initial="enter" - animate="centre" - exit="exit" - transition={{ duration: .2, ease: 'easeOut' }}> - {(visibleMenu[activeIdx]?.subMenu ?? []) - .filter (item => item.visible ?? true) - .map ((item, i) => ( - 'component' in item - ? ( - - {item.component} - ) - : ( - - - {item.name} - - )))} - - + {animationsOff + ? ( +
+ {(visibleMenu[activeIdx]?.subMenu ?? []) + .filter (item => item.visible ?? true) + .map ((item, i) => ( + 'component' in item + ? ( +
+ {item.component} +
) + : ( +
+ + {item.name} + +
)))} +
) + : ( + + ({ y: d * 24, opacity: 0 }), + centre: { y: 0, opacity: 1 }, + exit: (d: -1 | 1) => ({ y: (-d) * 24, opacity: 0 }) }} + className="absolute inset-0 flex items-center px-3" + initial="enter" + animate="centre" + exit="exit" + transition={topNavTransition}> + {(visibleMenu[activeIdx]?.subMenu ?? []) + .filter (item => item.visible ?? true) + .map ((item, i) => ( + 'component' in item + ? ( + + {item.component} + ) + : ( + + + {item.name} + + )))} + + )}
))}
- - {menuOpen && ( - - - {visibleMenu.map ((item, i) => ( - - ) => { - if (i !== openItemIdx) - { - ev.preventDefault () - setOpenItemIdx (i) - } - }}> - {item.name} - + {animationsOff + ? ( + menuOpen && ( +
+ + {visibleMenu.map ((item, i) => ( + + ) => { + if (i !== openItemIdx) + { + ev.preventDefault () + setOpenItemIdx (i) + } + }}> + {item.name} + - - {i === openItemIdx && ( - - {item.subMenu - .filter (subItem => subItem.visible ?? true) - .map ((subItem, j) => ( - 'component' in subItem - ? ( - - {subItem.component} - ) - : ( - - {subItem.name} - )))} - )} - - ))} - { - itemsRef.current[visibleMenu.length] = el - }} - className={cn ('w-full min-h-[40px] flex items-center pl-8', - ((openItemIdx < 0) - && 'font-bold bg-yellow-50 dark:bg-red-950'))}> - その他 » - - - - )} - + {i === openItemIdx && ( +
+ {item.subMenu + .filter (subItem => subItem.visible ?? true) + .map ((subItem, j) => ( + 'component' in subItem + ? ( + + {subItem.component} + ) + : ( + + {subItem.name} + )))} +
)} + ))} + { + itemsRef.current[visibleMenu.length] = el + }} + className={cn ('w-full min-h-[40px] flex items-center pl-8', + ((openItemIdx < 0) + && 'font-bold bg-yellow-50 dark:bg-red-950'))}> + その他 » + + + +
)) + : ( + + {menuOpen && ( + + + {visibleMenu.map ((item, i) => ( + + ) => { + if (i !== openItemIdx) + { + ev.preventDefault () + setOpenItemIdx (i) + } + }}> + {item.name} + + + + {i === openItemIdx && ( + + {item.subMenu + .filter (subItem => subItem.visible ?? true) + .map ((subItem, j) => ( + 'component' in subItem + ? ( + + {subItem.component} + ) + : ( + + {subItem.name} + )))} + )} + + ))} + { + itemsRef.current[visibleMenu.length] = el + }} + className={cn ('w-full min-h-[40px] flex items-center pl-8', + ((openItemIdx < 0) + && 'font-bold bg-yellow-50 dark:bg-red-950'))}> + その他 » + + + + )} + )} ) } diff --git a/frontend/src/components/users/BehaviourSettingsSection.tsx b/frontend/src/components/users/BehaviourSettingsSection.tsx index 7e5ca1d..7ad75cf 100644 --- a/frontend/src/components/users/BehaviourSettingsSection.tsx +++ b/frontend/src/components/users/BehaviourSettingsSection.tsx @@ -2,17 +2,16 @@ import { useEffect, useMemo, useState } from 'react' import { Button } from '@/components/ui/button' import { toast } from '@/components/ui/use-toast' -import { getClientBehaviorSettings, - setClientBehaviorSettings } from '@/lib/settings' +import { getClientBehaviorSettings, setClientBehaviorSettings } from '@/lib/settings' import { cn } from '@/lib/utils' -import type { - ClientAnimationMode, - ClientBehaviorSettings, - ClientEmbedAutoLoadMode, - ClientThumbnailMode } from '@/lib/settings' import type { FC, ReactNode } from 'react' +import type { ClientAnimationMode, + ClientBehaviorSettings, + ClientEmbedAutoLoadMode, + ClientThumbnailMode } from '@/lib/settings' + type Props = { sectionClassName: string } @@ -31,25 +30,22 @@ type SettingBlockProps = { children: ReactNode } const animationOptions: SegmentedOption[] = [ - { value: 'normal', label: '通常' }, - { value: 'reduced', label: '控えめ' }, { value: 'off', label: 'なし' }, -] + { value: 'reduced', label: '控えめ' }, + { value: 'normal', label: '通常' }] const embedAutoLoadOptions: SegmentedOption[] = [ - { value: 'auto', label: '自動' }, - { value: 'manual', label: 'クリック時' }, { value: 'off', label: '読み込まない' }, -] + { value: 'manual', label: 'クリック時' }, + { value: 'auto', label: '自動' }] const thumbnailModeOptions: SegmentedOption[] = [ - { value: 'normal', label: '通常' }, - { value: 'light', label: '軽量' }, { value: 'off', label: '非表示' }, -] + { value: 'light', label: '軽量' }, + { value: 'normal', label: '通常' }] -const SegmentedControl = ( +const SegmentedControl = ( { value, options, onChange }: SegmentedControlProps, ) => { return ( @@ -103,8 +99,7 @@ const BehaviourSettingsSection: FC = ({ sectionClassName }) => { const hasUnsavedChanges = useMemo ( () => JSON.stringify (draftSettings) !== JSON.stringify (savedSettings), - [draftSettings, savedSettings], - ) + [draftSettings, savedSettings]) const updateDraft = ( key: Key, @@ -129,12 +124,6 @@ const BehaviourSettingsSection: FC = ({ sectionClassName }) => {

動作

-

- この設定は現在のブラウザに保存されます。 -

-

- 重い端末や通信量を抑えたい場合に調整します。 -

{hasUnsavedChanges && (

未保存の変更があります diff --git a/frontend/src/lib/useClientBehaviourSettings.ts b/frontend/src/lib/useClientBehaviourSettings.ts new file mode 100644 index 0000000..d587bd3 --- /dev/null +++ b/frontend/src/lib/useClientBehaviourSettings.ts @@ -0,0 +1,26 @@ +import { useEffect, useState } from 'react' + +import { + CLIENT_SETTINGS_EVENT, + getClientBehaviorSettings, +} from '@/lib/settings' + +import type { ClientBehaviorSettings } from '@/lib/settings' + + +export const useClientBehaviourSettings = (): ClientBehaviorSettings => { + const [settings, setSettings] = + useState (() => getClientBehaviorSettings ()) + + useEffect (() => { + const handleClientSettingsChange = () => { + setSettings (getClientBehaviorSettings ()) + } + + window.addEventListener (CLIENT_SETTINGS_EVENT, handleClientSettingsChange) + return () => + window.removeEventListener (CLIENT_SETTINGS_EVENT, handleClientSettingsChange) + }, []) + + return settings +} diff --git a/frontend/src/pages/posts/PostDetailPage.tsx b/frontend/src/pages/posts/PostDetailPage.tsx index cf541fe..01957df 100644 --- a/frontend/src/pages/posts/PostDetailPage.tsx +++ b/frontend/src/pages/posts/PostDetailPage.tsx @@ -15,7 +15,7 @@ import { SITE_TITLE } from '@/config' import { isApiError } from '@/lib/api' import { fetchPost, toggleViewedFlg } from '@/lib/posts' import { postsKeys, tagsKeys } from '@/lib/queryKeys' -import { getClientThumbnailMode } from '@/lib/settings' +import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' import { canEditContent } from '@/lib/users' import { cn } from '@/lib/utils' import NotFound from '@/pages/NotFound' @@ -30,8 +30,17 @@ type Props = { user: User | null } const PostDetailPage: FC = ({ user }) => { const editable = canEditContent (user) - const thumbnailMode = getClientThumbnailMode () + const behaviourSettings = useClientBehaviourSettings () + const thumbnailMode = behaviourSettings.thumbnailMode ?? 'normal' + const animationMode = behaviourSettings.animation ?? 'normal' const { id } = useParams () + const heroLayoutId = animationMode === 'off' ? undefined : `page-${ id }` + const heroTransition = + animationMode === 'off' + ? { duration: 0 } + : animationMode === 'reduced' + ? { duration: .08, ease: 'linear' as const } + : { duration: .2, ease: 'easeOut' as const } const postId = String (id ?? '') const postKey = postsKeys.show (postId) @@ -138,12 +147,12 @@ const PostDetailPage: FC = ({ user }) => { {thumbnailMode !== 'off' && (post.thumbnail || post.thumbnailBase) && ( + transition={heroTransition}> {post.title = (

-
-

プレビュー

-

- 現在の表示テーマ: {themeLabelById (draftActiveThemeId, draftCustomThemes)} -

-
- {CATEGORIES.map (category => ( - ))} -
-
-