210 行
6.7 KiB
TypeScript
210 行
6.7 KiB
TypeScript
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 { cn } from '@/lib/utils'
|
|
|
|
import type { FC, ReactNode } from 'react'
|
|
|
|
import type { ClientAnimationMode,
|
|
ClientBehaviorSettings,
|
|
ClientEmbedAutoLoadMode,
|
|
ClientLinkPreloadMode,
|
|
ClientTagRelationDisplayMode,
|
|
ClientThumbnailMode } from '@/lib/settings'
|
|
|
|
type Props = {
|
|
sectionClassName: string }
|
|
|
|
type SegmentedOption<T extends string> = {
|
|
value: T
|
|
label: string }
|
|
|
|
type SegmentedControlProps<T extends string> = {
|
|
value: T
|
|
options: SegmentedOption<T>[]
|
|
onChange: (value: T) => void }
|
|
|
|
type SettingBlockProps = {
|
|
title: string
|
|
description: string
|
|
children: ReactNode }
|
|
|
|
const animationOptions: SegmentedOption<ClientAnimationMode>[] = [
|
|
{ value: 'off', label: 'なし' },
|
|
{ value: 'reduced', label: '控えめ' },
|
|
{ value: 'normal', label: '通常' }]
|
|
|
|
const embedAutoLoadOptions: SegmentedOption<ClientEmbedAutoLoadMode>[] = [
|
|
{ value: 'off', label: '読み込まない' },
|
|
{ value: 'manual', label: 'クリック時' },
|
|
{ value: 'auto', label: '自動' }]
|
|
|
|
const linkPreloadOptions: SegmentedOption<ClientLinkPreloadMode>[] = [
|
|
{ value: 'off', label: 'しない' },
|
|
{ value: 'intent', label: 'マウスを置いたら' }]
|
|
|
|
const tagRelationDisplayOptions: SegmentedOption<ClientTagRelationDisplayMode>[] = [
|
|
{ value: 'flat', label: 'まとめない' },
|
|
{ value: 'grouped', label: 'まとめる' }]
|
|
|
|
const thumbnailModeOptions: SegmentedOption<ClientThumbnailMode>[] = [
|
|
{ value: 'off', label: '非表示' },
|
|
{ value: 'light', label: '軽量' },
|
|
{ value: 'normal', label: '通常' }]
|
|
|
|
|
|
const SegmentedControl = <T extends string,> (
|
|
{ value, options, onChange }: SegmentedControlProps<T>,
|
|
) => {
|
|
return (
|
|
<div
|
|
role="radiogroup"
|
|
className="flex flex-wrap gap-2 rounded-xl border border-border bg-muted/40 p-1">
|
|
{options.map (option => (
|
|
<button
|
|
key={option.value}
|
|
type="button"
|
|
role="radio"
|
|
aria-checked={value === option.value}
|
|
className={cn (
|
|
'min-w-0 flex-1 rounded-lg px-3 py-2 text-sm font-medium',
|
|
'transition-colors focus-visible:outline-none focus-visible:ring-2',
|
|
'focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
value === option.value
|
|
? 'bg-primary text-primary-foreground shadow-sm ring-1 ring-primary/40'
|
|
: 'bg-transparent text-muted-foreground hover:bg-background/70 hover:text-foreground',
|
|
)}
|
|
onClick={() => onChange (option.value)}>
|
|
{option.label}
|
|
</button>))}
|
|
</div>)
|
|
}
|
|
|
|
|
|
const SettingBlock: FC<SettingBlockProps> = (
|
|
{ title, description, children },
|
|
) => (
|
|
<div className="space-y-3 rounded-xl border border-border/70 bg-background/70 p-4">
|
|
<div className="space-y-1">
|
|
<h3 className="text-sm font-semibold">{title}</h3>
|
|
<p className="text-sm text-muted-foreground">{description}</p>
|
|
</div>
|
|
{children}
|
|
</div>)
|
|
|
|
|
|
const BehaviourSettingsSection: FC<Props> = ({ sectionClassName }) => {
|
|
const [savedSettings, setSavedSettings] =
|
|
useState<ClientBehaviorSettings> (() => getClientBehaviorSettings ())
|
|
const [draftSettings, setDraftSettings] =
|
|
useState<ClientBehaviorSettings> (() => getClientBehaviorSettings ())
|
|
|
|
useEffect (() => {
|
|
const current = getClientBehaviorSettings ()
|
|
setSavedSettings (current)
|
|
setDraftSettings (current)
|
|
}, [])
|
|
|
|
const hasUnsavedChanges = useMemo (
|
|
() => JSON.stringify (draftSettings) !== JSON.stringify (savedSettings),
|
|
[draftSettings, savedSettings])
|
|
|
|
const updateDraft = <Key extends keyof ClientBehaviorSettings,> (
|
|
key: Key,
|
|
value: ClientBehaviorSettings[Key],
|
|
) => {
|
|
setDraftSettings (current => ({ ...current, [key]: value }))
|
|
}
|
|
|
|
const handleSave = () => {
|
|
const next = setClientBehaviorSettings (draftSettings)
|
|
setSavedSettings (next)
|
|
setDraftSettings (next)
|
|
toast ({ title: '動作設定を保存しました' })
|
|
}
|
|
|
|
const handleDiscard = () => {
|
|
setDraftSettings (savedSettings)
|
|
}
|
|
|
|
return (
|
|
<section className={sectionClassName}>
|
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
<div className="space-y-1">
|
|
<h2 className="text-xl font-bold">動作</h2>
|
|
{hasUnsavedChanges && (
|
|
<p className="text-sm text-amber-700 dark:text-amber-300">
|
|
未保存の変更があります
|
|
</p>)}
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
disabled={hasUnsavedChanges === false}
|
|
onClick={handleDiscard}>
|
|
変更を破棄
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
disabled={hasUnsavedChanges === false}
|
|
onClick={handleSave}>
|
|
保存
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<SettingBlock
|
|
title="アニメーション"
|
|
description="画面遷移やレイアウト移動の動きを調整します。">
|
|
<SegmentedControl
|
|
value={draftSettings.animation ?? 'normal'}
|
|
options={animationOptions}
|
|
onChange={value => updateDraft ('animation', value)}/>
|
|
</SettingBlock>
|
|
|
|
<SettingBlock
|
|
title="リンク先の先読み"
|
|
description="リンクにマウスを置いた時などに、移動先の情報を先に読みます。通信量や意図しない読込みが気になる場合は「しない」にできます。">
|
|
<SegmentedControl
|
|
value={draftSettings.linkPreload ?? 'intent'}
|
|
options={linkPreloadOptions}
|
|
onChange={value => updateDraft ('linkPreload', value)}/>
|
|
</SettingBlock>
|
|
|
|
<SettingBlock
|
|
title="タグの親子関係表示"
|
|
description="親子関係があるタグを、まとまりとして表示します。見た目を単純にしたい場合は「まとめない」にできます。">
|
|
<SegmentedControl
|
|
value={draftSettings.tagRelationDisplay ?? 'grouped'}
|
|
options={tagRelationDisplayOptions}
|
|
onChange={value => updateDraft ('tagRelationDisplay', value)}/>
|
|
</SettingBlock>
|
|
|
|
<SettingBlock
|
|
title="埋め込み自動読込"
|
|
description="外部埋め込みを自動で読み込むかを切り替えます。">
|
|
<SegmentedControl
|
|
value={draftSettings.embedAutoLoad ?? 'auto'}
|
|
options={embedAutoLoadOptions}
|
|
onChange={value => updateDraft ('embedAutoLoad', value)}/>
|
|
</SettingBlock>
|
|
|
|
<SettingBlock
|
|
title="サムネイル表示"
|
|
description="投稿一覧や詳細の画像表示を軽量化します。">
|
|
<SegmentedControl
|
|
value={draftSettings.thumbnailMode ?? 'normal'}
|
|
options={thumbnailModeOptions}
|
|
onChange={value => updateDraft ('thumbnailMode', value)}/>
|
|
</SettingBlock>
|
|
</div>
|
|
</section>)
|
|
}
|
|
|
|
export default BehaviourSettingsSection
|