Compare commits
2
Commits
0b87f3e4ed
...
v1.0.13
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49ef8ae82f | ||
|
|
261a6d4d40 |
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user