|
|
|
@ -1,50 +1,186 @@ |
|
|
|
|
import { StatusBar } from 'expo-status-bar'; |
|
|
|
|
import { SafeAreaView, StyleSheet, View } from 'react-native'; |
|
|
|
|
import { FlatList, Modal, Pressable, StyleSheet, View } from 'react-native'; |
|
|
|
|
import { SafeAreaView } from 'react-native-safe-area-context'; |
|
|
|
|
|
|
|
|
|
import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider'; |
|
|
|
|
import '@/global.css'; |
|
|
|
|
import { useWordbook, WordbookProvider } from './lib/wordbook/context'; |
|
|
|
|
import { Text } from './components/ui/text'; |
|
|
|
|
import { useMemo, useState } from 'react'; |
|
|
|
|
import { languages } from './lib/providers/dictionaries'; |
|
|
|
|
import { jaPack, languages } from './lib/providers/dictionaries'; |
|
|
|
|
import { Input, InputField } from './components/ui/input'; |
|
|
|
|
import { HStack } from './components/ui/hstack'; |
|
|
|
|
import { Button, ButtonText } from './components/ui/button'; |
|
|
|
|
import DictWebView from './components/DictWebView'; |
|
|
|
|
import AdBannerPlaceholder from './components/AdBannerPlaceholder'; |
|
|
|
|
|
|
|
|
|
function MainScreen() { |
|
|
|
|
const [q, setQ] = useState(''); |
|
|
|
|
const [langIdx, setLangIdx] = useState(0); |
|
|
|
|
const [query, setQuery] = useState(''); |
|
|
|
|
const [activeId, setActiveId] = useState<string>(''); |
|
|
|
|
const [wordbookOpen, setWordbookOpen] = useState(false); |
|
|
|
|
|
|
|
|
|
const pack = languages[langIdx]; |
|
|
|
|
const pack = jaPack; |
|
|
|
|
const providers = pack.providers; |
|
|
|
|
const canSearch = q.trim().length > 0; |
|
|
|
|
const hasQuery = query.trim().length > 0; |
|
|
|
|
|
|
|
|
|
const { add, items, remove, clear, loading } = useWordbook(); |
|
|
|
|
|
|
|
|
|
const urls = useMemo( |
|
|
|
|
() => |
|
|
|
|
providers.reduce<Record<string, string>>((acc, p) => { |
|
|
|
|
acc[p.id] = canSearch ? p.buildUrl(q.trim()) : ''; |
|
|
|
|
acc[p.id] = hasQuery ? p.buildUrl(query.trim()) : ''; |
|
|
|
|
return acc; |
|
|
|
|
}, {}), |
|
|
|
|
[providers, q, canSearch], |
|
|
|
|
[providers, query, hasQuery], |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
const onSearch = () => { |
|
|
|
|
if (!canSearch) return; |
|
|
|
|
setQuery(q.trim()); |
|
|
|
|
setActiveId(providers[0]?.id ?? ''); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const onSave = () => { |
|
|
|
|
if (!canSearch) return; |
|
|
|
|
add(q.trim(), pack.code); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const onPickWord = (word: string) => { |
|
|
|
|
const t = word.trim(); |
|
|
|
|
setWordbookOpen(false); |
|
|
|
|
setQ(t); |
|
|
|
|
setQuery(t); |
|
|
|
|
setActiveId(providers[0]?.id ?? ''); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
<SafeAreaView className="flex-1 bg-white"> |
|
|
|
|
<View className="flex-1"> |
|
|
|
|
{/* 상단 검색창, 버튼 */} |
|
|
|
|
<View className="p-4 gap-3"> |
|
|
|
|
<Text>Hello</Text> |
|
|
|
|
{/* 액션 버튼 */} |
|
|
|
|
<HStack space="md" className="flex-row"> |
|
|
|
|
<Button className="flex-1" onPress={() => setWordbookOpen(true)} action="secondary"> |
|
|
|
|
<ButtonText>단어장 보기</ButtonText> |
|
|
|
|
</Button> |
|
|
|
|
<Button className="flex-1" onPress={onSave} action="secondary" isDisabled={!canSearch}> |
|
|
|
|
<ButtonText>단어장 저장</ButtonText> |
|
|
|
|
</Button> |
|
|
|
|
</HStack> |
|
|
|
|
|
|
|
|
|
{/* 검색창 */} |
|
|
|
|
<HStack space="md" className="flex-row"> |
|
|
|
|
<Input className="flex-1"> |
|
|
|
|
<InputField |
|
|
|
|
placeholder="검색어를 입력하세요" |
|
|
|
|
value={q} |
|
|
|
|
onChangeText={setQ} |
|
|
|
|
returnKeyType="search" |
|
|
|
|
onSubmitEditing={onSearch} |
|
|
|
|
/> |
|
|
|
|
</Input> |
|
|
|
|
<Button onPress={onSearch} isDisabled={!canSearch}> |
|
|
|
|
<ButtonText>검색</ButtonText> |
|
|
|
|
</Button> |
|
|
|
|
</HStack> |
|
|
|
|
</View> |
|
|
|
|
|
|
|
|
|
{/* 커스텀 탭 헤더 */} |
|
|
|
|
<View className="px-4 pb-2"> |
|
|
|
|
<HStack space="sm" className="flex-row"> |
|
|
|
|
{providers.map((p) => { |
|
|
|
|
const selected = activeId ? activeId === p.id : false; |
|
|
|
|
return ( |
|
|
|
|
<Button |
|
|
|
|
className="flex-1" |
|
|
|
|
key={p.id} |
|
|
|
|
variant={selected ? 'solid' : 'outline'} |
|
|
|
|
onPress={() => setActiveId(p.id)} |
|
|
|
|
isDisabled={!canSearch} |
|
|
|
|
> |
|
|
|
|
<ButtonText>{p.label}</ButtonText> |
|
|
|
|
</Button> |
|
|
|
|
); |
|
|
|
|
})} |
|
|
|
|
</HStack> |
|
|
|
|
</View> |
|
|
|
|
|
|
|
|
|
{/* 패널: WebView 모두 마운트 후 토글 */} |
|
|
|
|
<View className="flex-1"> |
|
|
|
|
{!canSearch ? ( |
|
|
|
|
<View className="flex-1 items-center justify-center"> |
|
|
|
|
<Text>검색어를 입력하고 [검색]을 눌러주세요</Text> |
|
|
|
|
</View> |
|
|
|
|
) : activeId ? ( |
|
|
|
|
<DictWebView url={urls[activeId]} /> |
|
|
|
|
) : ( |
|
|
|
|
<View className="flex-1 items-center justify-center"> |
|
|
|
|
<Text>사전을 선택하세요</Text> |
|
|
|
|
</View> |
|
|
|
|
)} |
|
|
|
|
</View> |
|
|
|
|
|
|
|
|
|
<AdBannerPlaceholder /> |
|
|
|
|
|
|
|
|
|
<Modal |
|
|
|
|
visible={wordbookOpen} |
|
|
|
|
animationType="slide" |
|
|
|
|
onRequestClose={() => setWordbookOpen(false)} |
|
|
|
|
> |
|
|
|
|
<View className="flex-1 p-4 gap-3"> |
|
|
|
|
<View className="flex-row justify-between items-center"> |
|
|
|
|
<Text className="text-lg font-semibold">단어장</Text> |
|
|
|
|
<HStack space="md" className="flex-row"> |
|
|
|
|
<Button action="negative" onPress={clear}> |
|
|
|
|
<ButtonText>전체삭제</ButtonText> |
|
|
|
|
</Button> |
|
|
|
|
<Button onPress={() => setWordbookOpen(false)}> |
|
|
|
|
<ButtonText>닫기</ButtonText> |
|
|
|
|
</Button> |
|
|
|
|
</HStack> |
|
|
|
|
</View> |
|
|
|
|
|
|
|
|
|
{loading ? ( |
|
|
|
|
<View className="flex-1 items-center justify-center"> |
|
|
|
|
<Text>불러오는 중...</Text> |
|
|
|
|
</View> |
|
|
|
|
) : ( |
|
|
|
|
<FlatList |
|
|
|
|
className="flex-1" |
|
|
|
|
data={items} |
|
|
|
|
keyExtractor={(x) => x.id} |
|
|
|
|
renderItem={({ item }) => ( |
|
|
|
|
<View className="flex-row justify-between py-3 gap-4"> |
|
|
|
|
<Pressable |
|
|
|
|
onPress={() => onPickWord(item.text)} |
|
|
|
|
style={{ flex: 1 }} |
|
|
|
|
className="justify-center items-center" |
|
|
|
|
> |
|
|
|
|
<Text className="text-lg">{item.text}</Text> |
|
|
|
|
</Pressable> |
|
|
|
|
<Button action="secondary" onPress={() => remove(item.id)}> |
|
|
|
|
<ButtonText>삭제</ButtonText> |
|
|
|
|
</Button> |
|
|
|
|
</View> |
|
|
|
|
)} |
|
|
|
|
/> |
|
|
|
|
)} |
|
|
|
|
</View> |
|
|
|
|
</Modal> |
|
|
|
|
</View> |
|
|
|
|
</SafeAreaView> |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export default function App() { |
|
|
|
|
return ( |
|
|
|
|
<GluestackUIProvider mode="light"> |
|
|
|
|
<SafeAreaView style={{ flex: 1 }}> |
|
|
|
|
<StatusBar style="auto" /> |
|
|
|
|
<WordbookProvider> |
|
|
|
|
<MainScreen /> |
|
|
|
|
</WordbookProvider> |
|
|
|
|
</SafeAreaView> |
|
|
|
|
</GluestackUIProvider> |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|