Compare commits

...
2 Commits
Author SHA1 Message Date
tech08mag 49ef8ae82f Merge pull request 'fix: remove duplicate useRouter() declaration in CalendarScreen' (#16) from dev into main
Build APK / build (push) Failing after 1m10s
Build Desktop (deb) / build (push) Successful in 2m20s
release / release (push) Successful in 6m14s
Reviewed-on: #16
2026-08-26 20:37:58 +00:00
tech08mag 261a6d4d40 fix: remove duplicate useRouter() declaration in CalendarScreen 2026-08-26 22:36:17 +02:00
7 changed files with 116 additions and 18 deletions
+19 -1
View File
@@ -218,7 +218,22 @@ export default function CalendarScreen() {
return ( return (
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}> <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"> <KeyboardAvoidingView style={styles.kbAvoid} behavior="padding">
<ScrollView style={styles.scrollBody} showsVerticalScrollIndicator={false} contentContainerStyle={styles.scrollContent}> <ScrollView style={styles.scrollBody} showsVerticalScrollIndicator={false} contentContainerStyle={styles.scrollContent}>
@@ -609,6 +624,9 @@ const createStyles = (theme: ThemeColors) => StyleSheet.create({
emptySubtext: { emptySubtext: {
fontSize: 13, fontSize: 13,
}, },
backButton: {
padding: 8,
},
eventCard: { eventCard: {
marginHorizontal: 16, marginHorizontal: 16,
marginTop: 6, marginTop: 6,
+5 -4
View File
@@ -24,7 +24,7 @@ import { getLastVisitedTab, tabHref } from '@/utils/tabHistory';
export default function SettingsScreen() { export default function SettingsScreen() {
const router = useRouter(); 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 categories = useCategories();
const [picker, setPicker] = useState<null | 'category' | 'sort' | 'reminder' | 'todoAhead'>(null); const [picker, setPicker] = useState<null | 'category' | 'sort' | 'reminder' | 'todoAhead'>(null);
const [reminderTagPicker, setReminderTagPicker] = useState<Category | null>(null); const [reminderTagPicker, setReminderTagPicker] = useState<Category | null>(null);
@@ -35,6 +35,7 @@ export default function SettingsScreen() {
const [serverUrlVisible, setServerUrlVisible] = useState(false); const [serverUrlVisible, setServerUrlVisible] = useState(false);
const [customAccentVisible, setCustomAccentVisible] = useState(false); const [customAccentVisible, setCustomAccentVisible] = useState(false);
const [draftAccent, setDraftAccent] = useState(accentColor); const [draftAccent, setDraftAccent] = useState(accentColor);
const [draftAccentOpacity, setDraftAccentOpacity] = useState(accentOpacity);
const [syncSubtitle, setSyncSubtitle] = useState('Checking...'); const [syncSubtitle, setSyncSubtitle] = useState('Checking...');
const [updateSubtitle, setUpdateSubtitle] = useState('Tap to check'); const [updateSubtitle, setUpdateSubtitle] = useState('Tap to check');
@@ -393,18 +394,18 @@ export default function SettingsScreen() {
</Svg> </Svg>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<ColorWheel color={draftAccent} onChange={setDraftAccent} /> <ColorWheel color={draftAccent} opacity={draftAccentOpacity} onChange={(color, opacity) => { setDraftAccent(color); setDraftAccentOpacity(opacity); }} />
<View style={styles.accentModalActions}> <View style={styles.accentModalActions}>
<TouchableOpacity <TouchableOpacity
style={[styles.accentModalButton, { borderColor: theme.borderStrong }]} 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} activeOpacity={0.7}
> >
<Text style={[styles.accentModalButtonText, { color: theme.textSecondary }]}>Reset</Text> <Text style={[styles.accentModalButtonText, { color: theme.textSecondary }]}>Reset</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={[styles.accentModalButton, styles.accentModalButtonPrimary, { backgroundColor: theme.accent }]} style={[styles.accentModalButton, styles.accentModalButtonPrimary, { backgroundColor: theme.accent }]}
onPress={() => { setAccentColor(draftAccent); setCustomAccentVisible(false); }} onPress={() => { setAccentColor(draftAccent); setAccentOpacity(draftAccentOpacity); setCustomAccentVisible(false); }}
activeOpacity={0.8} activeOpacity={0.8}
> >
<Text style={[styles.accentModalButtonText, { color: theme.accentText }]}>Apply</Text> <Text style={[styles.accentModalButtonText, { color: theme.accentText }]}>Apply</Text>
@@ -70,10 +70,11 @@ function hsvToHex(h: number, s: number, v: number) {
interface ColorWheelProps { interface ColorWheelProps {
color: string; 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 { theme } = useSettings();
const initialHsv = useMemo(() => hexToHsv(color), [color]); const initialHsv = useMemo(() => hexToHsv(color), [color]);
const [hue, setHue] = useState(initialHsv.h); const [hue, setHue] = useState(initialHsv.h);
@@ -90,8 +91,8 @@ export default function ColorWheel({ color, onChange }: ColorWheelProps) {
useEffect(() => { useEffect(() => {
const newColor = hsvToHex(hue, saturation, value); const newColor = hsvToHex(hue, saturation, value);
onChange(newColor); onChange(newColor, opacity);
}, [hue, saturation, value, onChange]); }, [hue, saturation, value, opacity, onChange]);
const updateFromWheel = (locationX: number, locationY: number) => { const updateFromWheel = (locationX: number, locationY: number) => {
const center = WHEEL_SIZE / 2; const center = WHEEL_SIZE / 2;
@@ -69,10 +69,11 @@ function hsvToHex(h: number, s: number, v: number) {
interface ColorWheelProps { interface ColorWheelProps {
color: string; 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 { theme } = useSettings();
const initialHsv = useMemo(() => hexToHsv(color), [color]); const initialHsv = useMemo(() => hexToHsv(color), [color]);
const [hue, setHue] = useState(initialHsv.h); const [hue, setHue] = useState(initialHsv.h);
@@ -89,8 +90,8 @@ export default function ColorWheel({ color, onChange }: ColorWheelProps) {
useEffect(() => { useEffect(() => {
const newColor = hsvToHex(hue, saturation, value); const newColor = hsvToHex(hue, saturation, value);
onChange(newColor); onChange(newColor, opacity);
}, [hue, saturation, value, onChange]); }, [hue, saturation, value, opacity, onChange]);
const handleWheelMouseDown = (e: React.MouseEvent) => { const handleWheelMouseDown = (e: React.MouseEvent) => {
const rect = e.currentTarget.getBoundingClientRect(); 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,
},
});
+3 -3
View File
@@ -66,7 +66,7 @@ export function TaskList({ categoryIds = [], showCompleted = false, onSelectionC
const itemRefs = useRef<Map<string, View>>(new Map()); const itemRefs = useRef<Map<string, View>>(new Map());
const subtaskRefs = useRef<Map<string, { ref: View; parentTaskId: string }>>(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 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 { const {
modals, modals,
openTaskDelete, openTaskDelete,
@@ -334,7 +334,7 @@ export function TaskList({ categoryIds = [], showCompleted = false, onSelectionC
} }
}, [findHoverTarget]); }, [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()) }; subtaskDragRef.current = { subtaskId, parentTaskId, parentSubtaskId, ...(await measureAll()) };
}, [measureAll]); }, [measureAll]);
@@ -373,7 +373,7 @@ const handleSubtaskDragEnd = useCallback(async (absoluteY: number) => {
// Only allow dropping on tasks (not subtasks) to match task behavior // Only allow dropping on tasks (not subtasks) to match task behavior
// Check if target exists in main task positions, not subtask positions // 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) { if (target && isTaskTarget) {
// Move subtask to target task - makes it a subtask of that task // 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) // If no target or target is a subtask: do nothing (matches task behavior)
+10 -2
View File
@@ -57,6 +57,7 @@ export interface ThemeColors {
} }
export const DEFAULT_ACCENT = '#EF5350'; export const DEFAULT_ACCENT = '#EF5350';
export const DEFAULT_ACCENT_OPACITY = 1;
export const ACCENT_PRESETS: string[] = [ export const ACCENT_PRESETS: string[] = [
'#EF5350', '#EF5350',
@@ -74,6 +75,10 @@ export const ACCENT_PRESETS: string[] = [
function parseHex(hex: string): [number, number, number] { function parseHex(hex: string): [number, number, number] {
const h = hex.replace(/^#/, ''); 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)]; 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'; 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 softAccent = desaturate(accent, 0.35);
const accentWithOpacity = opacity < 1 ? `${opacity.toString(16).padStart(2, '0')}${accent.slice(1)}` : accent;
return { return {
background: '#121212', background: '#121212',
card: '#1E1E1E', card: '#1E1E1E',
@@ -113,7 +119,7 @@ export function colors(accent: string = DEFAULT_ACCENT): ThemeColors {
textSecondary: '#E3E4E8', textSecondary: '#E3E4E8',
textFaint: '#C4C6CC', textFaint: '#C4C6CC',
textMuted: '#9C9FA7', textMuted: '#9C9FA7',
accent, accent: opacity < 1 ? `${accentWithOpacity}` : accent,
accentStrong: mixHex(accent, '#FFFFFF', 0.55), accentStrong: mixHex(accent, '#FFFFFF', 0.55),
accentSoft: mixHex(softAccent, '#121212', 0.18), accentSoft: mixHex(softAccent, '#121212', 0.18),
accentBorder: mixHex(desaturate(accent, 0.15), '#121212', 0.5), accentBorder: mixHex(desaturate(accent, 0.15), '#121212', 0.5),
@@ -166,6 +172,7 @@ const STORAGE_KEYS = {
reminderPreference: 'settings:reminderPreference', reminderPreference: 'settings:reminderPreference',
tagReminderPreferences: TAG_REMINDER_PREFERENCES_KEY, tagReminderPreferences: TAG_REMINDER_PREFERENCES_KEY,
accentColor: 'settings:accentColor', accentColor: 'settings:accentColor',
accentOpacity: 'settings:accentOpacity',
todoAheadDays: 'settings:todoAheadDays', todoAheadDays: 'settings:todoAheadDays',
showCompleted: 'settings:showCompleted', showCompleted: 'settings:showCompleted',
deleteBySwipe: 'settings:deleteBySwipe', 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 [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>(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 [todoAheadDays, setTodoAheadDays] = useStoredSetting<number>(STORAGE_KEYS.todoAheadDays, DEFAULT_TODO_AHEAD_DAYS);
const [showCompleted, setShowCompleted] = useStoredSetting<boolean>(STORAGE_KEYS.showCompleted, false); const [showCompleted, setShowCompleted] = useStoredSetting<boolean>(STORAGE_KEYS.showCompleted, false);
const [deleteBySwipe, setDeleteBySwipe] = useStoredSetting<boolean>(STORAGE_KEYS.deleteBySwipe, false); const [deleteBySwipe, setDeleteBySwipe] = useStoredSetting<boolean>(STORAGE_KEYS.deleteBySwipe, false);