|
1 | | -import { |
2 | | - View, |
3 | | - Text, |
4 | | - TextInput, |
5 | | - TouchableOpacity, |
6 | | - StyleSheet, |
7 | | - Platform, |
8 | | -} from 'react-native'; |
9 | | -import { router } from 'expo-router'; |
10 | | -import { useState } from 'react'; |
11 | | -import { useSafeAreaInsets } from 'react-native-safe-area-context'; |
12 | | -import type { RecordedHaptic } from '../src/types/recording'; |
13 | | -import { useRecorder } from '../src/contexts/RecorderContext'; |
14 | | -import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'; |
15 | | -import { recordedHapticSchema } from '../src/schemas/recordingSchema'; |
16 | | -import { ZodError } from 'zod'; |
17 | | -import { hapticEventsToRecordingEvents } from '../src/utils/hapticPlayback'; |
18 | | -import type { HapticEvent } from 'react-native-ahaps'; |
| 1 | +import { ImportModal } from '../src/screens/ImportModal'; |
19 | 2 |
|
20 | | -const getDuration = (events: HapticEvent[]) => { |
21 | | - return events.reduce((maxTime, event) => { |
22 | | - const eventEnd = |
23 | | - event.type === 'continuous' |
24 | | - ? event.relativeTime + event.duration |
25 | | - : event.relativeTime; |
26 | | - return Math.max(maxTime, eventEnd); |
27 | | - }, 0); |
28 | | -}; |
29 | | - |
30 | | -export default function ImportModal() { |
31 | | - const insets = useSafeAreaInsets(); |
32 | | - const { importRecording } = useRecorder(); |
33 | | - const [title, setTitle] = useState(''); |
34 | | - const [jsonText, setJsonText] = useState(''); |
35 | | - const [error, setError] = useState(''); |
36 | | - |
37 | | - const handleImport = () => { |
38 | | - setError(''); |
39 | | - |
40 | | - // Validate title |
41 | | - if (!title.trim()) { |
42 | | - setError('Please enter a title'); |
43 | | - return; |
44 | | - } |
45 | | - |
46 | | - // Validate and parse JSON |
47 | | - try { |
48 | | - const parsed = JSON.parse(jsonText); |
49 | | - |
50 | | - // Validate using Zod schema |
51 | | - const validationResult = recordedHapticSchema.safeParse(parsed); |
52 | | - |
53 | | - if (!validationResult.success) { |
54 | | - // Format Zod errors for user-friendly display |
55 | | - const zodError = validationResult.error; |
56 | | - const firstError = zodError.issues[0]; |
57 | | - const errorPath = firstError?.path.join('.') || 'unknown field'; |
58 | | - const errorMessage = firstError?.message || 'Invalid format'; |
59 | | - setError(`Validation error at ${errorPath}: ${errorMessage}`); |
60 | | - return; |
61 | | - } |
62 | | - |
63 | | - const { events, curves } = validationResult.data; |
64 | | - |
65 | | - const duration = getDuration(events); |
66 | | - |
67 | | - const recordingEvents = hapticEventsToRecordingEvents(events, curves); |
68 | | - |
69 | | - const recording: RecordedHaptic = { |
70 | | - id: Date.now().toString(), |
71 | | - name: title, |
72 | | - createdAt: Date.now(), |
73 | | - duration, |
74 | | - events, |
75 | | - curves, |
76 | | - recordingEvents, |
77 | | - }; |
78 | | - |
79 | | - // Import the recording |
80 | | - importRecording(recording); |
81 | | - |
82 | | - // Navigate back |
83 | | - router.back(); |
84 | | - } catch (err) { |
85 | | - if (err instanceof ZodError) { |
86 | | - setError('Invalid recording format. Please check your JSON.'); |
87 | | - } else if (err instanceof SyntaxError) { |
88 | | - setError('Invalid JSON format. Please check your input.'); |
89 | | - } else { |
90 | | - setError('An error occurred while importing the recording.'); |
91 | | - } |
92 | | - console.error('Import error:', err); |
93 | | - } |
94 | | - }; |
95 | | - |
96 | | - const isPresented = router.canGoBack(); |
97 | | - |
98 | | - return ( |
99 | | - <KeyboardAwareScrollView |
100 | | - style={styles.container} |
101 | | - keyboardShouldPersistTaps="handled" |
102 | | - > |
103 | | - <View style={[styles.header, { paddingTop: insets.top + 16 }]}> |
104 | | - <Text style={styles.headerTitle}>Import Recording</Text> |
105 | | - {isPresented && ( |
106 | | - <TouchableOpacity |
107 | | - onPress={() => router.back()} |
108 | | - style={styles.closeButton} |
109 | | - > |
110 | | - <Text style={styles.closeButtonText}>✕</Text> |
111 | | - </TouchableOpacity> |
112 | | - )} |
113 | | - </View> |
114 | | - |
115 | | - <View style={styles.content}> |
116 | | - <View style={styles.inputGroup}> |
117 | | - <Text style={styles.label}>Title</Text> |
118 | | - <TextInput |
119 | | - style={styles.titleInput} |
120 | | - value={title} |
121 | | - autoFocus |
122 | | - onChangeText={setTitle} |
123 | | - placeholder="Enter recording name" |
124 | | - placeholderTextColor="#636366" |
125 | | - autoCapitalize="words" |
126 | | - /> |
127 | | - </View> |
128 | | - |
129 | | - <View style={styles.inputGroup}> |
130 | | - <Text style={styles.label}>JSON Data</Text> |
131 | | - <TextInput |
132 | | - style={styles.jsonInput} |
133 | | - value={jsonText} |
134 | | - onChangeText={setJsonText} |
135 | | - placeholder="Paste recording JSON here" |
136 | | - placeholderTextColor="#636366" |
137 | | - multiline |
138 | | - textAlignVertical="top" |
139 | | - autoCapitalize="none" |
140 | | - autoCorrect={false} |
141 | | - /> |
142 | | - </View> |
143 | | - |
144 | | - {error ? <Text style={styles.errorText}>{error}</Text> : null} |
145 | | - |
146 | | - <TouchableOpacity |
147 | | - style={[ |
148 | | - styles.importButton, |
149 | | - (!title.trim() || !jsonText.trim()) && styles.importButtonDisabled, |
150 | | - ]} |
151 | | - onPress={handleImport} |
152 | | - disabled={!title.trim() || !jsonText.trim()} |
153 | | - > |
154 | | - <Text style={styles.importButtonText}>Import</Text> |
155 | | - </TouchableOpacity> |
156 | | - |
157 | | - <Text style={styles.hint}> |
158 | | - Paste the JSON data exported from a recording. The recording will be |
159 | | - added to your list. |
160 | | - </Text> |
161 | | - </View> |
162 | | - </KeyboardAwareScrollView> |
163 | | - ); |
| 3 | +export default function ImportModalRoute() { |
| 4 | + return <ImportModal />; |
164 | 5 | } |
165 | | - |
166 | | -const styles = StyleSheet.create({ |
167 | | - container: { |
168 | | - backgroundColor: '#000000', |
169 | | - }, |
170 | | - header: { |
171 | | - flexDirection: 'row', |
172 | | - alignItems: 'center', |
173 | | - justifyContent: 'center', |
174 | | - paddingHorizontal: 16, |
175 | | - paddingBottom: 16, |
176 | | - borderBottomWidth: 1, |
177 | | - borderBottomColor: '#1C1C1E', |
178 | | - }, |
179 | | - headerTitle: { |
180 | | - fontSize: 20, |
181 | | - fontWeight: '700', |
182 | | - color: '#FFFFFF', |
183 | | - }, |
184 | | - closeButton: { |
185 | | - position: 'absolute', |
186 | | - right: 16, |
187 | | - top: 16, |
188 | | - width: 32, |
189 | | - height: 32, |
190 | | - borderRadius: 16, |
191 | | - backgroundColor: '#1C1C1E', |
192 | | - alignItems: 'center', |
193 | | - justifyContent: 'center', |
194 | | - }, |
195 | | - closeButtonText: { |
196 | | - fontSize: 18, |
197 | | - color: '#FFFFFF', |
198 | | - fontWeight: '600', |
199 | | - }, |
200 | | - content: { |
201 | | - flex: 1, |
202 | | - padding: 16, |
203 | | - }, |
204 | | - contentContainer: { |
205 | | - padding: 16, |
206 | | - }, |
207 | | - inputGroup: { |
208 | | - marginBottom: 24, |
209 | | - }, |
210 | | - label: { |
211 | | - fontSize: 16, |
212 | | - fontWeight: '600', |
213 | | - color: '#FFFFFF', |
214 | | - marginBottom: 8, |
215 | | - }, |
216 | | - titleInput: { |
217 | | - backgroundColor: '#1C1C1E', |
218 | | - borderRadius: 12, |
219 | | - padding: 16, |
220 | | - fontSize: 16, |
221 | | - color: '#FFFFFF', |
222 | | - borderWidth: 1, |
223 | | - borderColor: '#2C2C2E', |
224 | | - }, |
225 | | - jsonInput: { |
226 | | - backgroundColor: '#1C1C1E', |
227 | | - borderRadius: 12, |
228 | | - padding: 16, |
229 | | - fontSize: 14, |
230 | | - color: '#FFFFFF', |
231 | | - minHeight: 200, |
232 | | - fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', |
233 | | - borderWidth: 1, |
234 | | - borderColor: '#2C2C2E', |
235 | | - height: 200, |
236 | | - }, |
237 | | - errorText: { |
238 | | - color: '#FF3B30', |
239 | | - fontSize: 14, |
240 | | - marginBottom: 16, |
241 | | - textAlign: 'center', |
242 | | - }, |
243 | | - importButton: { |
244 | | - backgroundColor: '#007AFF', |
245 | | - borderRadius: 12, |
246 | | - padding: 16, |
247 | | - alignItems: 'center', |
248 | | - marginBottom: 16, |
249 | | - }, |
250 | | - importButtonDisabled: { |
251 | | - backgroundColor: '#1C1C1E', |
252 | | - }, |
253 | | - importButtonText: { |
254 | | - fontSize: 16, |
255 | | - fontWeight: '700', |
256 | | - color: '#FFFFFF', |
257 | | - }, |
258 | | - hint: { |
259 | | - fontSize: 12, |
260 | | - color: '#8E8E93', |
261 | | - textAlign: 'center', |
262 | | - lineHeight: 18, |
263 | | - }, |
264 | | -}); |
0 commit comments