You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
245 lines
7.7 KiB
245 lines
7.7 KiB
import { StatusBar } from 'expo-status-bar';
|
|
import { FlatList, Keyboard, Modal, Pressable, ScrollView, StyleSheet, View } from 'react-native';
|
|
import { SafeAreaProvider, SafeAreaView, useSafeAreaInsets } 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 { jaPack, languages } from './lib/providers/dictionaries';
|
|
import { Input, InputField } from './components/ui/input';
|
|
import { HStack } from './components/ui/hstack';
|
|
import { Button, ButtonIcon, ButtonText } from './components/ui/button';
|
|
import DictWebView from './components/DictWebView';
|
|
import AdBannerPlaceholder from './components/AdBannerPlaceholder';
|
|
import { Toast, ToastDescription, ToastTitle, useToast } from './components/ui/toast';
|
|
|
|
import mobileAds from 'react-native-google-mobile-ads';
|
|
import { Icon } from './components/ui/icon';
|
|
import { Bookmark, BookMarked, BookOpen, Search } from 'lucide-react-native';
|
|
|
|
function MainScreen() {
|
|
const [q, setQ] = useState('');
|
|
const [query, setQuery] = useState('');
|
|
const [activeId, setActiveId] = useState<string>('');
|
|
const [wordbookOpen, setWordbookOpen] = useState(false);
|
|
|
|
const insets = useSafeAreaInsets();
|
|
|
|
const pack = jaPack;
|
|
const providers = pack.providers;
|
|
const canSearch = q.trim().length > 0;
|
|
const hasQuery = query.trim().length > 0;
|
|
|
|
const toast = useToast();
|
|
|
|
const { add, items, remove, clear, loading } = useWordbook();
|
|
|
|
const urls = useMemo(
|
|
() =>
|
|
providers.reduce<Record<string, string>>((acc, p) => {
|
|
acc[p.id] = hasQuery ? p.buildUrl(query.trim()) : '';
|
|
return acc;
|
|
}, {}),
|
|
[providers, query, hasQuery],
|
|
);
|
|
|
|
const onSearch = () => {
|
|
if (!canSearch) return;
|
|
Keyboard.dismiss();
|
|
setQuery(q.trim());
|
|
setActiveId(providers[0]?.id ?? '');
|
|
};
|
|
|
|
const onSave = () => {
|
|
if (!canSearch) return;
|
|
Keyboard.dismiss();
|
|
add(q.trim(), pack.code);
|
|
|
|
toast.show({
|
|
id: 'onSave',
|
|
placement: 'top',
|
|
duration: 2000,
|
|
render: () => (
|
|
<View style={{ marginTop: insets.top + 12, alignItems: 'center', width: '100%' }}>
|
|
<Toast
|
|
className="rounded-full opacity-80 mx-5 py-2 flex-row items-center gap-2 shadow-lg max-w-[90%]"
|
|
variant="solid"
|
|
>
|
|
<ToastTitle>✅</ToastTitle>
|
|
<ToastDescription>단어장에 저장되었습니다</ToastDescription>
|
|
</Toast>
|
|
</View>
|
|
),
|
|
});
|
|
};
|
|
|
|
const onPickWord = (word: string) => {
|
|
const t = word.trim();
|
|
setWordbookOpen(false);
|
|
setQ(t);
|
|
setQuery(t);
|
|
setActiveId(providers[0]?.id ?? '');
|
|
};
|
|
|
|
return (
|
|
<View className="flex-1">
|
|
{/* 상단 검색창, 버튼 */}
|
|
<View className="p-4 gap-3">
|
|
{/* 검색창 */}
|
|
<HStack space="md" className="flex-row">
|
|
<Button
|
|
className="p-1"
|
|
onPress={() => setWordbookOpen(true)}
|
|
variant="outline"
|
|
action="secondary"
|
|
accessibilityLabel="단어장 보기"
|
|
>
|
|
<Icon as={BookMarked} />
|
|
</Button>
|
|
<Input className="flex-1">
|
|
<InputField
|
|
placeholder="검색어를 입력하세요"
|
|
value={q}
|
|
onChangeText={setQ}
|
|
returnKeyType="search"
|
|
submitBehavior="blurAndSubmit"
|
|
onSubmitEditing={onSearch}
|
|
/>
|
|
</Input>
|
|
<Button
|
|
className="p-1"
|
|
onPress={onSave}
|
|
variant="outline"
|
|
action="secondary"
|
|
isDisabled={!canSearch}
|
|
accessibilityLabel="단어장 저장"
|
|
>
|
|
<Icon as={Bookmark} />
|
|
</Button>
|
|
<Button onPress={onSearch} isDisabled={!canSearch}>
|
|
<ButtonText>검색</ButtonText>
|
|
</Button>
|
|
</HStack>
|
|
</View>
|
|
|
|
{/* 커스텀 탭 헤더 (가로스크롤) */}
|
|
<View className="px-4 pb-2">
|
|
<ScrollView
|
|
horizontal
|
|
showsHorizontalScrollIndicator={false}
|
|
contentContainerClassName="gap-1"
|
|
>
|
|
{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>
|
|
);
|
|
})}
|
|
</ScrollView>
|
|
</View>
|
|
|
|
{/* 패널: WebView 모두 마운트 후 토글 */}
|
|
<View className="flex-1">
|
|
{!hasQuery ? (
|
|
<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-1">
|
|
<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>
|
|
);
|
|
}
|
|
|
|
export default function App() {
|
|
// mobileAds()
|
|
// .initialize()
|
|
// .then(() => {
|
|
// // ready
|
|
// });
|
|
|
|
return (
|
|
<GluestackUIProvider mode="light">
|
|
<SafeAreaProvider>
|
|
<SafeAreaView style={{ flex: 1 }}>
|
|
<StatusBar style="auto" />
|
|
<WordbookProvider>
|
|
<MainScreen />
|
|
</WordbookProvider>
|
|
</SafeAreaView>
|
|
</SafeAreaProvider>
|
|
</GluestackUIProvider>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: '#fff',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
});
|
|
|