このコミットが含まれているのは:
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'
// 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<string, ClientPaneSettings>
lists?: Partial<Record<ClientListKey, ClientListSettings>>
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<UserSettings> =>
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<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 value = localStorage.getItem (LEGACY_THEATRE_LAYOUT_STORAGE_KEY)
return (
+31 -112
ファイルの表示
@@ -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<Props> = ({ user }) => {
const editable = canEditContent (user)
const { settings } = useUserSettings ()
const navigate = useNavigate ()
@@ -49,16 +47,8 @@ const PostNewPage: FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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 <Forbidden/>
@@ -183,8 +143,7 @@ const PostNewPage: FC<Props> = ({ user }) => {
onChange={e => setURL (e.target.value)}
aria-describedby={describedBy}
aria-invalid={invalid}
className={inputClass (invalid)}
onBlur={handleURLBlur}/>)}
className={inputClass (invalid)}/>)}
</FormField>
{/* タイトル */}
@@ -200,31 +159,14 @@ const PostNewPage: FC<Props> = ({ user }) => {
onChange={ev => setTitle (ev.target.value)}
disabled={titleLoading}/>
<div className="flex flex-wrap items-center gap-2 text-sm">
<span>
{titleAutoFlg
? 'URL 入力時に自動取得します.'
: titleFetchMode === 'manual'
? '自動取得しません.必要なら手動で取得できます.'
: '取得機能は無効です.'}
</span>
{titleFetchVisible && (
<>
<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>)}
</>)}
<span> URL </span>
<Button
type="button"
variant="outline"
onClick={() => void fetchTitle ()}
disabled={!(url) || titleLoading}>
</Button>
</div>
</div>)}
</FormField>
@@ -234,52 +176,29 @@ const PostNewPage: FC<Props> = ({ user }) => {
{({ describedBy, invalid }) => (
<>
<div className="mb-2 flex flex-wrap items-center gap-2 text-sm">
<span>
{thumbnailAutoFlg
? 'URL 入力時に自動取得します.'
: thumbnailFetchMode === 'manual'
? '自動取得しません.必要なら手動で取得できます.'
: '取得機能は無効です.'}
</span>
{thumbnailFetchVisible && (
<>
<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>)}
</>)}
<span> URL </span>
<Button
type="button"
variant="outline"
onClick={() => void fetchThumbnail ()}
disabled={!(url) || thumbnailLoading}>
</Button>
</div>
{thumbnailAutoFlg
? (thumbnailLoading
? <p className="text-gray-500 text-sm">Loading...</p>
: !(thumbnailPreview) && (
<p className="text-gray-500 text-sm">
URL
</p>))
: (
<input type="file"
accept="image/*"
aria-describedby={describedBy}
aria-invalid={invalid}
onChange={e => {
const file = e.target.files?.[0]
if (file)
{
setThumbnailFile (file)
setThumbnailPreview (URL.createObjectURL (file))
}
}}/>)}
{thumbnailLoading && (
<p className="text-gray-500 text-sm">Loading...</p>)}
<input type="file"
accept="image/*"
aria-describedby={describedBy}
aria-invalid={invalid}
onChange={e => {
const file = e.target.files?.[0]
if (file)
{
setThumbnailFile (file)
setThumbnailPreview (URL.createObjectURL (file))
}
}}/>
{thumbnailPreview && (
<img src={thumbnailPreview}
alt="preview"