From 3b29c3e10ebe9351a4ada743646aa03c483aaf3e Mon Sep 17 00:00:00 2001 From: miteruzo Date: Sun, 5 Jul 2026 00:03:13 +0900 Subject: [PATCH] #34 --- frontend/src/lib/settings.ts | 41 ++++--- frontend/src/pages/posts/PostNewPage.tsx | 143 +++++------------------ 2 files changed, 54 insertions(+), 130 deletions(-) diff --git a/frontend/src/lib/settings.ts b/frontend/src/lib/settings.ts index f32db7b..dce8538 100644 --- a/frontend/src/lib/settings.ts +++ b/frontend/src/lib/settings.ts @@ -2,12 +2,9 @@ import { apiGet, apiPatch } from '@/lib/api' import type { FetchPostsOrder, FetchTagsOrder } from '@/types' -// DB-backed user settings. These are worth sharing across browsers for one user. +// DB-backed user settings. These are the cross-device preferences worth syncing. export type UserSettings = { - theme: 'system' | 'light' | 'dark' - autoFetchTitle: 'auto' | 'manual' | 'off' - autoFetchThumbnail: 'auto' | 'manual' | 'off' - wikiEditorMode: 'split' | 'write' | 'preview' } + theme: 'system' | 'light' | 'dark' } export type TheatreLayoutMode = 'threeColumns' | 'tagsBottom' | 'commentsBottom' export type TheatreTagFlow = 'vertical' | 'horizontal' @@ -24,10 +21,14 @@ type ClientListSettings = { limit?: ClientListLimit order?: string } -// Browser-local settings. These depend on device or screen context. +type ClientKeyboardSettings = { + shortcutsEnabled?: boolean } + +// Browser-local settings. These stay device-specific and are not synced to DB. export type ClientSettings = { panes?: Record lists?: Partial> + keyboard?: ClientKeyboardSettings theatre?: { layoutMode?: TheatreLayoutMode tagFlow?: TheatreTagFlow @@ -41,14 +42,9 @@ export type ClientSettings = { export const CLIENT_SETTINGS_STORAGE_KEY = 'btrc_hub.client_settings' export const DEFAULT_USER_SETTINGS: UserSettings = { - theme: 'system', - autoFetchTitle: 'manual', - autoFetchThumbnail: 'manual', - wikiEditorMode: 'split' } + theme: 'system' } export const THEME_OPTIONS = ['system', 'light', 'dark'] as const -export const AUTO_FETCH_OPTIONS = ['auto', 'manual', 'off'] as const -export const WIKI_EDITOR_MODE_OPTIONS = ['split', 'write', 'preview'] as const export const LIST_LIMIT_OPTIONS = [20, 50, 100] as const export const POST_LIST_ORDER_OPTIONS = [ 'title:asc', @@ -89,12 +85,7 @@ export const fetchUserSettings = async (): Promise => export const updateUserSettings = async ( - settings: Partial<{ - theme: UserSettings['theme'] - auto_fetch_title: UserSettings['autoFetchTitle'] - auto_fetch_thumbnail: UserSettings['autoFetchThumbnail'] - wiki_editor_mode: UserSettings['wikiEditorMode'] - }>, + settings: Partial<{ theme: UserSettings['theme'] }>, ): Promise => await apiPatch ('/users/settings', settings) @@ -171,6 +162,20 @@ export const setClientListSettings = ( } +export const getClientKeyboardShortcutsEnabled = (): boolean => + loadClientSettings ().keyboard?.shortcutsEnabled ?? true + + +export const setClientKeyboardShortcutsEnabled = ( + enabled: boolean, +): void => { + updateClientSettings (settings => ({ + ...settings, + keyboard: { ...(settings.keyboard ?? { }), shortcutsEnabled: enabled }, + })) +} + + const legacyTheatreLayoutMode = (): TheatreLayoutMode | null => { const value = localStorage.getItem (LEGACY_THEATRE_LAYOUT_STORAGE_KEY) return ( diff --git a/frontend/src/pages/posts/PostNewPage.tsx b/frontend/src/pages/posts/PostNewPage.tsx index dca39c4..14c1b50 100644 --- a/frontend/src/pages/posts/PostNewPage.tsx +++ b/frontend/src/pages/posts/PostNewPage.tsx @@ -9,7 +9,6 @@ import Form from '@/components/common/Form' import FormField from '@/components/common/FormField' import PageTitle from '@/components/common/PageTitle' import MainArea from '@/components/layout/MainArea' -import { useUserSettings } from '@/components/users/UserSettingsProvider' import { Button } from '@/components/ui/button' import { toast } from '@/components/ui/use-toast' import { SITE_TITLE } from '@/config' @@ -31,7 +30,6 @@ type PostFormField = const PostNewPage: FC = ({ user }) => { const editable = canEditContent (user) - const { settings } = useUserSettings () const navigate = useNavigate () @@ -49,16 +47,8 @@ const PostNewPage: FC = ({ user }) => { const [title, setTitle] = useState ('') const [titleLoading, setTitleLoading] = useState (false) const [url, setURL] = useState ('') - const [titleAutoFlg, setTitleAutoFlg] = useState (settings.autoFetchTitle === 'auto') - const [thumbnailAutoFlg, setThumbnailAutoFlg] = - useState (settings.autoFetchThumbnail === 'auto') - const previousURLRef = useRef ('') const thumbnailPreviewRef = useRef ('') - const titleFetchMode = settings.autoFetchTitle - const thumbnailFetchMode = settings.autoFetchThumbnail - const titleFetchVisible = titleFetchMode !== 'off' - const thumbnailFetchVisible = thumbnailFetchMode !== 'off' const videoFlg = useMemo (() => tags.split (/\s+/).some (tag => tag.replace (/\[.*\]$/, '') === '動画'), [tags]) @@ -93,19 +83,7 @@ const PostNewPage: FC = ({ user }) => { } } - const handleURLBlur = () => { - if (!(url) || url === previousURLRef.current) - return - - if (titleAutoFlg) - fetchTitle () - if (thumbnailAutoFlg) - fetchThumbnail () - previousURLRef.current = url - } - const fetchTitle = useCallback (async () => { - setTitle ('') setTitleLoading (true) try { @@ -144,24 +122,6 @@ const PostNewPage: FC = ({ user }) => { thumbnailPreviewRef.current = thumbnailPreview }, [thumbnailPreview]) - useEffect (() => { - setTitleAutoFlg (settings.autoFetchTitle === 'auto') - }, [settings.autoFetchTitle]) - - useEffect (() => { - setThumbnailAutoFlg (settings.autoFetchThumbnail === 'auto') - }, [settings.autoFetchThumbnail]) - - useEffect (() => { - if (titleAutoFlg && url) - fetchTitle () - }, [fetchTitle, titleAutoFlg, url]) - - useEffect (() => { - if (thumbnailAutoFlg && url) - fetchThumbnail () - }, [fetchThumbnail, thumbnailAutoFlg, url]) - if (!(editable)) return @@ -183,8 +143,7 @@ const PostNewPage: FC = ({ user }) => { onChange={e => setURL (e.target.value)} aria-describedby={describedBy} aria-invalid={invalid} - className={inputClass (invalid)} - onBlur={handleURLBlur}/>)} + className={inputClass (invalid)}/>)} {/* タイトル */} @@ -200,31 +159,14 @@ const PostNewPage: FC = ({ user }) => { onChange={ev => setTitle (ev.target.value)} disabled={titleLoading}/>
- - {titleAutoFlg - ? 'URL 入力時に自動取得します.' - : titleFetchMode === 'manual' - ? '自動取得しません.必要なら手動で取得できます.' - : '取得機能は無効です.'} - - {titleFetchVisible && ( - <> - - {!(titleAutoFlg) && ( - )} - )} + 必要なタイミングで URL から取得できます. +
)} @@ -234,52 +176,29 @@ const PostNewPage: FC = ({ user }) => { {({ describedBy, invalid }) => ( <>
- - {thumbnailAutoFlg - ? 'URL 入力時に自動取得します.' - : thumbnailFetchMode === 'manual' - ? '自動取得しません.必要なら手動で取得できます.' - : '取得機能は無効です.'} - - {thumbnailFetchVisible && ( - <> - - {!(thumbnailAutoFlg) && ( - )} - )} + 必要なタイミングで URL から取得できます. +
- {thumbnailAutoFlg - ? (thumbnailLoading - ?

Loading...

- : !(thumbnailPreview) && ( -

- URL から自動取得されます。 -

)) - : ( - { - const file = e.target.files?.[0] - if (file) - { - setThumbnailFile (file) - setThumbnailPreview (URL.createObjectURL (file)) - } - }}/>)} + {thumbnailLoading && ( +

Loading...

)} + { + const file = e.target.files?.[0] + if (file) + { + setThumbnailFile (file) + setThumbnailPreview (URL.createObjectURL (file)) + } + }}/> {thumbnailPreview && (