Merge pull request 'fix: remove duplicate useRouter() declaration in CalendarScreen' (#16) from dev into main
Reviewed-on: #16
This commit was merged in pull request #16.
This commit is contained in:
@@ -218,7 +218,22 @@ export default function CalendarScreen() {
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
||||
<Header title="Calendar" showLogo={false} />
|
||||
<Header
|
||||
title="Calendar"
|
||||
showLogo={false}
|
||||
rightAction={
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
style={styles.backButton}
|
||||
accessible={true}
|
||||
accessibilityLabel="Back"
|
||||
>
|
||||
<Svg width={20} height={20} viewBox="0 0 24 24">
|
||||
<Path d="M15 18l-6-6 6-6" stroke="currentColor" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
</Svg>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
/>
|
||||
|
||||
<KeyboardAvoidingView style={styles.kbAvoid} behavior="padding">
|
||||
<ScrollView style={styles.scrollBody} showsVerticalScrollIndicator={false} contentContainerStyle={styles.scrollContent}>
|
||||
@@ -609,6 +624,9 @@ const createStyles = (theme: ThemeColors) => StyleSheet.create({
|
||||
emptySubtext: {
|
||||
fontSize: 13,
|
||||
},
|
||||
backButton: {
|
||||
padding: 8,
|
||||
},
|
||||
eventCard: {
|
||||
marginHorizontal: 16,
|
||||
marginTop: 6,
|
||||
|
||||
@@ -24,7 +24,7 @@ import { getLastVisitedTab, tabHref } from '@/utils/tabHistory';
|
||||
|
||||
export default function SettingsScreen() {
|
||||
const router = useRouter();
|
||||
const { theme, notifications, setNotifications, defaultCategoryId, setDefaultCategoryId, sortBy, setSortBy, reminderPreference, setReminderPreference, tagReminderPreferences, setTagReminderPreferences, apiUrl, accentColor, setAccentColor, todoAheadDays, setTodoAheadDays, showCompleted, setShowCompleted, deleteBySwipe, setDeleteBySwipe } = useSettings();
|
||||
const { theme, notifications, setNotifications, defaultCategoryId, setDefaultCategoryId, sortBy, setSortBy, reminderPreference, setReminderPreference, tagReminderPreferences, setTagReminderPreferences, apiUrl, accentColor, setAccentColor, accentOpacity, setAccentOpacity, todoAheadDays, setTodoAheadDays, showCompleted, setShowCompleted, deleteBySwipe, setDeleteBySwipe } = useSettings();
|
||||
const categories = useCategories();
|
||||
const [picker, setPicker] = useState<null | 'category' | 'sort' | 'reminder' | 'todoAhead'>(null);
|
||||
const [reminderTagPicker, setReminderTagPicker] = useState<Category | null>(null);
|
||||
@@ -35,6 +35,7 @@ export default function SettingsScreen() {
|
||||
const [serverUrlVisible, setServerUrlVisible] = useState(false);
|
||||
const [customAccentVisible, setCustomAccentVisible] = useState(false);
|
||||
const [draftAccent, setDraftAccent] = useState(accentColor);
|
||||
const [draftAccentOpacity, setDraftAccentOpacity] = useState(accentOpacity);
|
||||
const [syncSubtitle, setSyncSubtitle] = useState('Checking...');
|
||||
const [updateSubtitle, setUpdateSubtitle] = useState('Tap to check');
|
||||
|
||||
@@ -393,18 +394,18 @@ export default function SettingsScreen() {
|
||||
</Svg>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<ColorWheel color={draftAccent} onChange={setDraftAccent} />
|
||||
<ColorWheel color={draftAccent} opacity={draftAccentOpacity} onChange={(color, opacity) => { setDraftAccent(color); setDraftAccentOpacity(opacity); }} />
|
||||
<View style={styles.accentModalActions}>
|
||||
<TouchableOpacity
|
||||
style={[styles.accentModalButton, { borderColor: theme.borderStrong }]}
|
||||
onPress={() => { setDraftAccent(DEFAULT_ACCENT); setAccentColor(DEFAULT_ACCENT); }}
|
||||
onPress={() => { setDraftAccent(DEFAULT_ACCENT); setDraftAccentOpacity(DEFAULT_ACCENT_OPACITY); setAccentColor(DEFAULT_ACCENT); setAccentOpacity(DEFAULT_ACCENT_OPACITY); }}
|
||||
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); }}
|
||||
onPress={() => { setAccentColor(draftAccent); setAccentOpacity(draftAccentOpacity); setCustomAccentVisible(false); }}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={[styles.accentModalButtonText, { color: theme.accentText }]}>Apply</Text>
|
||||
|
||||
@@ -70,10 +70,11 @@ function hsvToHex(h: number, s: number, v: number) {
|
||||
|
||||
interface ColorWheelProps {
|
||||
color: string;
|
||||
onChange: (color: string) => void;
|
||||
opacity: number;
|
||||
onChange: (color: string, opacity: number) => void;
|
||||
}
|
||||
|
||||
export default function ColorWheel({ color, onChange }: ColorWheelProps) {
|
||||
export default function ColorWheel({ color, opacity, onChange }: ColorWheelProps) {
|
||||
const { theme } = useSettings();
|
||||
const initialHsv = useMemo(() => hexToHsv(color), [color]);
|
||||
const [hue, setHue] = useState(initialHsv.h);
|
||||
@@ -90,8 +91,8 @@ export default function ColorWheel({ color, onChange }: ColorWheelProps) {
|
||||
|
||||
useEffect(() => {
|
||||
const newColor = hsvToHex(hue, saturation, value);
|
||||
onChange(newColor);
|
||||
}, [hue, saturation, value, onChange]);
|
||||
onChange(newColor, opacity);
|
||||
}, [hue, saturation, value, opacity, onChange]);
|
||||
|
||||
const updateFromWheel = (locationX: number, locationY: number) => {
|
||||
const center = WHEEL_SIZE / 2;
|
||||
|
||||
@@ -69,10 +69,11 @@ function hsvToHex(h: number, s: number, v: number) {
|
||||
|
||||
interface ColorWheelProps {
|
||||
color: string;
|
||||
onChange: (color: string) => void;
|
||||
opacity: number;
|
||||
onChange: (color: string, opacity: number) => void;
|
||||
}
|
||||
|
||||
export default function ColorWheel({ color, onChange }: ColorWheelProps) {
|
||||
export default function ColorWheel({ color, opacity, onChange }: ColorWheelProps) {
|
||||
const { theme } = useSettings();
|
||||
const initialHsv = useMemo(() => hexToHsv(color), [color]);
|
||||
const [hue, setHue] = useState(initialHsv.h);
|
||||
@@ -89,8 +90,8 @@ export default function ColorWheel({ color, onChange }: ColorWheelProps) {
|
||||
|
||||
useEffect(() => {
|
||||
const newColor = hsvToHex(hue, saturation, value);
|
||||
onChange(newColor);
|
||||
}, [hue, saturation, value, onChange]);
|
||||
onChange(newColor, opacity);
|
||||
}, [hue, saturation, value, opacity, onChange]);
|
||||
|
||||
const handleWheelMouseDown = (e: React.MouseEvent) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet, Switch } from 'react-native';
|
||||
|
||||
export const ListItem = ({ title, subtitle, onPress, leftIcon, rightIcon, disabled, leftElement, rightElement, showChevron }: {
|
||||
title: string;
|
||||
subtitle?: string | undefined;
|
||||
onPress?: () => void;
|
||||
leftIcon?: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
leftElement?: React.ReactNode;
|
||||
rightElement?: React.ReactNode;
|
||||
showChevron?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress} disabled={disabled} style={styles.item}>
|
||||
<View style={styles.left}>
|
||||
{leftElement}
|
||||
{leftIcon}
|
||||
</View>
|
||||
<View style={styles.center}>
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
{subtitle && <Text style={styles.subtitle}>{subtitle}</Text>}
|
||||
</View>
|
||||
<View style={styles.right}>
|
||||
{rightElement}
|
||||
{rightIcon}
|
||||
</View>
|
||||
{showChevron && <Text style={styles.chevron}>▶</Text>}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
item: {
|
||||
padding: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
backgroundColor: 'white',
|
||||
marginVertical: 4,
|
||||
borderRadius: 8,
|
||||
},
|
||||
left: {
|
||||
flex: 1,
|
||||
},
|
||||
center: {
|
||||
flex: 3,
|
||||
marginHorizontal: 12,
|
||||
},
|
||||
right: {
|
||||
flex: 1,
|
||||
textAlign: 'right',
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 12,
|
||||
color: '#757575',
|
||||
marginTop: 2,
|
||||
},
|
||||
chevron: {
|
||||
fontSize: 14,
|
||||
color: '#757575',
|
||||
marginLeft: 4,
|
||||
},
|
||||
});
|
||||
@@ -66,7 +66,7 @@ export function TaskList({ categoryIds = [], showCompleted = false, onSelectionC
|
||||
const itemRefs = useRef<Map<string, View>>(new Map());
|
||||
const subtaskRefs = useRef<Map<string, { ref: View; parentTaskId: string }>>(new Map());
|
||||
const dragStateRef = useRef<{ taskId: string; positions: Record<string, { top: number; bottom: number }>; subPositions: Record<string, { top: number; bottom: number }> } | null>(null);
|
||||
const subtaskDragRef = useRef<{ subtaskId: string; parentTaskId: string; positions: Record<string, { top: number; bottom: number }>; subPositions: Record<string, { top: number; bottom: number }> } | null>(null);
|
||||
const subtaskDragRef = useRef<{ subtaskId: string; parentTaskId: string; parentSubtaskId: string | null; positions: Record<string, { top: number; bottom: number }>; subPositions: Record<string, { top: number; bottom: number }> } | null>(null);
|
||||
const {
|
||||
modals,
|
||||
openTaskDelete,
|
||||
@@ -334,7 +334,7 @@ export function TaskList({ categoryIds = [], showCompleted = false, onSelectionC
|
||||
}
|
||||
}, [findHoverTarget]);
|
||||
|
||||
const handleSubtaskDragStart = useCallback(async (subtaskId: string, parentTaskId: string, parentSubtaskId?: string) => {
|
||||
const handleSubtaskDragStart = useCallback(async (subtaskId: string, parentTaskId: string, parentSubtaskId: string | null = null) => {
|
||||
subtaskDragRef.current = { subtaskId, parentTaskId, parentSubtaskId, ...(await measureAll()) };
|
||||
}, [measureAll]);
|
||||
|
||||
@@ -373,7 +373,7 @@ const handleSubtaskDragEnd = useCallback(async (absoluteY: number) => {
|
||||
|
||||
// Only allow dropping on tasks (not subtasks) to match task behavior
|
||||
// Check if target exists in main task positions, not subtask positions
|
||||
const isTaskTarget = state.positions && target in state.positions;
|
||||
const isTaskTarget = state.positions != null && target != null && target in state.positions;
|
||||
if (target && isTaskTarget) {
|
||||
// Move subtask to target task - makes it a subtask of that task
|
||||
// If no target or target is a subtask: do nothing (matches task behavior)
|
||||
|
||||
@@ -57,6 +57,7 @@ export interface ThemeColors {
|
||||
}
|
||||
|
||||
export const DEFAULT_ACCENT = '#EF5350';
|
||||
export const DEFAULT_ACCENT_OPACITY = 1;
|
||||
|
||||
export const ACCENT_PRESETS: string[] = [
|
||||
'#EF5350',
|
||||
@@ -74,6 +75,10 @@ export const ACCENT_PRESETS: string[] = [
|
||||
|
||||
function parseHex(hex: string): [number, number, number] {
|
||||
const h = hex.replace(/^#/, '');
|
||||
// Handle both #RRGGBB (6 digits) and #AARRGGBB (8 digits with alpha)
|
||||
if (h.length === 8) {
|
||||
return [parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16), parseInt(h.slice(6, 8), 16)];
|
||||
}
|
||||
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
||||
}
|
||||
|
||||
@@ -101,8 +106,9 @@ function accentTextColor(accent: string): string {
|
||||
return luminance > 0.5 ? '#111111' : '#FFFFFF';
|
||||
}
|
||||
|
||||
export function colors(accent: string = DEFAULT_ACCENT): ThemeColors {
|
||||
export function colors(accent: string = DEFAULT_ACCENT, opacity: number = 1): ThemeColors {
|
||||
const softAccent = desaturate(accent, 0.35);
|
||||
const accentWithOpacity = opacity < 1 ? `${opacity.toString(16).padStart(2, '0')}${accent.slice(1)}` : accent;
|
||||
return {
|
||||
background: '#121212',
|
||||
card: '#1E1E1E',
|
||||
@@ -113,7 +119,7 @@ export function colors(accent: string = DEFAULT_ACCENT): ThemeColors {
|
||||
textSecondary: '#E3E4E8',
|
||||
textFaint: '#C4C6CC',
|
||||
textMuted: '#9C9FA7',
|
||||
accent,
|
||||
accent: opacity < 1 ? `${accentWithOpacity}` : accent,
|
||||
accentStrong: mixHex(accent, '#FFFFFF', 0.55),
|
||||
accentSoft: mixHex(softAccent, '#121212', 0.18),
|
||||
accentBorder: mixHex(desaturate(accent, 0.15), '#121212', 0.5),
|
||||
@@ -166,6 +172,7 @@ const STORAGE_KEYS = {
|
||||
reminderPreference: 'settings:reminderPreference',
|
||||
tagReminderPreferences: TAG_REMINDER_PREFERENCES_KEY,
|
||||
accentColor: 'settings:accentColor',
|
||||
accentOpacity: 'settings:accentOpacity',
|
||||
todoAheadDays: 'settings:todoAheadDays',
|
||||
showCompleted: 'settings:showCompleted',
|
||||
deleteBySwipe: 'settings:deleteBySwipe',
|
||||
@@ -213,6 +220,7 @@ export function SettingsProvider({ children }: { children: ReactNode }) {
|
||||
);
|
||||
const [apiUrl, setApiUrl] = useStoredSetting<string>(API_URL_KEY, DEFAULT_API_BASE_URL);
|
||||
const [accentColor, setAccentColor] = useStoredSetting<string>(STORAGE_KEYS.accentColor, DEFAULT_ACCENT);
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user