Dev #35
@@ -74,7 +74,9 @@ export default function SettingsScreen() {
|
|||||||
loadSettingsFromServer,
|
loadSettingsFromServer,
|
||||||
} = useSettings();
|
} = useSettings();
|
||||||
const categories = useCategories();
|
const categories = useCategories();
|
||||||
const [picker, setPicker] = useState<null | 'category' | 'sort' | 'reminder' | 'todoAhead'>(null);
|
const [picker, setPicker] = useState<
|
||||||
|
null | 'category' | 'sort' | 'reminder' | 'todoAhead' | 'themeMode'
|
||||||
|
>(null);
|
||||||
const [reminderTagPicker, setReminderTagPicker] = useState<Category | null>(null);
|
const [reminderTagPicker, setReminderTagPicker] = useState<Category | null>(null);
|
||||||
const [editingCategory, setEditingCategory] = useState<Category | null | 'new'>(null);
|
const [editingCategory, setEditingCategory] = useState<Category | null | 'new'>(null);
|
||||||
const [syncVisible, setSyncVisible] = useState(false);
|
const [syncVisible, setSyncVisible] = useState(false);
|
||||||
@@ -245,7 +247,7 @@ export default function SettingsScreen() {
|
|||||||
rightElement={
|
rightElement={
|
||||||
<Switch
|
<Switch
|
||||||
value={notifications}
|
value={notifications}
|
||||||
onValueChange={async (value) => {
|
onValueChange={async value => {
|
||||||
setNotifications(value);
|
setNotifications(value);
|
||||||
if (!value) {
|
if (!value) {
|
||||||
const { cancelAllReminders } = await import('@/services/notifications');
|
const { cancelAllReminders } = await import('@/services/notifications');
|
||||||
@@ -358,6 +360,15 @@ export default function SettingsScreen() {
|
|||||||
onPress={openCustomAccent}
|
onPress={openCustomAccent}
|
||||||
showChevron
|
showChevron
|
||||||
/>
|
/>
|
||||||
|
<Text style={[styles.sectionHint, { color: theme.textFaint }]}>Theme</Text>
|
||||||
|
<ListItem
|
||||||
|
title="Theme Mode"
|
||||||
|
subtitle={
|
||||||
|
themeMode === 'system' ? 'System default' : themeMode === 'dark' ? 'Dark' : 'Light'
|
||||||
|
}
|
||||||
|
onPress={() => setPicker('themeMode')}
|
||||||
|
showChevron
|
||||||
|
/>
|
||||||
<Text style={[styles.sectionTitle, { color: theme.textMuted }]}>Data</Text>
|
<Text style={[styles.sectionTitle, { color: theme.textMuted }]}>Data</Text>
|
||||||
<SyncStatus />
|
<SyncStatus />
|
||||||
<ListItem
|
<ListItem
|
||||||
@@ -366,6 +377,32 @@ export default function SettingsScreen() {
|
|||||||
onPress={() => setSyncVisible(true)}
|
onPress={() => setSyncVisible(true)}
|
||||||
showChevron
|
showChevron
|
||||||
/>
|
/>
|
||||||
|
<ListItem
|
||||||
|
title="Sync Settings to Server"
|
||||||
|
subtitle="Push local settings to backend"
|
||||||
|
onPress={async () => {
|
||||||
|
try {
|
||||||
|
await syncSettingsToServer();
|
||||||
|
Alert.alert('Success', 'Settings synced to server');
|
||||||
|
} catch {
|
||||||
|
Alert.alert('Error', 'Failed to sync settings');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
showChevron
|
||||||
|
/>
|
||||||
|
<ListItem
|
||||||
|
title="Load Settings from Server"
|
||||||
|
subtitle="Pull settings from backend"
|
||||||
|
onPress={async () => {
|
||||||
|
try {
|
||||||
|
await loadSettingsFromServer();
|
||||||
|
Alert.alert('Success', 'Settings loaded from server');
|
||||||
|
} catch {
|
||||||
|
Alert.alert('Error', 'Failed to load settings');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
showChevron
|
||||||
|
/>
|
||||||
<ListItem
|
<ListItem
|
||||||
title="Friends"
|
title="Friends"
|
||||||
subtitle="Manage friends and requests"
|
subtitle="Manage friends and requests"
|
||||||
@@ -444,6 +481,15 @@ export default function SettingsScreen() {
|
|||||||
onClose={() => setPicker(null)}
|
onClose={() => setPicker(null)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<OptionPickerModal
|
||||||
|
visible={picker === 'themeMode'}
|
||||||
|
title="Theme Mode"
|
||||||
|
options={THEME_MODE_OPTIONS}
|
||||||
|
selectedValue={themeMode}
|
||||||
|
onSelect={value => setThemeMode(value as ThemeMode)}
|
||||||
|
onClose={() => setPicker(null)}
|
||||||
|
/>
|
||||||
|
|
||||||
<CategoryEditorModal
|
<CategoryEditorModal
|
||||||
visible={editorVisible}
|
visible={editorVisible}
|
||||||
category={editorCategory}
|
category={editorCategory}
|
||||||
@@ -551,6 +597,12 @@ const AHEAD_OPTIONS: { value: string; label: string }[] = Array.from({ length: 2
|
|||||||
label: i === 0 ? 'Only today' : `${i} day${i === 1 ? '' : 's'}`,
|
label: i === 0 ? 'Only today' : `${i} day${i === 1 ? '' : 's'}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const THEME_MODE_OPTIONS: { value: ThemeMode; label: string }[] = [
|
||||||
|
{ value: 'light', label: 'Light' },
|
||||||
|
{ value: 'dark', label: 'Dark' },
|
||||||
|
{ value: 'system', label: 'System default' },
|
||||||
|
];
|
||||||
|
|
||||||
function formatSyncTime(timestamp: number): string {
|
function formatSyncTime(timestamp: number): string {
|
||||||
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
||||||
if (seconds < 60) return 'just now';
|
if (seconds < 60) return 'just now';
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState, ReactNode } from 'react';
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
ReactNode,
|
||||||
|
} from 'react';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { DEFAULT_API_BASE_URL, API_URL_KEY } from '@/services/auth';
|
import { DEFAULT_API_BASE_URL, API_URL_KEY } from '@/services/auth';
|
||||||
import { Platform } from 'react-native';
|
import { Platform } from 'react-native';
|
||||||
@@ -110,9 +118,14 @@ function accentTextColor(accent: string): string {
|
|||||||
return luminance > 0.5 ? '#111111' : '#FFFFFF';
|
return luminance > 0.5 ? '#111111' : '#FFFFFF';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function colors(accent: string = DEFAULT_ACCENT, mode: ThemeMode = 'dark', opacity: number = 1): ThemeColors {
|
export function colors(
|
||||||
|
accent: string = DEFAULT_ACCENT,
|
||||||
|
mode: ThemeMode = 'dark',
|
||||||
|
opacity: number = 1
|
||||||
|
): ThemeColors {
|
||||||
const softAccent = desaturate(accent, 0.35);
|
const softAccent = desaturate(accent, 0.35);
|
||||||
const accentWithOpacity = opacity < 1 ? `${opacity.toString(16).padStart(2, '0')}${accent.slice(1)}` : accent;
|
const accentWithOpacity =
|
||||||
|
opacity < 1 ? `${opacity.toString(16).padStart(2, '0')}${accent.slice(1)}` : accent;
|
||||||
|
|
||||||
if (mode === 'light') {
|
if (mode === 'light') {
|
||||||
return {
|
return {
|
||||||
@@ -289,34 +302,52 @@ export function SettingsProvider({ children }: { children: ReactNode }) {
|
|||||||
STORAGE_KEYS.reminderPreference,
|
STORAGE_KEYS.reminderPreference,
|
||||||
'15'
|
'15'
|
||||||
);
|
);
|
||||||
const [tagReminderPreferences, setTagReminderPreferences] = useStoredSetting<TagReminderPreferences>(
|
const [tagReminderPreferences, setTagReminderPreferences] =
|
||||||
STORAGE_KEYS.tagReminderPreferences,
|
useStoredSetting<TagReminderPreferences>(STORAGE_KEYS.tagReminderPreferences, {});
|
||||||
{}
|
|
||||||
);
|
|
||||||
const [apiUrl, setApiUrl] = useStoredSetting<string>(API_URL_KEY, DEFAULT_API_BASE_URL);
|
const [apiUrl, setApiUrl] = useStoredSetting<string>(API_URL_KEY, DEFAULT_API_BASE_URL);
|
||||||
const [accentColor, setAccentColor] = useStoredSetting<string>(STORAGE_KEYS.accentColor, DEFAULT_ACCENT);
|
const [accentColor, setAccentColor] = useStoredSetting<string>(
|
||||||
const [accentOpacity, setAccentOpacity] = useStoredSetting<number>(STORAGE_KEYS.accentOpacity, DEFAULT_ACCENT_OPACITY);
|
STORAGE_KEYS.accentColor,
|
||||||
const [todoAheadDays, setTodoAheadDays] = useStoredSetting<number>(STORAGE_KEYS.todoAheadDays, DEFAULT_TODO_AHEAD_DAYS);
|
DEFAULT_ACCENT
|
||||||
const [showCompleted, setShowCompleted] = useStoredSetting<boolean>(STORAGE_KEYS.showCompleted, false);
|
);
|
||||||
const [deleteBySwipe, setDeleteBySwipe] = useStoredSetting<boolean>(STORAGE_KEYS.deleteBySwipe, false);
|
const [accentOpacity, setAccentOpacity] = useStoredSetting<number>(
|
||||||
|
STORAGE_KEYS.accentOpacity,
|
||||||
|
DEFAULT_ACCENT_OPACITY
|
||||||
|
);
|
||||||
|
const [todoAheadDays, setTodoAheadDays] = useStoredSetting<number>(
|
||||||
|
STORAGE_KEYS.todoAheadDays,
|
||||||
|
DEFAULT_TODO_AHEAD_DAYS
|
||||||
|
);
|
||||||
|
const [showCompleted, setShowCompleted] = useStoredSetting<boolean>(
|
||||||
|
STORAGE_KEYS.showCompleted,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
const [deleteBySwipe, setDeleteBySwipe] = useStoredSetting<boolean>(
|
||||||
|
STORAGE_KEYS.deleteBySwipe,
|
||||||
|
false
|
||||||
|
);
|
||||||
const [themeMode, setThemeMode, themeModeLoaded] = useStoredSetting<ThemeMode>(
|
const [themeMode, setThemeMode, themeModeLoaded] = useStoredSetting<ThemeMode>(
|
||||||
STORAGE_KEYS.themeMode,
|
STORAGE_KEYS.themeMode,
|
||||||
'system'
|
'system'
|
||||||
);
|
);
|
||||||
|
|
||||||
const effectiveMode = themeMode === 'system'
|
const effectiveMode: 'light' | 'dark' =
|
||||||
? (Platform.OS === 'web' ? 'dark' : 'dark') // Will be updated by useColorScheme in real app
|
themeMode === 'system'
|
||||||
: themeMode;
|
? 'dark' // Will be updated by useColorScheme in real app
|
||||||
|
: themeMode;
|
||||||
|
|
||||||
const theme = useMemo(() => colors(accentColor, effectiveMode, accentOpacity), [accentColor, effectiveMode, accentOpacity]);
|
const theme = useMemo(
|
||||||
|
() => colors(accentColor, effectiveMode, accentOpacity),
|
||||||
|
[accentColor, effectiveMode, accentOpacity]
|
||||||
|
);
|
||||||
|
|
||||||
const syncSettingsToServer = useCallback(async () => {
|
const syncSettingsToServer = useCallback(async () => {
|
||||||
const { getAuthToken } = await import('@/services/auth');
|
const { getAuthToken } = await import('@/services/auth');
|
||||||
const token = await getAuthToken();
|
const token = await getAuthToken();
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
|
||||||
|
const isDark = effectiveMode === 'dark';
|
||||||
const settings = {
|
const settings = {
|
||||||
darkMode: effectiveMode === 'dark' || (themeMode === 'system' && effectiveMode === 'dark'),
|
darkMode: isDark,
|
||||||
defaultCategoryId,
|
defaultCategoryId,
|
||||||
sortOrder: sortBy,
|
sortOrder: sortBy,
|
||||||
reminderPreference,
|
reminderPreference,
|
||||||
@@ -325,7 +356,17 @@ export function SettingsProvider({ children }: { children: ReactNode }) {
|
|||||||
deleteBySwipe,
|
deleteBySwipe,
|
||||||
};
|
};
|
||||||
await pushServerSettings(apiUrl, token, settings);
|
await pushServerSettings(apiUrl, token, settings);
|
||||||
}, [apiUrl, effectiveMode, themeMode, defaultCategoryId, sortBy, reminderPreference, todoAheadDays, showCompleted, deleteBySwipe]);
|
}, [
|
||||||
|
apiUrl,
|
||||||
|
effectiveMode,
|
||||||
|
themeMode,
|
||||||
|
defaultCategoryId,
|
||||||
|
sortBy,
|
||||||
|
reminderPreference,
|
||||||
|
todoAheadDays,
|
||||||
|
showCompleted,
|
||||||
|
deleteBySwipe,
|
||||||
|
]);
|
||||||
|
|
||||||
const loadSettingsFromServer = useCallback(async () => {
|
const loadSettingsFromServer = useCallback(async () => {
|
||||||
const { getAuthToken } = await import('@/services/auth');
|
const { getAuthToken } = await import('@/services/auth');
|
||||||
@@ -344,7 +385,16 @@ export function SettingsProvider({ children }: { children: ReactNode }) {
|
|||||||
if (serverSettings.todoAheadDays !== undefined) setTodoAheadDays(serverSettings.todoAheadDays);
|
if (serverSettings.todoAheadDays !== undefined) setTodoAheadDays(serverSettings.todoAheadDays);
|
||||||
if (serverSettings.showCompleted !== undefined) setShowCompleted(serverSettings.showCompleted);
|
if (serverSettings.showCompleted !== undefined) setShowCompleted(serverSettings.showCompleted);
|
||||||
if (serverSettings.deleteBySwipe !== undefined) setDeleteBySwipe(serverSettings.deleteBySwipe);
|
if (serverSettings.deleteBySwipe !== undefined) setDeleteBySwipe(serverSettings.deleteBySwipe);
|
||||||
}, [apiUrl, setThemeMode, setDefaultCategoryId, setSortBy, setReminderPreference, setTodoAheadDays, setShowCompleted, setDeleteBySwipe]);
|
}, [
|
||||||
|
apiUrl,
|
||||||
|
setThemeMode,
|
||||||
|
setDefaultCategoryId,
|
||||||
|
setSortBy,
|
||||||
|
setReminderPreference,
|
||||||
|
setTodoAheadDays,
|
||||||
|
setShowCompleted,
|
||||||
|
setDeleteBySwipe,
|
||||||
|
]);
|
||||||
|
|
||||||
const value = useMemo<SettingsContextType>(
|
const value = useMemo<SettingsContextType>(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -407,11 +457,7 @@ export function SettingsProvider({ children }: { children: ReactNode }) {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return <SettingsContext.Provider value={value}>{children}</SettingsContext.Provider>;
|
||||||
<SettingsContext.Provider value={value}>
|
|
||||||
{children}
|
|
||||||
</SettingsContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSettings() {
|
export function useSettings() {
|
||||||
|
|||||||
Reference in New Issue
Block a user