このコミットが含まれているのは:
2026-07-05 00:03:13 +09:00
コミット 3b29c3e10e
2個のファイルの変更54行の追加130行の削除
+23 -18
ファイルの表示
@@ -2,12 +2,9 @@ import { apiGet, apiPatch } from '@/lib/api'
import type { FetchPostsOrder, FetchTagsOrder } from '@/types' 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 = { export type UserSettings = {
theme: 'system' | 'light' | 'dark' theme: 'system' | 'light' | 'dark' }
autoFetchTitle: 'auto' | 'manual' | 'off'
autoFetchThumbnail: 'auto' | 'manual' | 'off'
wikiEditorMode: 'split' | 'write' | 'preview' }
export type TheatreLayoutMode = 'threeColumns' | 'tagsBottom' | 'commentsBottom' export type TheatreLayoutMode = 'threeColumns' | 'tagsBottom' | 'commentsBottom'
export type TheatreTagFlow = 'vertical' | 'horizontal' export type TheatreTagFlow = 'vertical' | 'horizontal'
@@ -24,10 +21,14 @@ type ClientListSettings = {
limit?: ClientListLimit limit?: ClientListLimit
order?: string } 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 = { export type ClientSettings = {
panes?: Record<string, ClientPaneSettings> panes?: Record<string, ClientPaneSettings>
lists?: Partial<Record<ClientListKey, ClientListSettings>> lists?: Partial<Record<ClientListKey, ClientListSettings>>
keyboard?: ClientKeyboardSettings
theatre?: { theatre?: {
layoutMode?: TheatreLayoutMode layoutMode?: TheatreLayoutMode
tagFlow?: TheatreTagFlow tagFlow?: TheatreTagFlow
@@ -41,14 +42,9 @@ export type ClientSettings = {
export const CLIENT_SETTINGS_STORAGE_KEY = 'btrc_hub.client_settings' export const CLIENT_SETTINGS_STORAGE_KEY = 'btrc_hub.client_settings'
export const DEFAULT_USER_SETTINGS: UserSettings = { export const DEFAULT_USER_SETTINGS: UserSettings = {
theme: 'system', theme: 'system' }
autoFetchTitle: 'manual',
autoFetchThumbnail: 'manual',
wikiEditorMode: 'split' }
export const THEME_OPTIONS = ['system', 'light', 'dark'] as const 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 LIST_LIMIT_OPTIONS = [20, 50, 100] as const
export const POST_LIST_ORDER_OPTIONS = [ export const POST_LIST_ORDER_OPTIONS = [
'title:asc', 'title:asc',
@@ -89,12 +85,7 @@ export const fetchUserSettings = async (): Promise<UserSettings> =>
export const updateUserSettings = async ( export const updateUserSettings = async (
settings: Partial<{ settings: Partial<{ theme: UserSettings['theme'] }>,
theme: UserSettings['theme']
auto_fetch_title: UserSettings['autoFetchTitle']
auto_fetch_thumbnail: UserSettings['autoFetchThumbnail']
wiki_editor_mode: UserSettings['wikiEditorMode']
}>,
): Promise<UserSettings> => await apiPatch<UserSettings> ('/users/settings', settings) ): Promise<UserSettings> => await apiPatch<UserSettings> ('/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 legacyTheatreLayoutMode = (): TheatreLayoutMode | null => {
const value = localStorage.getItem (LEGACY_THEATRE_LAYOUT_STORAGE_KEY) const value = localStorage.getItem (LEGACY_THEATRE_LAYOUT_STORAGE_KEY)
return ( return (
+31 -112
ファイルの表示
@@ -9,7 +9,6 @@ import Form from '@/components/common/Form'
import FormField from '@/components/common/FormField' import FormField from '@/components/common/FormField'
import PageTitle from '@/components/common/PageTitle' import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea' import MainArea from '@/components/layout/MainArea'
import { useUserSettings } from '@/components/users/UserSettingsProvider'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast' import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config' import { SITE_TITLE } from '@/config'
@@ -31,7 +30,6 @@ type PostFormField =
const PostNewPage: FC<Props> = ({ user }) => { const PostNewPage: FC<Props> = ({ user }) => {
const editable = canEditContent (user) const editable = canEditContent (user)
const { settings } = useUserSettings ()
const navigate = useNavigate () const navigate = useNavigate ()
@@ -49,16 +47,8 @@ const PostNewPage: FC<Props> = ({ user }) => {
const [title, setTitle] = useState ('') const [title, setTitle] = useState ('')
const [titleLoading, setTitleLoading] = useState (false) const [titleLoading, setTitleLoading] = useState (false)
const [url, setURL] = useState ('') 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 thumbnailPreviewRef = useRef ('')
const titleFetchMode = settings.autoFetchTitle
const thumbnailFetchMode = settings.autoFetchThumbnail
const titleFetchVisible = titleFetchMode !== 'off'
const thumbnailFetchVisible = thumbnailFetchMode !== 'off'
const videoFlg = const videoFlg =
useMemo (() => tags.split (/\s+/).some (tag => tag.replace (/\[.*\]$/, '') === '動画'), useMemo (() => tags.split (/\s+/).some (tag => tag.replace (/\[.*\]$/, '') === '動画'),
[tags]) [tags])
@@ -93,19 +83,7 @@ const PostNewPage: FC<Props> = ({ user }) => {
} }
} }
const handleURLBlur = () => {
if (!(url) || url === previousURLRef.current)
return
if (titleAutoFlg)
fetchTitle ()
if (thumbnailAutoFlg)
fetchThumbnail ()
previousURLRef.current = url
}
const fetchTitle = useCallback (async () => { const fetchTitle = useCallback (async () => {
setTitle ('')
setTitleLoading (true) setTitleLoading (true)
try try
{ {
@@ -144,24 +122,6 @@ const PostNewPage: FC<Props> = ({ user }) => {
thumbnailPreviewRef.current = thumbnailPreview thumbnailPreviewRef.current = thumbnailPreview
}, [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)) if (!(editable))
return <Forbidden/> return <Forbidden/>
@@ -183,8 +143,7 @@ const PostNewPage: FC<Props> = ({ user }) => {
onChange={e => setURL (e.target.value)} onChange={e => setURL (e.target.value)}
aria-describedby={describedBy} aria-describedby={describedBy}
aria-invalid={invalid} aria-invalid={invalid}
className={inputClass (invalid)} className={inputClass (invalid)}/>)}
onBlur={handleURLBlur}/>)}
</FormField> </FormField>
{/* タイトル */} {/* タイトル */}
@@ -200,31 +159,14 @@ const PostNewPage: FC<Props> = ({ user }) => {
onChange={ev => setTitle (ev.target.value)} onChange={ev => setTitle (ev.target.value)}
disabled={titleLoading}/> disabled={titleLoading}/>
<div className="flex flex-wrap items-center gap-2 text-sm"> <div className="flex flex-wrap items-center gap-2 text-sm">
<span> <span> URL </span>
{titleAutoFlg <Button
? 'URL 入力時に自動取得します.' type="button"
: titleFetchMode === 'manual' variant="outline"
? '自動取得しません.必要なら手動で取得できます.' onClick={() => void fetchTitle ()}
: '取得機能は無効です.'} disabled={!(url) || titleLoading}>
</span>
{titleFetchVisible && ( </Button>
<>
<label className="flex items-center gap-1">
<input
type="checkbox"
checked={titleAutoFlg}
onChange={ev => setTitleAutoFlg (ev.target.checked)}/>
<span></span>
</label>
{!(titleAutoFlg) && (
<Button
type="button"
variant="outline"
onClick={() => void fetchTitle ()}
disabled={!(url) || titleLoading}>
</Button>)}
</>)}
</div> </div>
</div>)} </div>)}
</FormField> </FormField>
@@ -234,52 +176,29 @@ const PostNewPage: FC<Props> = ({ user }) => {
{({ describedBy, invalid }) => ( {({ describedBy, invalid }) => (
<> <>
<div className="mb-2 flex flex-wrap items-center gap-2 text-sm"> <div className="mb-2 flex flex-wrap items-center gap-2 text-sm">
<span> <span> URL </span>
{thumbnailAutoFlg <Button
? 'URL 入力時に自動取得します.' type="button"
: thumbnailFetchMode === 'manual' variant="outline"
? '自動取得しません.必要なら手動で取得できます.' onClick={() => void fetchThumbnail ()}
: '取得機能は無効です.'} disabled={!(url) || thumbnailLoading}>
</span>
{thumbnailFetchVisible && ( </Button>
<>
<label className="flex items-center gap-1">
<input
type="checkbox"
checked={thumbnailAutoFlg}
onChange={ev => setThumbnailAutoFlg (ev.target.checked)}/>
<span></span>
</label>
{!(thumbnailAutoFlg) && (
<Button
type="button"
variant="outline"
onClick={() => void fetchThumbnail ()}
disabled={!(url) || thumbnailLoading}>
</Button>)}
</>)}
</div> </div>
{thumbnailAutoFlg {thumbnailLoading && (
? (thumbnailLoading <p className="text-gray-500 text-sm">Loading...</p>)}
? <p className="text-gray-500 text-sm">Loading...</p> <input type="file"
: !(thumbnailPreview) && ( accept="image/*"
<p className="text-gray-500 text-sm"> aria-describedby={describedBy}
URL aria-invalid={invalid}
</p>)) onChange={e => {
: ( const file = e.target.files?.[0]
<input type="file" if (file)
accept="image/*" {
aria-describedby={describedBy} setThumbnailFile (file)
aria-invalid={invalid} setThumbnailPreview (URL.createObjectURL (file))
onChange={e => { }
const file = e.target.files?.[0] }}/>
if (file)
{
setThumbnailFile (file)
setThumbnailPreview (URL.createObjectURL (file))
}
}}/>)}
{thumbnailPreview && ( {thumbnailPreview && (
<img src={thumbnailPreview} <img src={thumbnailPreview}
alt="preview" alt="preview"