131 lines
4.3 KiB
TypeScript
131 lines
4.3 KiB
TypeScript
import { useEffect, useState } from 'react'
|
||
import { api, ApiError } from '../api/client'
|
||
import {
|
||
connectionModeDescription,
|
||
connectionModeLabel,
|
||
connectionReadyLabel,
|
||
} from '../lib/connectionMode'
|
||
import type { ThreadsAccountConnectionData } from '../types/api'
|
||
import { useOnboarding } from '../onboarding/OnboardingContext'
|
||
import { AcLink, Badge, ChoiceCard, ErrorText, SectionTitle, SuccessText } from './ui'
|
||
|
||
type AccountConnectionModeProps = {
|
||
accountId: string
|
||
connectionsPath: string
|
||
}
|
||
|
||
export function AccountConnectionMode({ accountId, connectionsPath }: AccountConnectionModeProps) {
|
||
const { refresh: refreshOnboarding } = useOnboarding()
|
||
const [connection, setConnection] = useState<ThreadsAccountConnectionData | null>(null)
|
||
const [loading, setLoading] = useState(true)
|
||
const [busy, setBusy] = useState(false)
|
||
const [error, setError] = useState('')
|
||
const [message, setMessage] = useState('')
|
||
|
||
const load = async () => {
|
||
if (!accountId) return
|
||
setLoading(true)
|
||
setError('')
|
||
try {
|
||
const data = await api.get<ThreadsAccountConnectionData>(
|
||
`/api/v1/threads-accounts/${encodeURIComponent(accountId)}/connection`,
|
||
{ auth: true },
|
||
)
|
||
setConnection(data)
|
||
} catch (e) {
|
||
setError(e instanceof ApiError ? e.message : '載入連線設定失敗')
|
||
setConnection(null)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
load().catch(() => undefined)
|
||
}, [accountId])
|
||
|
||
const saveDevMode = async (devMode: boolean) => {
|
||
if (!accountId) return
|
||
setBusy(true)
|
||
setError('')
|
||
setMessage('')
|
||
try {
|
||
const data = await api.patch<ThreadsAccountConnectionData>(
|
||
`/api/v1/threads-accounts/${encodeURIComponent(accountId)}/connection`,
|
||
{ dev_mode: devMode },
|
||
{ auth: true },
|
||
)
|
||
setConnection(data)
|
||
setMessage(devMode ? '已切換為開發模式(爬蟲)' : '已切換為正式模式(API)')
|
||
await refreshOnboarding()
|
||
} catch (e) {
|
||
setError(e instanceof ApiError ? e.message : '儲存失敗')
|
||
} finally {
|
||
setBusy(false)
|
||
}
|
||
}
|
||
|
||
const prefs = connection?.prefs
|
||
const devMode = !!prefs?.dev_mode
|
||
|
||
return (
|
||
<div className="grid gap-4">
|
||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||
<div className="grid gap-2">
|
||
<SectionTitle>連線模式</SectionTitle>
|
||
<p className="text-sm leading-relaxed text-ink-secondary">
|
||
決定此帳號走 Threads 官方 API,或本機爬蟲。OAuth、Chrome 同步等細節在{' '}
|
||
<AcLink to={connectionsPath}>連線設定</AcLink>。
|
||
</p>
|
||
</div>
|
||
{connection ? (
|
||
<div className="flex flex-wrap gap-2">
|
||
<Badge tone={devMode ? 'warning' : 'brand'}>{connectionModeLabel(devMode)}</Badge>
|
||
<Badge
|
||
tone={
|
||
devMode
|
||
? connection.browser_connected
|
||
? 'success'
|
||
: 'warning'
|
||
: connection.api_connected
|
||
? 'success'
|
||
: 'warning'
|
||
}
|
||
>
|
||
{connectionReadyLabel(devMode, connection.browser_connected, connection.api_connected)}
|
||
</Badge>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
|
||
{loading ? (
|
||
<p className="text-sm text-muted">載入連線模式…</p>
|
||
) : prefs ? (
|
||
<>
|
||
<p className="text-sm text-ink-secondary">{connectionModeDescription(devMode)}</p>
|
||
<div className="grid gap-3 sm:grid-cols-2">
|
||
<ChoiceCard
|
||
title="正式模式(API)"
|
||
hint="搜尋、發文、留言走官方 API"
|
||
active={!devMode}
|
||
disabled={busy || !devMode}
|
||
onClick={() => saveDevMode(false)}
|
||
/>
|
||
<ChoiceCard
|
||
title="開發模式(爬蟲)"
|
||
hint="本機除錯用,需同步 Chrome Session"
|
||
active={devMode}
|
||
disabled={busy || devMode}
|
||
onClick={() => saveDevMode(true)}
|
||
/>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<p className="text-sm text-ink-secondary">無法載入連線設定。</p>
|
||
)}
|
||
|
||
<ErrorText message={error} />
|
||
<SuccessText message={message} />
|
||
</div>
|
||
)
|
||
} |