このコミットが含まれているのは:
+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 (
|
||||
|
||||
@@ -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"
|
||||
|
||||
新しい課題から参照
ユーザをブロックする