Full UI 180 & Overall improvements
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
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,
|
||||
@@ -8,42 +10,149 @@ export default function BackupModal({
|
||||
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="slide" transparent>
|
||||
<View style={styles.modalBackdrop}>
|
||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={styles.modalKeyboardWrap}>
|
||||
<View style={styles.modalCard}>
|
||||
<View style={styles.sectionRow}>
|
||||
<Text style={styles.sectionTitle}>Backup & Restore</Text>
|
||||
<Pressable onPress={onClose}>
|
||||
<Text style={styles.closeText}>Close</Text>
|
||||
<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={{ paddingBottom: 12 }} showsVerticalScrollIndicator={false}>
|
||||
<Text style={styles.cardTitle}>Export JSON</Text>
|
||||
<Text style={styles.cardMeta}>Copy this JSON and store it safely.</Text>
|
||||
<TextInput
|
||||
style={styles.inputMultiline}
|
||||
multiline
|
||||
value={exportJson}
|
||||
editable={false}
|
||||
selectTextOnFocus
|
||||
/>
|
||||
<Text style={styles.cardTitle}>Import JSON</Text>
|
||||
<Text style={styles.cardMeta}>Paste a previous backup. This will replace current data.</Text>
|
||||
<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="#6b7280"
|
||||
placeholderTextColor="#71717a"
|
||||
/>
|
||||
<Pressable style={styles.primaryBtn} onPress={applyImport}>
|
||||
<Text style={styles.primaryBtnText}>Import & Replace</Text>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user