/ components / ChapterGuideOverlay.tsx
ChapterGuideOverlay.tsx
  1  import React, { useState, useEffect, useRef } from 'react';
  2  import {
  3    View,
  4    Text,
  5    TouchableOpacity,
  6    StyleSheet,
  7    Animated,
  8  } from 'react-native';
  9  import { Ionicons } from '@expo/vector-icons';
 10  import { useSafeAreaInsets } from 'react-native-safe-area-context';
 11  import AsyncStorage from '@react-native-async-storage/async-storage';
 12  
 13  interface ChapterGuideOverlayProps {
 14    visible: boolean;
 15    onDismiss: () => void;
 16    colors: any;
 17    onStepChange: (step: number) => void;
 18    hideControls?: () => void;
 19    showControls?: () => void;
 20  }
 21  
 22  const GUIDE_STORAGE_KEY = 'chapter_guide_seen';
 23  
 24  export const ChapterGuideOverlay: React.FC<ChapterGuideOverlayProps> = ({
 25    visible,
 26    onDismiss,
 27    colors,
 28    onStepChange,
 29    hideControls,
 30    showControls,
 31  }) => {
 32    const insets = useSafeAreaInsets();
 33    const opacity = useRef(new Animated.Value(0)).current;
 34    const [currentStep, setCurrentStep] = useState(1);
 35    const totalSteps = 4;
 36  
 37    useEffect(() => {
 38      if (visible) {
 39        Animated.timing(opacity, {
 40          toValue: 1,
 41          duration: 300,
 42          useNativeDriver: true,
 43        }).start();
 44      } else {
 45        Animated.timing(opacity, {
 46          toValue: 0,
 47          duration: 200,
 48          useNativeDriver: true,
 49        }).start();
 50      }
 51    }, [visible, opacity]);
 52  
 53    useEffect(() => {
 54      onStepChange(currentStep);
 55    }, [currentStep, onStepChange]);
 56  
 57    const handleNext = () => {
 58      if (currentStep === 1 && hideControls) {
 59        hideControls();
 60      }
 61  
 62      if (currentStep < totalSteps) {
 63        setCurrentStep((prevStep) => prevStep + 1);
 64      } else {
 65        handleDismiss();
 66      }
 67    };
 68  
 69    const handleBack = () => {
 70      if (currentStep > 1) {
 71        const newStep = currentStep - 1;
 72        setCurrentStep(newStep);
 73  
 74        // If going back to step 1, show the navigation controls
 75        if (newStep === 1 && showControls) {
 76          showControls();
 77        }
 78      }
 79    };
 80  
 81    const handleDismiss = async () => {
 82      try {
 83        await AsyncStorage.setItem(GUIDE_STORAGE_KEY, 'true');
 84      } catch (error) {
 85        console.error('Error saving guide state:', error);
 86      }
 87      onDismiss();
 88    };
 89  
 90    const renderStep = () => {
 91      switch (currentStep) {
 92        case 1:
 93          return (
 94            <>
 95              <View style={styles.guideOverlay} pointerEvents="none">
 96                <View style={[styles.navHighlight, { top: insets.top }]} />
 97              </View>
 98              <View
 99                style={[styles.guideContent, { marginTop: insets.top + 100 }]}
100              >
101                <Text style={[styles.guideTitle, { color: colors.primary }]}>
102                  Navigation Controls
103                </Text>
104                <Text style={[styles.guideExplanation, { color: colors.text }]}>
105                  Try using the controls at the top:
106                </Text>
107                <View style={styles.guideRow}>
108                  <View
109                    style={[
110                      styles.iconBubble,
111                      { backgroundColor: colors.primary + '20' },
112                    ]}
113                  >
114                    <Ionicons
115                      name="arrow-back"
116                      size={24}
117                      color={colors.primary}
118                    />
119                  </View>
120                  <Text style={[styles.guideText, { color: colors.text }]}>
121                    Return to details page
122                  </Text>
123                </View>
124                <View style={styles.guideRow}>
125                  <View
126                    style={[
127                      styles.iconBubble,
128                      { backgroundColor: colors.primary + '20' },
129                    ]}
130                  >
131                    <Ionicons name="menu" size={24} color={colors.primary} />
132                  </View>
133                  <Text style={[styles.guideText, { color: colors.text }]}>
134                    Tap title area to browse all chapters
135                  </Text>
136                </View>
137                <View style={styles.guideRow}>
138                  <View
139                    style={[
140                      styles.iconBubble,
141                      { backgroundColor: colors.primary + '20' },
142                    ]}
143                  >
144                    <Ionicons
145                      name="chevron-forward"
146                      size={24}
147                      color={colors.primary}
148                    />
149                  </View>
150                  <Text style={[styles.guideText, { color: colors.text }]}>
151                    Navigate between chapters
152                  </Text>
153                </View>
154                <Text style={[styles.guideNote, { color: colors.text + '80' }]}>
155                  Works for both manga and manhwa
156                </Text>
157              </View>
158            </>
159          );
160        case 2:
161          return (
162            <>
163              <View style={styles.fullOverlay} pointerEvents="none" />
164              <View style={styles.centerContentWrapper}>
165                <View style={styles.mainTapArea} pointerEvents="none" />
166                <View style={styles.guideContent}>
167                  <Text style={[styles.guideTitle, { color: colors.primary }]}>
168                    Interactive Area
169                  </Text>
170                  <View style={styles.guideRow}>
171                    <View
172                      style={[
173                        styles.iconBubble,
174                        { backgroundColor: colors.primary + '20' },
175                      ]}
176                    >
177                      <Ionicons
178                        name="hand-left-outline"
179                        size={24}
180                        color={colors.primary}
181                      />
182                    </View>
183                    <Text style={[styles.guideText, { color: colors.text }]}>
184                      Tapping most of the screen will show/hide navigation
185                      controls
186                    </Text>
187                  </View>
188                  <Text style={[styles.guideNote, { color: colors.text + '80' }]}>
189                    Great for manga and manhwa reading - tap to toggle controls
190                    when needed
191                  </Text>
192                </View>
193              </View>
194            </>
195          );
196        case 3:
197          return (
198            <>
199              <View style={styles.fullOverlay} pointerEvents="none" />
200              <View style={styles.centerContentWrapper}>
201                <View style={styles.edgeTapAreas} pointerEvents="none">
202                  <View style={styles.leftEdge} />
203                  <View style={styles.rightEdge} />
204                </View>
205                <View style={styles.guideContent}>
206                  <Text style={[styles.guideTitle, { color: colors.primary }]}>
207                    Safe Scroll Zones
208                  </Text>
209                  <View style={styles.guideRow}>
210                    <View
211                      style={[
212                        styles.iconBubble,
213                        { backgroundColor: colors.primary + '20' },
214                      ]}
215                    >
216                      <Ionicons
217                        name="finger-print-outline"
218                        size={24}
219                        color={colors.primary}
220                      />
221                    </View>
222                    <Text style={[styles.guideText, { color: colors.text }]}>
223                      The edge areas (60px wide) let you scroll without triggering
224                      controls
225                    </Text>
226                  </View>
227                  <Text style={[styles.guideNote, { color: colors.text + '80' }]}>
228                    Perfect for long manga and manhwa chapters
229                  </Text>
230                </View>
231              </View>
232            </>
233          );
234        case 4:
235          return (
236            <>
237              <View style={styles.fullOverlay} pointerEvents="none" />
238              <View style={styles.centerContentWrapper}>
239                <View style={styles.guideContent}>
240                  <Text style={[styles.guideTitle, { color: colors.primary }]}>
241                    Ready to Read!
242                  </Text>
243                  <View style={styles.finalStep}>
244                    <Ionicons
245                      name="book-outline"
246                      size={48}
247                      color={colors.primary}
248                    />
249                    <Text
250                      style={[
251                        styles.guideText,
252                        {
253                          color: colors.text,
254                          textAlign: 'center',
255                          marginTop: 16,
256                        },
257                      ]}
258                    >
259                      Enjoy reading your manga and manhwa with these intuitive
260                      controls
261                    </Text>
262                    <Text
263                      style={[
264                        styles.guideNote,
265                        {
266                          color: colors.text + '80',
267                          textAlign: 'center',
268                          marginTop: 8,
269                        },
270                      ]}
271                    >
272                      This guide won&apos;t appear again, but you can reset it in
273                      the Debug menu
274                    </Text>
275                  </View>
276                </View>
277              </View>
278            </>
279          );
280        default:
281          return null;
282      }
283    };
284  
285    if (!visible) return null;
286  
287    return (
288      <Animated.View
289        style={[
290          styles.overlay,
291          { opacity, paddingTop: insets.top, paddingBottom: insets.bottom },
292        ]}
293        pointerEvents={currentStep === 1 ? 'box-none' : 'auto'}
294      >
295        {renderStep()}
296  
297        <View style={styles.controlRow}>
298          <View style={styles.navButtons}>
299            {currentStep > 1 && (
300              <TouchableOpacity
301                style={[styles.backButton, { borderColor: colors.primary }]}
302                onPress={handleBack}
303                activeOpacity={0.7}
304                testID="guide-back-button"
305              >
306                <Ionicons
307                  name="arrow-back"
308                  size={16}
309                  color={colors.primary}
310                  style={styles.backIcon}
311                />
312                <Text style={[styles.backButtonText, { color: colors.primary }]}>
313                  Back
314                </Text>
315              </TouchableOpacity>
316            )}
317            <Text style={[styles.stepIndicator, { color: colors.text }]}>
318              {currentStep}/{totalSteps}
319            </Text>
320          </View>
321  
322          <TouchableOpacity
323            style={[styles.nextButton, { backgroundColor: colors.primary }]}
324            onPress={handleNext}
325            activeOpacity={0.7}
326            testID="guide-next-button"
327          >
328            <Text style={styles.nextButtonText}>
329              {currentStep < totalSteps ? 'Next' : 'Finish'}
330            </Text>
331            <Ionicons
332              name="arrow-forward"
333              size={16}
334              color="white"
335              style={styles.nextIcon}
336            />
337          </TouchableOpacity>
338        </View>
339      </Animated.View>
340    );
341  };
342  
343  // Helper to check if the user has seen the guide before
344  export const hasSeenChapterGuide = async (): Promise<boolean> => {
345    try {
346      const value = await AsyncStorage.getItem(GUIDE_STORAGE_KEY);
347      return value === 'true';
348    } catch (error) {
349      console.error('Error checking guide state:', error);
350      return false;
351    }
352  };
353  
354  const styles = StyleSheet.create({
355    overlay: {
356      ...StyleSheet.absoluteFillObject,
357      backgroundColor: 'rgba(0, 0, 0, 0.75)',
358      justifyContent: 'space-between',
359      zIndex: 100,
360    },
361    fullOverlay: {
362      ...StyleSheet.absoluteFillObject,
363      backgroundColor: 'transparent',
364    },
365    guideOverlay: {
366      ...StyleSheet.absoluteFillObject,
367      zIndex: 1,
368    },
369    navHighlight: {
370      height: 56,
371      width: '100%',
372      backgroundColor: 'rgba(255, 255, 255, 0.2)',
373      position: 'absolute',
374    },
375    centerContentWrapper: {
376      flex: 1,
377      justifyContent: 'center',
378      alignItems: 'center',
379    },
380    mainTapArea: {
381      position: 'absolute',
382      top: 0,
383      left: 60, // 60px from left
384      right: 60, // 60px from right
385      bottom: 0,
386      backgroundColor: 'rgba(255, 255, 255, 0.15)',
387      borderWidth: 2,
388      borderColor: 'rgba(255, 255, 255, 0.3)',
389      borderStyle: 'dashed',
390    },
391    edgeTapAreas: {
392      ...StyleSheet.absoluteFillObject,
393      flexDirection: 'row',
394      justifyContent: 'space-between',
395    },
396    leftEdge: {
397      width: 60, // 60px exactly
398      height: '100%',
399      backgroundColor: 'rgba(255, 255, 255, 0.25)',
400      borderRightWidth: 2,
401      borderColor: 'rgba(255, 255, 255, 0.5)',
402      borderStyle: 'dashed',
403    },
404    rightEdge: {
405      width: 60, // 60px exactly
406      height: '100%',
407      backgroundColor: 'rgba(255, 255, 255, 0.25)',
408      borderLeftWidth: 2,
409      borderColor: 'rgba(255, 255, 255, 0.5)',
410      borderStyle: 'dashed',
411    },
412    guideContent: {
413      padding: 24,
414      backgroundColor: 'rgba(0, 0, 0, 0.8)',
415      borderRadius: 16,
416      margin: 16,
417      alignSelf: 'center',
418      maxWidth: 400,
419      width: '90%',
420      zIndex: 5,
421    },
422    guideTitle: {
423      fontSize: 22,
424      fontWeight: 'bold',
425      marginBottom: 16,
426      textAlign: 'center',
427    },
428    guideExplanation: {
429      fontSize: 16,
430      marginBottom: 16,
431      textAlign: 'center',
432    },
433    guideRow: {
434      flexDirection: 'row',
435      alignItems: 'center',
436      marginBottom: 16,
437    },
438    iconBubble: {
439      width: 40,
440      height: 40,
441      borderRadius: 20,
442      justifyContent: 'center',
443      alignItems: 'center',
444      marginRight: 12,
445    },
446    guideText: {
447      fontSize: 16,
448      flex: 1,
449    },
450    guideNote: {
451      fontSize: 14,
452      marginTop: 8,
453      fontStyle: 'italic',
454    },
455    finalStep: {
456      alignItems: 'center',
457      paddingVertical: 20,
458    },
459    controlRow: {
460      flexDirection: 'row',
461      justifyContent: 'space-between',
462      alignItems: 'center',
463      padding: 16,
464      paddingBottom: 32,
465      zIndex: 2,
466    },
467    navButtons: {
468      flexDirection: 'row',
469      alignItems: 'center',
470    },
471    stepIndicator: {
472      fontSize: 16,
473      fontWeight: 'bold',
474    },
475    backButton: {
476      flexDirection: 'row',
477      alignItems: 'center',
478      marginRight: 16,
479      paddingHorizontal: 16,
480      paddingVertical: 8,
481      borderRadius: 24,
482      borderWidth: 1,
483    },
484    backButtonText: {
485      fontSize: 14,
486      fontWeight: '600',
487    },
488    backIcon: {
489      marginRight: 6,
490    },
491    nextButton: {
492      paddingHorizontal: 24,
493      paddingVertical: 12,
494      borderRadius: 24,
495      flexDirection: 'row',
496      alignItems: 'center',
497      justifyContent: 'center',
498      minWidth: 120,
499    },
500    nextButtonText: {
501      color: 'white',
502      fontSize: 16,
503      fontWeight: 'bold',
504    },
505    nextIcon: {
506      marginLeft: 8,
507    },
508  });