164 lines
6.6 KiB
JavaScript
164 lines
6.6 KiB
JavaScript
import React, { useEffect, useRef, useState } from 'react';
|
|
import { KeyboardAvoidingView, Modal, Platform, Pressable, ScrollView, Text, TextInput, View } from 'react-native';
|
|
import Ionicons from '@expo/vector-icons/Ionicons';
|
|
import { styles } from '../styles';
|
|
import { cn } from '../utils/cn';
|
|
|
|
export default function BackupModal({
|
|
visible,
|
|
onClose,
|
|
exportJson,
|
|
importJson,
|
|
setImportJson,
|
|
exportFile,
|
|
importFile,
|
|
applyImport,
|
|
}) {
|
|
const exportInputRef = useRef(null);
|
|
const [showExportJson, setShowExportJson] = useState(false);
|
|
const [copyState, setCopyState] = useState('idle');
|
|
|
|
useEffect(() => {
|
|
if (!visible) return;
|
|
setShowExportJson(false);
|
|
setCopyState('idle');
|
|
}, [visible]);
|
|
|
|
useEffect(() => {
|
|
if (copyState !== 'copied') return undefined;
|
|
const timeout = setTimeout(() => setCopyState('idle'), 1400);
|
|
return () => clearTimeout(timeout);
|
|
}, [copyState]);
|
|
|
|
async function handleCopyData() {
|
|
if (Platform.OS === 'web' && typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
|
|
try {
|
|
await navigator.clipboard.writeText(exportJson);
|
|
setCopyState('copied');
|
|
return;
|
|
} catch {
|
|
// fall through to manual copy fallback
|
|
}
|
|
}
|
|
|
|
setShowExportJson(true);
|
|
setCopyState('manual');
|
|
|
|
if (Platform.OS === 'android') {
|
|
return;
|
|
}
|
|
|
|
requestAnimationFrame(() => {
|
|
exportInputRef.current?.focus?.();
|
|
if (exportInputRef.current?.setNativeProps) {
|
|
exportInputRef.current.setNativeProps({
|
|
selection: { start: 0, end: exportJson.length },
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
return (
|
|
<Modal visible={visible} animationType="none" transparent>
|
|
<View className={styles.modalBackdrop}>
|
|
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} className={styles.modalKeyboardWrap}>
|
|
<View className={styles.modalCard}>
|
|
<View className={styles.sectionHeader}>
|
|
<View className={styles.sectionHeaderLeft}>
|
|
<View className={styles.sectionHeaderIconWrap}>
|
|
<Ionicons name="cloud-upload-outline" size={16} color="#d4d4d8" />
|
|
</View>
|
|
<Text className={styles.cardTitle}>Backup & Restore</Text>
|
|
</View>
|
|
<Pressable className={styles.secondaryBtnTight} onPress={onClose}>
|
|
<View className={styles.buttonContent}>
|
|
<Ionicons name="close" size={14} color="#f4f4f5" />
|
|
<Text className={styles.secondaryBtnText}>Close</Text>
|
|
</View>
|
|
</Pressable>
|
|
</View>
|
|
|
|
<ScrollView keyboardShouldPersistTaps="handled" contentContainerStyle={styles.modalScrollContent} showsVerticalScrollIndicator={false}>
|
|
<View className={styles.sectionHeaderLeft}>
|
|
<Ionicons name="download-outline" size={14} color="#d4d4d8" />
|
|
<Text className={styles.cardTitle}>Export JSON</Text>
|
|
</View>
|
|
<Text className={styles.cardMeta}>Use Show JSON or Copy Data.</Text>
|
|
|
|
<View className={styles.actionRow}>
|
|
<Pressable className={cn(styles.secondaryBtnTight, styles.flex)} onPress={() => setShowExportJson((prev) => !prev)}>
|
|
<View className={styles.buttonContent}>
|
|
<Ionicons name={showExportJson ? 'eye-off-outline' : 'eye-outline'} size={14} color="#f4f4f5" />
|
|
<Text className={styles.secondaryBtnText}>{showExportJson ? 'Hide JSON' : 'Show JSON'}</Text>
|
|
</View>
|
|
</Pressable>
|
|
|
|
<Pressable className={cn(styles.primaryBtnTight, styles.flex)} onPress={handleCopyData}>
|
|
<View className={styles.buttonContent}>
|
|
<Ionicons name={copyState === 'copied' ? 'checkmark-circle-outline' : 'copy-outline'} size={14} color="#ffffff" />
|
|
<Text className={styles.primaryBtnText}>{copyState === 'copied' ? 'Copied' : 'Copy Data'}</Text>
|
|
</View>
|
|
</Pressable>
|
|
</View>
|
|
|
|
<Pressable className={styles.secondaryBtn} onPress={exportFile}>
|
|
<View className={styles.buttonContent}>
|
|
<Ionicons name="share-social-outline" size={15} color="#f4f4f5" />
|
|
<Text className={styles.secondaryBtnText}>Export File</Text>
|
|
</View>
|
|
</Pressable>
|
|
|
|
{copyState === 'manual' ? (
|
|
<Text className={styles.cardMeta}>Direct clipboard is unavailable here. JSON is shown for manual copy.</Text>
|
|
) : null}
|
|
|
|
{showExportJson ? (
|
|
Platform.OS === 'android' ? (
|
|
<View style={styles.exportJsonBlock}>
|
|
<Text style={styles.exportJsonText} selectable>{exportJson}</Text>
|
|
</View>
|
|
) : (
|
|
<TextInput
|
|
ref={exportInputRef}
|
|
style={styles.inputMultiline}
|
|
multiline
|
|
value={exportJson}
|
|
editable={false}
|
|
selectTextOnFocus
|
|
/>
|
|
)
|
|
) : null}
|
|
|
|
<View className={styles.sectionHeaderLeft}>
|
|
<Ionicons name="cloud-upload-outline" size={14} color="#d4d4d8" />
|
|
<Text className={styles.cardTitle}>Import JSON</Text>
|
|
</View>
|
|
<Text className={styles.cardMeta}>Paste a previous backup. This will replace current data.</Text>
|
|
<Pressable className={styles.secondaryBtn} onPress={importFile}>
|
|
<View className={styles.buttonContent}>
|
|
<Ionicons name="document-outline" size={15} color="#f4f4f5" />
|
|
<Text className={styles.secondaryBtnText}>Pick Backup File</Text>
|
|
</View>
|
|
</Pressable>
|
|
<TextInput
|
|
style={styles.inputMultiline}
|
|
multiline
|
|
value={importJson}
|
|
onChangeText={setImportJson}
|
|
placeholder="Paste backup JSON here"
|
|
placeholderTextColor="#71717a"
|
|
/>
|
|
<Pressable className={styles.primaryBtn} onPress={applyImport}>
|
|
<View className={styles.buttonContent}>
|
|
<Ionicons name="sync-outline" size={15} color="#ffffff" />
|
|
<Text className={styles.primaryBtnText}>Import & Replace</Text>
|
|
</View>
|
|
</Pressable>
|
|
</ScrollView>
|
|
</View>
|
|
</KeyboardAvoidingView>
|
|
</View>
|
|
</Modal>
|
|
);
|
|
}
|