f5b632ed89
Reviewed-on: #397 Co-authored-by: miteruzo <miteruzo@naver.com> Co-committed-by: miteruzo <miteruzo@naver.com>
199 行
5.9 KiB
TypeScript
199 行
5.9 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 } from '@/lib/settings'
|
|
|
|
type Props = {
|
|
sectionClassName: string
|
|
onDirtyStateChange?: (
|
|
dirty: boolean,
|
|
discard: () => void,
|
|
) => void }
|
|
|
|
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 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 px-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,
|
|
onDirtyStateChange },
|
|
) => {
|
|
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)
|
|
}
|
|
|
|
useEffect (() => {
|
|
onDirtyStateChange?.(hasUnsavedChanges, handleDiscard)
|
|
|
|
return () => {
|
|
onDirtyStateChange?.(false, () => { })
|
|
}
|
|
}, [handleDiscard, hasUnsavedChanges, onDirtyStateChange])
|
|
|
|
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>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
<Button
|
|
type="button"
|
|
variant="destructive"
|
|
disabled={hasUnsavedChanges === false}
|
|
onClick={handleDiscard}>
|
|
変更を破棄
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
disabled={hasUnsavedChanges === false}
|
|
onClick={handleSave}>
|
|
保存
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<SettingBlock title="アニメーション">
|
|
<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="タグの親子関係表示">
|
|
<SegmentedControl
|
|
value={draftSettings.tagRelationDisplay ?? 'grouped'}
|
|
options={tagRelationDisplayOptions}
|
|
onChange={value => updateDraft ('tagRelationDisplay', value)}/>
|
|
</SettingBlock>
|
|
|
|
<SettingBlock title="埋め込み自動読込">
|
|
<SegmentedControl
|
|
value={draftSettings.embedAutoLoad ?? 'auto'}
|
|
options={embedAutoLoadOptions}
|
|
onChange={value => updateDraft ('embedAutoLoad', value)}/>
|
|
</SettingBlock>
|
|
</div>
|
|
</section>)
|
|
}
|
|
|
|
export default BehaviourSettingsSection
|