import React, { useState, useEffect, useRef, useCallback } from 'react'; import { View, Text, TextInput, TouchableOpacity, ScrollView, StyleSheet, Switch, Platform } from 'react-native'; import { useDeviceStore } from '../stores/deviceStore'; import { SEND_FREQ_TABLE, SAMPLE_FREQ_TABLE, AMP_RATIO_TABLE, SOURCE_MODE_TABLE } from '../protocol/constants'; import { useTheme } from '../design/tokens'; interface PickerRowProps { label: string; options: readonly { code: number; label: string }[]; value: number; onChange: (code: number) => void; } const CHIP_W = 72; function PickerRow({ label, options, value, onChange }: PickerRowProps) { const theme = useTheme(); const scrollRef = useRef(null); const activeIdx = options.findIndex((o) => o.code === value); useEffect(() => { if (activeIdx > 0 && scrollRef.current) { scrollRef.current.scrollTo({ x: Math.max(0, activeIdx * CHIP_W - CHIP_W), animated: false }); } }, [activeIdx]); return ( {label} {options.map((o) => { const active = o.code === value; return ( onChange(o.code)} activeOpacity={0.7} > {o.label} ); })} ); } interface NumberRowProps { label: string; value: number; min: number; max: number; onCommit: (v: number) => void; suffix?: string; } // Keeps a local draft string while typing; commits a clamped integer on blur. function NumberRow({ label, value, min, max, onCommit, suffix }: NumberRowProps) { const theme = useTheme(); const [draft, setDraft] = useState(String(value)); // Sync when the store value changes externally (e.g. presets). useEffect(() => { setDraft(String(value)); }, [value]); const commit = () => { const n = parseInt(draft, 10); const clamped = isNaN(n) ? min : Math.max(min, Math.min(max, n)); setDraft(String(clamped)); if (clamped !== value) onCommit(clamped); }; const invalid = (() => { const n = parseInt(draft, 10); return isNaN(n) || n < min || n > max; })(); return ( {label} {suffix && {suffix}} {invalid && ( {min}–{max} )} ); } function SectionHeader({ title }: { title: string }) { const theme = useTheme(); return ( {title} ); } interface FormProps { onAnyChange?: () => void } export function ParamForm({ onAnyChange }: FormProps = {}) { const theme = useTheme(); const { config, updateConfig } = useDeviceStore(); const patch = (partial: Partial) => { updateConfig(partial); onAnyChange?.(); }; return ( {/* Channel count */} 通道数 {[1, 2, 3, 4, 5, 6].map((n) => { const active = config.channelNum === n; return ( patch({ channelNum: n })} activeOpacity={0.7} > {n} ); })} patch({ sendFreq: v })} /> patch({ sampleFreq: v })} /> { patch({ sampleDepth: v }); }} /> { patch({ accNum: v }); }} /> patch({ ampRatio: v })} /> patch({ sourceMode: v })} /> {/* Reverse accumulation */} 反向叠加 CH1-3 patch({ negAcc123: v })} thumbColor={config.negAcc123 ? theme.blue.fg : theme.text.muted} trackColor={{ false: theme.bg.border, true: theme.blue.border }} /> CH4-6 patch({ negAcc456: v })} thumbColor={config.negAcc456 ? theme.blue.fg : theme.text.muted} trackColor={{ false: theme.bg.border, true: theme.blue.border }} /> { patch({ compDisableDelay: v }); }} suffix="×50μs" /> {/* File prefix */} 文件前缀 patch({ filePrefix: v.slice(0, 15) })} maxLength={15} autoCapitalize="none" placeholderTextColor={theme.text.muted} /> ); } const S = StyleSheet.create({ container: {}, sectionHeader: { paddingHorizontal: 16, paddingTop: 14, paddingBottom: 6, }, sectionTitle: { fontSize: 9, fontWeight: '700', letterSpacing: 2, textTransform: 'uppercase', }, row: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 10, borderBottomWidth: StyleSheet.hairlineWidth, gap: 12, marginHorizontal: 0, }, rowLabel: { fontSize: 12, width: 68, flexShrink: 0, letterSpacing: 0.3 }, // Picker chips optScroll: { flex: 1 }, optChip: { borderWidth: 1, borderRadius: 6, paddingHorizontal: 9, paddingVertical: 4, marginRight: 5 }, optText: { fontSize: 11 }, // Number input inputWrap: { flexDirection: 'row', alignItems: 'center', flex: 1 }, input: { flex: 1, fontSize: 13, fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace', paddingVertical: 4, borderBottomWidth: 1, }, suffix: { fontSize: 10, marginLeft: 8 }, hint: { fontSize: 9, marginLeft: 4 }, // Channel chips channelRow: { flexDirection: 'row', gap: 6, flex: 1 }, chChip: { width: 30, height: 30, borderRadius: 15, borderWidth: 1, alignItems: 'center', justifyContent: 'center', }, chText: { fontSize: 12, fontWeight: '700' }, // Switch row switchRow: { flexDirection: 'row', alignItems: 'center', gap: 8, flex: 1 }, switchLabel: { fontSize: 12 }, });