forked from defunkt/toes
308 lines
9.4 KiB
TypeScript
308 lines
9.4 KiB
TypeScript
import { useEffect, useState } from 'hono/jsx'
|
|
import { connectToWifi, getWifiStatus, scanWifiNetworks } from '../api'
|
|
import { setCurrentView, setupMode } from '../state'
|
|
import {
|
|
Button,
|
|
DashboardInstallCmd,
|
|
ErrorBox,
|
|
FormActions,
|
|
FormField,
|
|
FormInput,
|
|
FormLabel,
|
|
HeaderActions,
|
|
InfoLabel,
|
|
InfoRow,
|
|
InfoValue,
|
|
Main,
|
|
MainContent,
|
|
MainHeader,
|
|
MainTitle,
|
|
NetworkItem,
|
|
NetworkListWrap,
|
|
NetworkMeta,
|
|
NetworkName,
|
|
Section,
|
|
SectionTitle,
|
|
SignalBarSegment,
|
|
SignalBarsWrap,
|
|
Spinner,
|
|
SpinnerWrap,
|
|
SuccessCheck,
|
|
WifiColumn,
|
|
} from '../styles'
|
|
import { theme } from '../themes'
|
|
import type { WifiNetwork } from '../../shared/types'
|
|
|
|
type WifiStep = 'status' | 'scanning' | 'networks' | 'password' | 'connecting' | 'success'
|
|
|
|
function signalBars(signal: number) {
|
|
const level = signal > 75 ? 4 : signal > 50 ? 3 : signal > 25 ? 2 : 1
|
|
return (
|
|
<SignalBarsWrap>
|
|
{[1, 2, 3, 4].map(i => (
|
|
<SignalBarSegment
|
|
key={i}
|
|
level={i <= level ? 'active' : 'inactive'}
|
|
style={{ height: 3 + i * 3 }}
|
|
/>
|
|
))}
|
|
</SignalBarsWrap>
|
|
)
|
|
}
|
|
|
|
function NetworkList({ networks, onSelect }: { networks: WifiNetwork[], onSelect: (net: WifiNetwork) => void }) {
|
|
if (networks.length === 0) {
|
|
return (
|
|
<SpinnerWrap>
|
|
No networks found. Try scanning again.
|
|
</SpinnerWrap>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<NetworkListWrap>
|
|
{networks.map(net => (
|
|
<NetworkItem key={net.ssid} onClick={() => onSelect(net)}>
|
|
<NetworkName>{net.ssid}</NetworkName>
|
|
<NetworkMeta>
|
|
{net.security && net.security !== '' && net.security !== '--' && <span style={{ fontSize: 12 }}>🔒</span>}
|
|
{signalBars(net.signal)}
|
|
</NetworkMeta>
|
|
</NetworkItem>
|
|
))}
|
|
</NetworkListWrap>
|
|
)
|
|
}
|
|
|
|
export function SettingsPage({ render }: { render: () => void }) {
|
|
const [step, setStep] = useState<WifiStep>('status')
|
|
const [connected, setConnected] = useState(false)
|
|
const [currentSsid, setCurrentSsid] = useState('')
|
|
const [currentIp, setCurrentIp] = useState('')
|
|
const [networks, setNetworks] = useState<WifiNetwork[]>([])
|
|
const [selectedNetwork, setSelectedNetwork] = useState<WifiNetwork | null>(null)
|
|
const [password, setPassword] = useState('')
|
|
const [error, setError] = useState('')
|
|
const [successSsid, setSuccessSsid] = useState('')
|
|
const [successIp, setSuccessIp] = useState('')
|
|
const [serverUrl, setServerUrl] = useState('')
|
|
|
|
const fetchStatus = () => {
|
|
getWifiStatus().then(status => {
|
|
setConnected(status.connected)
|
|
setCurrentSsid(status.ssid)
|
|
setCurrentIp(status.ip)
|
|
if (status.url) setServerUrl(status.url)
|
|
}).catch(() => {})
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchStatus()
|
|
if (setupMode) doScan()
|
|
}, [])
|
|
|
|
const goBack = () => {
|
|
setCurrentView('dashboard')
|
|
render()
|
|
}
|
|
|
|
const doScan = () => {
|
|
setStep('scanning')
|
|
setError('')
|
|
scanWifiNetworks()
|
|
.then(nets => {
|
|
setNetworks(nets)
|
|
setStep('networks')
|
|
})
|
|
.catch(() => {
|
|
setError('Failed to scan networks')
|
|
setStep('networks')
|
|
})
|
|
}
|
|
|
|
const handleSelectNetwork = (net: WifiNetwork) => {
|
|
setSelectedNetwork(net)
|
|
setPassword('')
|
|
setError('')
|
|
const needsPassword = net.security && net.security !== '' && net.security !== '--'
|
|
if (needsPassword) {
|
|
setStep('password')
|
|
} else {
|
|
doConnect(net.ssid)
|
|
}
|
|
}
|
|
|
|
const doConnect = (ssid: string, pw?: string) => {
|
|
setStep('connecting')
|
|
setError('')
|
|
connectToWifi(ssid, pw)
|
|
.then(result => {
|
|
if (result.ok) {
|
|
setSuccessSsid(result.ssid || ssid)
|
|
setSuccessIp(result.ip || '')
|
|
setStep('success')
|
|
fetchStatus()
|
|
} else {
|
|
setError(result.error || 'Connection failed. Check your password and try again.')
|
|
setStep('password')
|
|
}
|
|
})
|
|
.catch(() => {
|
|
setError('Connection failed. Please try again.')
|
|
setStep('password')
|
|
})
|
|
}
|
|
|
|
const handleConnect = (e: Event) => {
|
|
e.preventDefault()
|
|
if (!selectedNetwork) return
|
|
doConnect(selectedNetwork.ssid, password || undefined)
|
|
}
|
|
|
|
const title = setupMode ? 'WiFi Setup' : 'Settings'
|
|
|
|
return (
|
|
<Main>
|
|
<MainHeader centered>
|
|
<MainTitle>{title}</MainTitle>
|
|
{!setupMode && (
|
|
<HeaderActions>
|
|
<Button onClick={goBack}>Back</Button>
|
|
</HeaderActions>
|
|
)}
|
|
</MainHeader>
|
|
<MainContent centered>
|
|
<Section>
|
|
<SectionTitle>WiFi</SectionTitle>
|
|
|
|
{/* Status display */}
|
|
{step === 'status' && (
|
|
<WifiColumn>
|
|
<div>
|
|
<InfoRow>
|
|
<InfoLabel>Status</InfoLabel>
|
|
<InfoValue style={{ color: connected ? theme('colors-statusRunning') : theme('colors-error'), fontWeight: 500 }}>
|
|
{connected ? 'Connected' : 'Disconnected'}
|
|
</InfoValue>
|
|
</InfoRow>
|
|
{connected && currentSsid && (
|
|
<InfoRow>
|
|
<InfoLabel>Network</InfoLabel>
|
|
<InfoValue>{currentSsid}</InfoValue>
|
|
</InfoRow>
|
|
)}
|
|
{connected && currentIp && (
|
|
<InfoRow>
|
|
<InfoLabel>IP</InfoLabel>
|
|
<InfoValue style={{ fontFamily: theme('fonts-mono') }}>{currentIp}</InfoValue>
|
|
</InfoRow>
|
|
)}
|
|
</div>
|
|
<FormActions>
|
|
<Button onClick={doScan}>Scan Networks</Button>
|
|
</FormActions>
|
|
</WifiColumn>
|
|
)}
|
|
|
|
{/* Scanning spinner */}
|
|
{step === 'scanning' && (
|
|
<SpinnerWrap>
|
|
<Spinner />
|
|
<p style={{ color: theme('colors-textMuted') }}>Scanning for networks...</p>
|
|
</SpinnerWrap>
|
|
)}
|
|
|
|
{/* Network list */}
|
|
{step === 'networks' && (
|
|
<WifiColumn style={{ gap: 12 }}>
|
|
<NetworkList networks={networks} onSelect={handleSelectNetwork} />
|
|
{error && <ErrorBox>{error}</ErrorBox>}
|
|
<FormActions>
|
|
{!setupMode && <Button onClick={() => setStep('status')}>Back</Button>}
|
|
<Button onClick={doScan}>Rescan</Button>
|
|
</FormActions>
|
|
</WifiColumn>
|
|
)}
|
|
|
|
{/* Password entry */}
|
|
{step === 'password' && (
|
|
<WifiColumn>
|
|
<FormField>
|
|
<FormLabel>Network</FormLabel>
|
|
<div style={{ fontWeight: 500, fontSize: 16 }}>{selectedNetwork?.ssid}</div>
|
|
</FormField>
|
|
<form onSubmit={handleConnect} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
<FormField>
|
|
<FormLabel>Password</FormLabel>
|
|
<FormInput
|
|
type="password"
|
|
value={password}
|
|
onInput={(e: Event) => setPassword((e.target as HTMLInputElement).value)}
|
|
placeholder="Enter WiFi password"
|
|
autofocus
|
|
/>
|
|
</FormField>
|
|
{error && <ErrorBox>{error}</ErrorBox>}
|
|
<FormActions>
|
|
<Button onClick={() => { setError(''); setStep('networks') }}>Back</Button>
|
|
<Button variant="primary" type="submit">Connect</Button>
|
|
</FormActions>
|
|
</form>
|
|
</WifiColumn>
|
|
)}
|
|
|
|
{/* Connecting spinner */}
|
|
{step === 'connecting' && (
|
|
<SpinnerWrap>
|
|
<Spinner />
|
|
<p style={{ color: theme('colors-textMuted') }}>Connecting to <strong>{selectedNetwork?.ssid}</strong>...</p>
|
|
</SpinnerWrap>
|
|
)}
|
|
|
|
{/* Success */}
|
|
{step === 'success' && (
|
|
<WifiColumn style={{ textAlign: 'center' }}>
|
|
<SuccessCheck>✓</SuccessCheck>
|
|
<h3 style={{ fontSize: 20, fontWeight: 600, marginBottom: 8 }}>Connected!</h3>
|
|
<p style={{ color: theme('colors-textMuted'), marginBottom: 16 }}>
|
|
Connected to <strong>{successSsid}</strong>
|
|
{successIp && <span> ({successIp})</span>}
|
|
</p>
|
|
{setupMode ? (
|
|
<div style={{
|
|
marginTop: 20,
|
|
padding: 16,
|
|
background: 'var(--colors-bgSubtle)',
|
|
borderRadius: 'var(--radius-md)',
|
|
border: '1px solid var(--colors-border)',
|
|
}}>
|
|
<p style={{ marginBottom: 8 }}>Reconnect your device to <strong>{successSsid}</strong> and visit:</p>
|
|
<a
|
|
href={serverUrl}
|
|
style={{ color: theme('colors-statusRunning'), fontSize: 18, fontWeight: 600, textDecoration: 'none' }}
|
|
>
|
|
{serverUrl}
|
|
</a>
|
|
</div>
|
|
) : (
|
|
<FormActions>
|
|
<Button onClick={() => { fetchStatus(); setStep('status') }}>Done</Button>
|
|
</FormActions>
|
|
)}
|
|
</WifiColumn>
|
|
)}
|
|
</Section>
|
|
|
|
{!setupMode && (
|
|
<Section>
|
|
<SectionTitle>Install CLI</SectionTitle>
|
|
<DashboardInstallCmd>
|
|
curl -fsSL {location.origin}/install | bash
|
|
</DashboardInstallCmd>
|
|
</Section>
|
|
)}
|
|
</MainContent>
|
|
</Main>
|
|
)
|
|
}
|