functioning apk with reworked calendar
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, StyleSheet, SafeAreaView, ScrollView, Switch, TouchableOpacity, Alert, Linking } from 'react-native';
|
||||
import { View, Text, StyleSheet, SafeAreaView, ScrollView, Switch, TouchableOpacity, Alert, Linking, Modal, Pressable } from 'react-native';
|
||||
import { Header } from '@/components/Header';
|
||||
import { ListItem } from '@/components/ListItem';
|
||||
import { OptionPickerModal } from '@/components/OptionPickerModal';
|
||||
@@ -9,16 +9,17 @@ import { FriendsModal } from '@/components/FriendsModal';
|
||||
import { LegalModal } from '@/components/LegalModal';
|
||||
import { ServerUrlModal } from '@/components/ServerUrlModal';
|
||||
import SyncStatus from '@/components/SyncStatus';
|
||||
import { useSettings, SORT_OPTIONS, REMINDER_OPTIONS } from '@/theme';
|
||||
import { useSettings, SORT_OPTIONS, REMINDER_OPTIONS, ACCENT_PRESETS, DEFAULT_ACCENT } from '@/theme';
|
||||
import { useCategories } from '@/hooks/useDatabase';
|
||||
import { getAuthUser, getAuthToken } from '@/services/auth';
|
||||
import { checkForUpdates, getCurrentAppVersion } from '@/services/updates';
|
||||
import { getLastSyncTime } from '@/database/sync';
|
||||
import Category from '@/models/Category';
|
||||
import Svg, { Path } from 'react-native-svg';
|
||||
import ColorWheel from '@/components/ColorWheel';
|
||||
|
||||
export default function SettingsScreen() {
|
||||
const { theme, notifications, setNotifications, defaultCategoryId, setDefaultCategoryId, sortBy, setSortBy, reminderPreference, setReminderPreference, apiUrl } = useSettings();
|
||||
const { theme, notifications, setNotifications, defaultCategoryId, setDefaultCategoryId, sortBy, setSortBy, reminderPreference, setReminderPreference, apiUrl, accentColor, setAccentColor } = useSettings();
|
||||
const categories = useCategories();
|
||||
const [picker, setPicker] = useState<null | 'category' | 'sort' | 'reminder'>(null);
|
||||
const [editingCategory, setEditingCategory] = useState<Category | null | 'new'>(null);
|
||||
@@ -26,6 +27,8 @@ export default function SettingsScreen() {
|
||||
const [friendsVisible, setFriendsVisible] = useState(false);
|
||||
const [legalVisible, setLegalVisible] = useState<null | 'privacy' | 'terms'>(null);
|
||||
const [serverUrlVisible, setServerUrlVisible] = useState(false);
|
||||
const [customAccentVisible, setCustomAccentVisible] = useState(false);
|
||||
const [draftAccent, setDraftAccent] = useState(accentColor);
|
||||
const [syncSubtitle, setSyncSubtitle] = useState('Checking...');
|
||||
const [updateSubtitle, setUpdateSubtitle] = useState('Tap to check');
|
||||
|
||||
@@ -76,6 +79,11 @@ export default function SettingsScreen() {
|
||||
const defaultCategoryLabel = defaultCategory?.name ?? (categories.length > 0 ? categories[0].name : 'None');
|
||||
const reminderLabel = REMINDER_OPTIONS.find((o) => o.value === reminderPreference)?.label ?? 'No reminder';
|
||||
|
||||
const openCustomAccent = () => {
|
||||
setDraftAccent(accentColor);
|
||||
setCustomAccentVisible(true);
|
||||
};
|
||||
|
||||
const handleDefaultCategory = (value: string | string[]) => {
|
||||
setDefaultCategoryId(Array.isArray(value) ? value[0] : value);
|
||||
};
|
||||
@@ -148,6 +156,35 @@ export default function SettingsScreen() {
|
||||
onPress={() => setPicker('sort')}
|
||||
showChevron
|
||||
/>
|
||||
<Text style={[styles.sectionTitle, { color: theme.textMuted }]}>Appearance</Text>
|
||||
<Text style={[styles.sectionHint, { color: theme.textFaint }]}>Accent color</Text>
|
||||
<View style={styles.accentPresets}>
|
||||
{ACCENT_PRESETS.map((c) => (
|
||||
<TouchableOpacity
|
||||
key={c}
|
||||
style={[
|
||||
styles.accentSwatch,
|
||||
{ backgroundColor: c },
|
||||
accentColor.toUpperCase() === c && styles.accentSwatchSelected,
|
||||
]}
|
||||
onPress={() => setAccentColor(c)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{accentColor.toUpperCase() === c && (
|
||||
<Svg width={16} height={16} viewBox="0 0 24 24">
|
||||
<Path d="M5 12l5 5 9-10" stroke={contrastOnSwatch(c)} strokeWidth={3} strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
</Svg>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
<ListItem
|
||||
title="Custom Color"
|
||||
subtitle={accentColor}
|
||||
leftElement={<View style={[styles.categoryDot, { backgroundColor: accentColor }]} />}
|
||||
onPress={openCustomAccent}
|
||||
showChevron
|
||||
/>
|
||||
<Text style={[styles.sectionTitle, { color: theme.textMuted }]}>Data</Text>
|
||||
<SyncStatus />
|
||||
<ListItem
|
||||
@@ -232,6 +269,41 @@ export default function SettingsScreen() {
|
||||
|
||||
<ServerUrlModal visible={serverUrlVisible} onClose={() => setServerUrlVisible(false)} />
|
||||
|
||||
<Modal visible={customAccentVisible} transparent animationType="fade" onRequestClose={() => setCustomAccentVisible(false)}>
|
||||
<Pressable
|
||||
style={styles.accentModalOverlay}
|
||||
onPress={() => setCustomAccentVisible(false)}
|
||||
>
|
||||
<Pressable style={[styles.accentModalSheet, { backgroundColor: theme.sheetBg }]} onPress={() => {}}>
|
||||
<View style={styles.accentModalHeader}>
|
||||
<Text style={[styles.accentModalTitle, { color: theme.text }]}>Custom Accent Color</Text>
|
||||
<TouchableOpacity onPress={() => setCustomAccentVisible(false)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
|
||||
<Svg width={18} height={18} viewBox="0 0 24 24">
|
||||
<Path d="M18 6L6 18M6 6l12 12" stroke={theme.textMuted} strokeWidth={2} strokeLinecap="round" />
|
||||
</Svg>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<ColorWheel color={draftAccent} onChange={setDraftAccent} />
|
||||
<View style={styles.accentModalActions}>
|
||||
<TouchableOpacity
|
||||
style={[styles.accentModalButton, { borderColor: theme.borderStrong }]}
|
||||
onPress={() => { setDraftAccent(DEFAULT_ACCENT); setAccentColor(DEFAULT_ACCENT); }}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.accentModalButtonText, { color: theme.textSecondary }]}>Reset</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.accentModalButton, styles.accentModalButtonPrimary, { backgroundColor: theme.accent }]}
|
||||
onPress={() => { setAccentColor(draftAccent); setCustomAccentVisible(false); }}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={[styles.accentModalButtonText, { color: theme.accentText }]}>Apply</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
</Modal>
|
||||
|
||||
<LegalModal
|
||||
visible={legalVisible !== null}
|
||||
type={legalVisible}
|
||||
@@ -249,6 +321,16 @@ function formatSyncTime(timestamp: number): string {
|
||||
return new Date(timestamp).toLocaleDateString();
|
||||
}
|
||||
|
||||
function contrastOnSwatch(hex: string): string {
|
||||
const h = hex.replace(/^#/, '');
|
||||
const r = parseInt(h.slice(0, 2), 16) / 255;
|
||||
const g = parseInt(h.slice(2, 4), 16) / 255;
|
||||
const b = parseInt(h.slice(4, 6), 16) / 255;
|
||||
const linear = (v: number) => (v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4));
|
||||
const luminance = 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b);
|
||||
return luminance > 0.5 ? '#111111' : '#FFFFFF';
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
@@ -294,4 +376,73 @@ const styles = StyleSheet.create({
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
},
|
||||
sectionHint: {
|
||||
fontSize: 12,
|
||||
marginLeft: 4,
|
||||
marginBottom: 8,
|
||||
},
|
||||
accentPresets: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 10,
|
||||
paddingHorizontal: 4,
|
||||
marginBottom: 4,
|
||||
},
|
||||
accentSwatch: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
accentSwatchSelected: {
|
||||
borderWidth: 2,
|
||||
borderColor: '#FFFFFF',
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 3,
|
||||
elevation: 3,
|
||||
},
|
||||
accentModalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 24,
|
||||
},
|
||||
accentModalSheet: {
|
||||
width: '100%',
|
||||
maxWidth: 400,
|
||||
borderRadius: 16,
|
||||
padding: 20,
|
||||
},
|
||||
accentModalHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 16,
|
||||
},
|
||||
accentModalTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
},
|
||||
accentModalActions: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
marginTop: 16,
|
||||
},
|
||||
accentModalButton: {
|
||||
flex: 1,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
alignItems: 'center',
|
||||
},
|
||||
accentModalButtonPrimary: {
|
||||
borderWidth: 0,
|
||||
},
|
||||
accentModalButtonText: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user