107 lines
5.1 KiB
TypeScript
107 lines
5.1 KiB
TypeScript
import React, { useState, useEffect } from 'react'
|
|
import { View, Text, Switch, TouchableOpacity, StyleSheet, ScrollView, Alert } from 'react-native'
|
|
import * as SecureStore from 'expo-secure-store'
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
const Haptics = (() => { try { return require('expo-haptics') } catch { return null } })()
|
|
import { COLORS } from '../../constants/Config'
|
|
import { useTheme } from '../../contexts/ThemeContext'
|
|
import { useFontScale } from '../../contexts/FontContext'
|
|
|
|
export default function ThemeSettingsScreen() {
|
|
const { isDark, toggleTheme } = useTheme()
|
|
const { fontScale, setFontScale } = useFontScale()
|
|
const [vibration, setVibration] = useState('short')
|
|
const [colorBlind, setColorBlind] = useState('default')
|
|
const [screenLock, setScreenLock] = useState(false)
|
|
|
|
useEffect(() => {
|
|
Promise.all([
|
|
SecureStore.getItemAsync('grd_vibration'),
|
|
SecureStore.getItemAsync('grd_colorblind'),
|
|
SecureStore.getItemAsync('grd_screen_lock'),
|
|
]).then(([v, c, l]) => {
|
|
if (v) setVibration(v)
|
|
if (c) setColorBlind(c)
|
|
if (l) setScreenLock(l === 'true')
|
|
})
|
|
}, [])
|
|
|
|
const saveVibration = async (v: string) => {
|
|
setVibration(v)
|
|
await SecureStore.setItemAsync('grd_vibration', v)
|
|
if (v !== 'none') await Haptics?.notificationAsync?.(Haptics?.NotificationFeedbackType?.Success)
|
|
}
|
|
|
|
const saveColorBlind = async (c: string) => {
|
|
setColorBlind(c)
|
|
await SecureStore.setItemAsync('grd_colorblind', c)
|
|
Alert.alert('색맹 지원', `${c === 'default' ? '기본' : c === 'protanopia' ? '제1색맹' : '제2색맹'} 팔레트가 적용됐습니다.`)
|
|
}
|
|
|
|
const toggleScreenLock = async (v: boolean) => {
|
|
setScreenLock(v)
|
|
await SecureStore.setItemAsync('grd_screen_lock', String(v))
|
|
Alert.alert('화면 방향', v ? '세로 방향으로 고정됩니다.' : '자동 회전이 활성화됩니다.')
|
|
}
|
|
|
|
return (
|
|
<ScrollView style={s.container} contentContainerStyle={{ paddingBottom: 30 }}>
|
|
|
|
{/* 다크모드 */}
|
|
<Text style={s.section}>테마</Text>
|
|
<View style={s.row}>
|
|
<Text style={s.label}>다크모드</Text>
|
|
<Switch value={isDark} onValueChange={toggleTheme} trackColor={{ true: COLORS.accent }} />
|
|
</View>
|
|
|
|
{/* 글자 크기 */}
|
|
<Text style={s.section}>글자 크기</Text>
|
|
{([1.0, 1.2, 1.5] as const).map(scale => (
|
|
<TouchableOpacity key={scale} style={[s.option, fontScale === scale && s.optionActive]} onPress={() => setFontScale(scale)}>
|
|
<Text style={[s.optionText, fontScale === scale && s.optionTextActive, { fontSize: 14 * scale }]}>
|
|
{scale === 1.0 ? '작게 (기본)' : scale === 1.2 ? '보통' : '크게'}
|
|
</Text>
|
|
{fontScale === scale && <Text style={s.check}>✓</Text>}
|
|
</TouchableOpacity>
|
|
))}
|
|
|
|
{/* 진동 패턴 */}
|
|
<Text style={s.section}>진동 패턴</Text>
|
|
{[['none', '없음'], ['short', '짧게'], ['long', '길게']].map(([v, label]) => (
|
|
<TouchableOpacity key={v} style={[s.option, vibration === v && s.optionActive]} onPress={() => saveVibration(v)}>
|
|
<Text style={[s.optionText, vibration === v && s.optionTextActive]}>{label}</Text>
|
|
{vibration === v && <Text style={s.check}>✓</Text>}
|
|
</TouchableOpacity>
|
|
))}
|
|
|
|
{/* 색맹 지원 */}
|
|
<Text style={s.section}>색맹 지원 팔레트</Text>
|
|
{[['default', '기본'], ['protanopia', '제1색맹 (적록)'], ['deuteranopia', '제2색맹 (녹적)']].map(([c, label]) => (
|
|
<TouchableOpacity key={c} style={[s.option, colorBlind === c && s.optionActive]} onPress={() => saveColorBlind(c)}>
|
|
<Text style={[s.optionText, colorBlind === c && s.optionTextActive]}>{label}</Text>
|
|
{colorBlind === c && <Text style={s.check}>✓</Text>}
|
|
</TouchableOpacity>
|
|
))}
|
|
|
|
{/* 화면 방향 잠금 */}
|
|
<Text style={s.section}>화면 방향</Text>
|
|
<View style={s.row}>
|
|
<Text style={s.label}>세로 방향 잠금</Text>
|
|
<Switch value={screenLock} onValueChange={toggleScreenLock} trackColor={{ true: COLORS.accent }} />
|
|
</View>
|
|
</ScrollView>
|
|
)
|
|
}
|
|
|
|
const s = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: COLORS.bg },
|
|
section: { fontSize: 13, fontWeight: '700', color: COLORS.muted, paddingHorizontal: 16, paddingTop: 20, paddingBottom: 6, textTransform: 'uppercase', letterSpacing: 1 },
|
|
row: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', backgroundColor: '#fff', paddingHorizontal: 16, paddingVertical: 14, borderBottomWidth: 1, borderBottomColor: COLORS.border },
|
|
label: { fontSize: 15, color: COLORS.text },
|
|
option: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', backgroundColor: '#fff', paddingHorizontal: 16, paddingVertical: 14, borderBottomWidth: 1, borderBottomColor: COLORS.border },
|
|
optionActive: { backgroundColor: COLORS.light },
|
|
optionText: { fontSize: 15, color: COLORS.text },
|
|
optionTextActive:{ color: COLORS.accent, fontWeight: '600' },
|
|
check: { fontSize: 16, color: COLORS.accent },
|
|
})
|