This commit is contained in:
2026-03-28 23:51:50 -04:00
parent 0a477300f4
commit e56e3ba531
47 changed files with 13489 additions and 201 deletions

548
src/components/settings.tsx Normal file
View File

@@ -0,0 +1,548 @@
import React from 'react';
import { ScrollView, StyleSheet, Switch, View, TextInput, Pressable } from 'react-native';
import { useColorScheme } from 'react-native';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { Collapsible } from '@/components/ui/collapsible';
import { SYNC_INTERVALS, SyncInterval, NotificationPreferences, ReadingPreferences } from '@/types/feed';
import { AccountSettings } from '@/types/global';
import { useTheme } from '@/hooks/use-theme';
import { useSettingsStore } from '@/stores/settings-store';
export default function SettingsScreen() {
const colorScheme = useColorScheme();
const theme = useTheme();
const {
syncInterval,
setSyncInterval,
theme: themeSetting,
setTheme,
notificationPreferences,
setNotificationPreferences,
readingPreferences,
setReadingPreferences,
accountSettings,
setAccountSettings,
} = useSettingsStore();
// Sync interval handler
const handleSyncIntervalChange = (value: number) => {
const interval = SYNC_INTERVALS.find((i) => i.value === value);
if (interval) {
setSyncInterval(interval);
}
};
// Notification handlers
const handleNotificationToggle = (key: keyof NotificationPreferences, value: boolean) => {
setNotificationPreferences({ [key]: value });
};
// Reading preference handlers
const handleReadingToggle = (key: keyof ReadingPreferences, value: boolean) => {
setReadingPreferences({ [key]: value });
};
const handleFontSizeChange = (size: ReadingPreferences['fontSize']) => {
setReadingPreferences({ fontSize: size });
};
const handleLineHeightChange = (height: ReadingPreferences['lineHeight']) => {
setReadingPreferences({ lineHeight: height });
};
// Theme handler
const handleThemeChange = (theme: 'light' | 'dark' | 'system') => {
setTheme(theme);
};
// Account settings handlers
const handleAccountSettingChange = (key: keyof AccountSettings, value: string | boolean) => {
setAccountSettings({ [key]: value });
};
return (
<ThemedView style={styles.container}>
<ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContent}>
{/* Sync Settings */}
<ThemedView style={styles.section}>
<ThemedText type="section" style={styles.sectionTitle}>Sync Settings</ThemedText>
<ThemedText type="defaultSemiBold" style={styles.sectionDescription}>
Choose how often your feeds should be updated.
</ThemedText>
<View style={styles.intervalContainer}>
{SYNC_INTERVALS.map((interval) => (
<Pressable
key={interval.value}
onPress={() => handleSyncIntervalChange(interval.value)}
style={({ pressed }) => [
styles.intervalItem,
{
backgroundColor:
syncInterval.value === interval.value
? theme.backgroundSelected
: theme.backgroundElement,
opacity: pressed ? 0.7 : 1,
},
]}
>
<ThemedText style={styles.intervalLabel}>
{interval.label}
</ThemedText>
{syncInterval.value === interval.value && (
<View style={styles.checkmark}>
<View style={styles.checkmarkCircle} />
</View>
)}
</Pressable>
))}
</View>
</ThemedView>
{/* Theme */}
<ThemedView style={styles.section}>
<ThemedText type="section" style={styles.sectionTitle}>Theme</ThemedText>
<ThemedText type="defaultSemiBold" style={styles.sectionDescription}>
Choose your preferred appearance.
</ThemedText>
<View style={styles.themeContainer}>
<Pressable
onPress={() => handleThemeChange('light')}
style={({ pressed }) => [
styles.themeButton,
{ opacity: pressed ? 0.7 : 1 },
]}
>
<View style={[
styles.themeCircle,
{ backgroundColor: themeSetting === 'light' ? '#ffffff' : theme.backgroundElement }
]} />
<ThemedText style={styles.themeLabel}>Light</ThemedText>
</Pressable>
<Pressable
onPress={() => handleThemeChange('dark')}
style={({ pressed }) => [
styles.themeButton,
{ opacity: pressed ? 0.7 : 1 },
]}
>
<View style={[
styles.themeCircle,
{ backgroundColor: themeSetting === 'dark' ? '#000000' : theme.backgroundElement }
]} />
<ThemedText style={styles.themeLabel}>Dark</ThemedText>
</Pressable>
<Pressable
onPress={() => handleThemeChange('system')}
style={({ pressed }) => [
styles.themeButton,
{ opacity: pressed ? 0.7 : 1 },
]}
>
<View style={[
styles.themeCircle,
{ backgroundColor: themeSetting === 'system' ? theme.backgroundSelected : theme.backgroundElement }
]} />
<ThemedText style={styles.themeLabel}>System</ThemedText>
</Pressable>
</View>
<ThemedView type="backgroundElement" style={styles.warningBox}>
<ThemedText type="smallBold" style={styles.warningTitle}> Note</ThemedText>
<ThemedText type="small" style={styles.warningText}>
Changing theme will apply on app restart.
</ThemedText>
</ThemedView>
</ThemedView>
{/* Notifications */}
<ThemedView style={styles.section}>
<ThemedText type="section" style={styles.sectionTitle}>Notifications</ThemedText>
<ThemedText type="defaultSemiBold" style={styles.sectionDescription}>
Manage how you receive notifications.
</ThemedText>
<ThemedView style={styles.prefGroup}>
<View style={styles.prefHeader}>
<ThemedText style={styles.prefTitle}>New Articles</ThemedText>
<Switch
value={notificationPreferences.newArticles}
onValueChange={(value) => handleNotificationToggle('newArticles', value)}
trackColor={{ false: theme.textSecondary, true: theme.text }}
thumbColor="white"
/>
</View>
</ThemedView>
<ThemedView style={styles.prefGroup}>
<View style={styles.prefHeader}>
<ThemedText style={styles.prefTitle}>Episode Releases</ThemedText>
<Switch
value={notificationPreferences.episodeReleases}
onValueChange={(value) => handleNotificationToggle('episodeReleases', value)}
trackColor={{ false: theme.textSecondary, true: theme.text }}
thumbColor="white"
/>
</View>
</ThemedView>
<ThemedView style={styles.prefGroup}>
<View style={styles.prefHeader}>
<ThemedText style={styles.prefTitle}>Custom Alerts</ThemedText>
<Switch
value={notificationPreferences.customAlerts}
onValueChange={(value) => handleNotificationToggle('customAlerts', value)}
trackColor={{ false: theme.textSecondary, true: theme.text }}
thumbColor="white"
/>
</View>
</ThemedView>
<Collapsible title="Sound & Vibration">
<ThemedView style={styles.prefGroup}>
<View style={styles.prefHeader}>
<ThemedText style={styles.prefTitle}>Sound</ThemedText>
<Switch
value={notificationPreferences.sound}
onValueChange={(value) => handleNotificationToggle('sound', value)}
trackColor={{ false: theme.textSecondary, true: theme.text }}
thumbColor="white"
/>
</View>
</ThemedView>
<ThemedView style={styles.prefGroup}>
<View style={styles.prefHeader}>
<ThemedText style={styles.prefTitle}>Vibration</ThemedText>
<Switch
value={notificationPreferences.vibration}
onValueChange={(value) => handleNotificationToggle('vibration', value)}
trackColor={{ false: theme.textSecondary, true: theme.text }}
thumbColor="white"
/>
</View>
</ThemedView>
</Collapsible>
</ThemedView>
{/* Reading Preferences */}
<ThemedView style={styles.section}>
<ThemedText type="section" style={styles.sectionTitle}>Reading Preferences</ThemedText>
<ThemedText type="defaultSemiBold" style={styles.sectionDescription}>
Customize your reading experience.
</ThemedText>
<ThemedView style={styles.prefGroup}>
<ThemedText style={styles.prefTitle}>Font Size</ThemedText>
<View style={styles.fontSizeSelector}>
{(['small', 'medium', 'large', 'xlarge'] as ReadingPreferences['fontSize'][]).map((size) => (
<Pressable
key={size}
onPress={() => handleFontSizeChange(size)}
style={({ pressed }) => [
styles.fontSizeButton,
{
backgroundColor:
readingPreferences.fontSize === size
? theme.backgroundSelected
: theme.backgroundElement,
opacity: pressed ? 0.7 : 1,
},
]}
>
<ThemedText style={[
styles.fontSizeLabel,
size === 'small' && { fontSize: 12 },
size === 'medium' && { fontSize: 16 },
size === 'large' && { fontSize: 20 },
size === 'xlarge' && { fontSize: 24 }
]}>
{size === 'small' ? 'A' : size === 'medium' ? 'B' : size === 'large' ? 'C' : 'D'}
</ThemedText>
</Pressable>
))}
</View>
</ThemedView>
<ThemedView style={styles.prefGroup}>
<ThemedText style={styles.prefTitle}>Line Height</ThemedText>
<View style={styles.lineHeightSelector}>
{(['normal', 'relaxed', 'loose'] as ReadingPreferences['lineHeight'][]).map((height) => (
<Pressable
key={height}
onPress={() => handleLineHeightChange(height)}
style={({ pressed }) => [
styles.lineHeightButton,
{
backgroundColor:
readingPreferences.lineHeight === height
? theme.backgroundSelected
: theme.backgroundElement,
opacity: pressed ? 0.7 : 1,
},
]}
>
<ThemedText style={styles.lineHeightLabel}>{height.charAt(0).toUpperCase() + height.slice(1)}</ThemedText>
</Pressable>
))}
</View>
</ThemedView>
<ThemedView style={styles.prefGroup}>
<View style={styles.prefHeader}>
<ThemedText style={styles.prefTitle}>Show Reading Time</ThemedText>
<Switch
value={readingPreferences.showReadingTime}
onValueChange={(value) => handleReadingToggle('showReadingTime', value)}
trackColor={{ false: theme.textSecondary, true: theme.text }}
thumbColor="white"
/>
</View>
</ThemedView>
<ThemedView style={styles.prefGroup}>
<View style={styles.prefHeader}>
<ThemedText style={styles.prefTitle}>Show Author</ThemedText>
<Switch
value={readingPreferences.showAuthor}
onValueChange={(value) => handleReadingToggle('showAuthor', value)}
trackColor={{ false: theme.textSecondary, true: theme.text }}
thumbColor="white"
/>
</View>
</ThemedView>
<ThemedView style={styles.prefGroup}>
<View style={styles.prefHeader}>
<ThemedText style={styles.prefTitle}>Show Date</ThemedText>
<Switch
value={readingPreferences.showDate}
onValueChange={(value) => handleReadingToggle('showDate', value)}
trackColor={{ false: theme.textSecondary, true: theme.text }}
thumbColor="white"
/>
</View>
</ThemedView>
</ThemedView>
{/* Account Settings */}
<ThemedView style={styles.section}>
<ThemedText type="section" style={styles.sectionTitle}>Account Settings</ThemedText>
<ThemedText type="defaultSemiBold" style={styles.sectionDescription}>
Manage your account preferences.
</ThemedText>
<ThemedView style={styles.prefGroup}>
<ThemedText style={styles.prefTitle}>Email</ThemedText>
<TextInput
style={[styles.input, { backgroundColor: theme.backgroundElement }]}
placeholder="Enter your email"
value={accountSettings.email}
onChangeText={(text) => handleAccountSettingChange('email', text)}
keyboardType="email-address"
autoCapitalize="none"
/>
</ThemedView>
<ThemedView style={styles.prefGroup}>
<ThemedText style={styles.prefTitle}>Privacy Level</ThemedText>
<View style={styles.privacySelector}>
{(['public', 'private', 'anonymous'] as AccountSettings['privacy'][]).map((privacy) => (
<Pressable
key={privacy}
onPress={() => handleAccountSettingChange('privacy', privacy)}
style={({ pressed }) => [
styles.privacyButton,
{
backgroundColor:
accountSettings.privacy === privacy
? theme.backgroundSelected
: theme.backgroundElement,
opacity: pressed ? 0.7 : 1,
},
]}
>
<ThemedText style={styles.privacyLabel}>
{privacy.charAt(0).toUpperCase() + privacy.slice(1)}
</ThemedText>
</Pressable>
))}
</View>
</ThemedView>
<ThemedView style={styles.prefGroup}>
<ThemedText style={styles.prefTitle}>Language</ThemedText>
<TextInput
style={[styles.input, { backgroundColor: theme.backgroundElement }]}
placeholder="e.g., en-US"
value={accountSettings.language}
onChangeText={(text) => handleAccountSettingChange('language', text)}
/>
</ThemedView>
<ThemedView style={styles.prefGroup}>
<ThemedText style={styles.prefTitle}>Timezone</ThemedText>
<TextInput
style={styles.input}
placeholder="e.g., UTC"
value={accountSettings.timezone}
onChangeText={(text) => handleAccountSettingChange('timezone', text)}
/>
</ThemedView>
<ThemedView style={styles.prefGroup}>
<View style={styles.prefHeader}>
<ThemedText style={styles.prefTitle}>Notifications Enabled</ThemedText>
<Switch
value={accountSettings.notificationsEnabled}
onValueChange={(value) => handleAccountSettingChange('notificationsEnabled', value)}
trackColor={{ false: theme.textSecondary, true: theme.text }}
thumbColor="white"
/>
</View>
</ThemedView>
</ThemedView>
</ScrollView>
</ThemedView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
scrollView: {
flex: 1,
},
scrollContent: {
paddingBottom: 24,
},
section: {
marginBottom: 24,
paddingHorizontal: 16,
},
sectionTitle: {
marginBottom: 4,
},
sectionDescription: {
marginBottom: 16,
},
intervalContainer: {
gap: 8,
},
intervalItem: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 16,
borderRadius: 12,
gap: 12,
},
intervalLabel: {
fontSize: 14,
},
checkmark: {
width: 20,
height: 20,
borderRadius: 10,
justifyContent: 'center',
alignItems: 'center',
},
checkmarkCircle: {
width: 12,
height: 12,
borderRadius: 6,
backgroundColor: '#4CAF50',
},
themeContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center',
marginBottom: 16,
},
themeButton: {
alignItems: 'center',
gap: 4,
},
themeCircle: {
width: 40,
height: 40,
borderRadius: 20,
},
themeLabel: {
fontSize: 12,
},
warningBox: {
padding: 12,
borderRadius: 8,
marginBottom: 16,
},
warningTitle: {
marginBottom: 4,
},
warningText: {},
prefGroup: {
marginBottom: 8,
},
prefHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
prefTitle: {
fontSize: 14,
},
fontSizeSelector: {
flexDirection: 'row',
gap: 8,
},
fontSizeButton: {
padding: 12,
borderRadius: 8,
minWidth: 44,
alignItems: 'center',
justifyContent: 'center',
},
fontSizeLabel: {
fontWeight: 'bold',
},
lineHeightSelector: {
flexDirection: 'row',
gap: 8,
},
lineHeightButton: {
flex: 1,
padding: 12,
borderRadius: 8,
alignItems: 'center',
},
lineHeightLabel: {
fontSize: 12,
textAlign: 'center',
},
input: {
borderRadius: 8,
padding: 12,
fontSize: 14,
marginTop: 4,
},
privacySelector: {
flexDirection: 'row',
gap: 8,
marginTop: 4,
},
privacyButton: {
flex: 1,
padding: 12,
borderRadius: 8,
alignItems: 'center',
},
privacyLabel: {
fontSize: 14,
textAlign: 'center',
},
});