This commit is contained in:
Morten Olsen
2022-05-02 14:26:11 +02:00
commit d83a4aebc7
77 changed files with 16638 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
import { UserLocation } from "#/types/location"
import { createContext } from "react"
type AgendaContext = {
enabled: boolean;
locations?: UserLocation[];
startMax?: Date;
startMin?: Date;
duration?: number;
count?: number;
}
type AgendaContextContextValue = {
contexts: {[id: string]: AgendaContext};
set: (id: string, context: AgendaContext) => Promise<void>;
}
const AgendaContextContext = createContext<AgendaContextContextValue>(undefined as any);
export type { AgendaContext, AgendaContextContextValue };
export {AgendaContextContext };

View File

@@ -0,0 +1,56 @@
import { useAsyncCallback } from "#/hooks/async";
import { Task } from "#/types/task";
import { set } from "date-fns";
import { useContext, useMemo } from "react"
import { useDate } from "../calendar";
import { useTasks } from "../tasks";
import { AgendaContextContext } from "./context"
const toToday = (today: Date, target: Date) => set(target, {
year: today.getFullYear(),
month: today.getMonth(),
date: today.getDate(),
})
export const useAgendaContext = () => {
const { contexts } = useContext(AgendaContextContext);
return contexts;
}
export const useSetAgendaContext = () => {
const { set } = useContext(AgendaContextContext);
const result = useAsyncCallback(set, [set]);
return result;
}
export const useTasksWithContext = () => {
const { all } = useTasks();
const date = useDate();
const contexts = useAgendaContext();
const withContext = useMemo<(Task & { enabled: boolean })[]>(
() => all.map((task) => {
const context = contexts[task.id];
if (!context) {
return { ...task, enabled: true };
}
return {
...task,
locations: context.locations?.length || 0 > 0 ? context.locations : task.locations,
start: {
min: context.startMin ? toToday(date, context.startMin) : task.start.min,
max: context.startMax ? toToday(date, context.startMax) : task.start.max,
},
duration: {
...task.duration,
min: context.duration || task.duration.min,
},
count: context.count,
enabled: typeof context.enabled === 'undefined' ? true : context.enabled,
}
}),
[all, contexts],
);
return withContext;
}

View File

@@ -0,0 +1,2 @@
export { AgendaContextProvider } from './provider';
export * from './hooks';

View File

@@ -0,0 +1,64 @@
import { useAsync } from "#/hooks/async";
import AsyncStorageLib from "@react-native-async-storage/async-storage";
import { format } from "date-fns";
import { ReactNode, useCallback, useMemo, useState } from "react";
import { AgendaContext, AgendaContextContext, AgendaContextContextValue } from "./context";
type AgendaContextProviderProps = {
children: ReactNode;
day: Date;
}
const AGENDA_CONTEXT_STORAGE_KEY = 'agenda-contexts';
const AgendaContextProvider: React.FC<AgendaContextProviderProps> = ({
children,
day,
}) => {
const [contexts, setContexts] = useState<AgendaContextContextValue['contexts']>({});
const key = useMemo(
() => `${AGENDA_CONTEXT_STORAGE_KEY}-${format(day, 'yyyy-MM-dd')}`,
[day],
);
const set = useCallback(
async (id: string, context: AgendaContext) => {
const index = {
...contexts,
[id]: {...context},
};
setContexts(index);
await AsyncStorageLib.setItem(key, JSON.stringify(contexts));
},
[setContexts, contexts, key],
);
useAsync(
async () => {
const raw = await AsyncStorageLib.getItem(key);
if (!raw) {
return;
}
const items = JSON.parse(raw) as AgendaContextContextValue['contexts'];
Object.values(items).forEach((item) => {
if (item.startMax) {
item.startMax = new Date(item.startMax);
}
if (item.startMin) {
item.startMin = new Date(item.startMin);
}
})
setContexts(items);
},
[key],
)
return (
<AgendaContextContext.Provider value={{ contexts, set }}>
{children}
</AgendaContextContext.Provider>
);
};
export type { AgendaContextProviderProps };
export { AgendaContextProvider };

View File

@@ -0,0 +1,17 @@
import { Calendar } from "expo-calendar";
import { createContext } from "react";
type CalendarContextValue = {
date: Date;
setDate: (date: Date) => void;
calendars: Calendar[];
calendar: Calendar;
selected: Calendar[];
setSelected: (calendars: Calendar[]) => void;
error?: any;
}
const CalendarContext = createContext<CalendarContextValue>(undefined as any);
export type { CalendarContextValue };
export { CalendarContext };

View File

@@ -0,0 +1,95 @@
import { useContext } from "react"
import { CalendarContext } from "./context"
import { set } from 'date-fns'
import { useAsync, useAsyncCallback } from "#/hooks/async";
import { createEventAsync, deleteEventAsync, getEventsAsync } from "expo-calendar";
import { PlanItem } from "#/types/plans";
export const useCalendar = () => {
const { calendar } = useContext(CalendarContext);
return calendar;
}
export const useCalendars = () => {
const { calendars } = useContext(CalendarContext);
return calendars;
}
export const useSelectedCalendars = () => {
const { selected } = useContext(CalendarContext);
return selected;
}
export const useSetSelectedCalendars = () => {
const { setSelected } = useContext(CalendarContext);
return setSelected;
}
export const useDate = () => {
const { date } = useContext(CalendarContext);
return date;
}
export const useSetDate = () => {
const { setDate } = useContext(CalendarContext);
return setDate;
}
export const useCommit = () => {
const date = useDate();
const calendar = useCalendar();
const result = useAsyncCallback(
async (plan: PlanItem[]) => {
const end = set(date, {
hours: 24,
minutes: 0,
seconds: 0,
milliseconds: 0,
});
const current = await getEventsAsync([calendar.id], date, end);
await Promise.all(
current.map(async (item) => {
await deleteEventAsync(item.id)
}),
);
for (let item of plan) {
if (item.type === 'task' && item.external) {
continue;
}
const title = item.type === 'task' ? item.name : `${item.from.title} to ${item.to.title}`;
await createEventAsync(calendar.id, {
title: title,
startDate: item.start,
endDate: item.end,
})
}
},
[date],
);
return result;
}
export const useToday = (start: Date, end?: Date) => {
const selectedCalendars = useSelectedCalendars();
if (!end) {
end = set(start, {
hours: 24,
minutes: 0,
seconds: 0,
milliseconds: 0,
});
}
const result = useAsync(
async () => {
if (selectedCalendars.length === 0) {
return [];
}
return getEventsAsync(selectedCalendars.map(c => c.id), start, end!)
},
[selectedCalendars, start.getTime()],
);
return result;
}

View File

@@ -0,0 +1,2 @@
export { CalendarProvider } from './provider';
export * from './hooks';

View File

@@ -0,0 +1,105 @@
import { Calendar, CalendarAccessLevel, createCalendarAsync, EntityTypes, getCalendarsAsync, getDefaultCalendarAsync, requestCalendarPermissionsAsync } from "expo-calendar";
import React, { ReactNode, useCallback, useMemo, useState } from "react";
import { useAsync } from "#/hooks/async";
import { CalendarContext } from "./context";
import AsyncStorage from '@react-native-async-storage/async-storage';
const SELECTED_STORAGE_KEY = 'selected_calendars';
type CalendarProviderProps = {
calendarName?: string,
date: Date;
children: ReactNode;
setDate: (date: Date) => void;
}
type SetupResponse = {
status: 'rejected';
} | {
status: 'ready';
calendar: Calendar;
calendars: Calendar[];
};
const CalendarProvider: React.FC<CalendarProviderProps> = ({
date,
children,
setDate,
calendarName = 'Bob the planner',
}) => {
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [value] = useAsync<SetupResponse>(
async () => {
const { status } = await requestCalendarPermissionsAsync();
if (status !== 'granted') {
return { status: 'rejected' };
}
let calendars = await getCalendarsAsync(EntityTypes.EVENT);
let calendar = calendars.find(c => c.title === calendarName);
if (!calendar) {
const defaultCalendar = await getDefaultCalendarAsync();
await createCalendarAsync({
title: calendarName,
source: defaultCalendar.source,
sourceId: defaultCalendar.source.id,
ownerAccount: 'personal',
accessLevel: CalendarAccessLevel.OWNER,
entityType: EntityTypes.EVENT,
name: calendarName,
});
calendars = await getCalendarsAsync(EntityTypes.EVENT);
calendar = calendars.find(c => c.name === calendarName)!;
}
const selectedRaw = await AsyncStorage.getItem(SELECTED_STORAGE_KEY)
if (selectedRaw) {
setSelectedIds(JSON.parse(selectedRaw));
}
return {
status: 'ready',
calendars,
calendar,
};
},
[],
);
const setSelected = useCallback(
(calendars: Calendar[]) => {
const ids = calendars.map(c => c.id);
setSelectedIds(ids);
AsyncStorage.setItem(SELECTED_STORAGE_KEY, JSON.stringify(ids));
},
[setSelectedIds]
)
const selected = useMemo(
() => {
if (value?.status !== 'ready') {
return [];
}
return value.calendars.filter(c => selectedIds.includes(c.id));
},
[value, selectedIds],
);
if (!value || value.status !== 'ready') {
return <></>
}
return (
<CalendarContext.Provider
value={{
setDate,
date,
selected,
setSelected,
calendar: value.calendar,
calendars: value.calendars,
}}
>
{children}
</CalendarContext.Provider>
)
};
export type { CalendarProviderProps };
export { CalendarProvider };

View File

View File

@@ -0,0 +1,17 @@
import { GetTransition, UserLocation } from "#/types/location";
import { createContext } from "react"
type LocationContextValue = {
locations: {
[id: string]: UserLocation;
};
set: (location: UserLocation) => any;
remove: (id: string) => any;
lookup?: (address: string) => UserLocation[];
getTransition: GetTransition;
}
const LocationContext = createContext<LocationContextValue>(undefined as any);
export type { LocationContextValue };
export { LocationContext };

View File

@@ -0,0 +1,73 @@
import { useAsync } from "#/hooks/async";
import { useContext } from "react"
import { requestForegroundPermissionsAsync, getCurrentPositionAsync } from 'expo-location';
import { LocationContext } from "./context"
import { UserLocation } from "#/types/location";
import { getDistanceFromLatLonInKm } from "./utils";
export const useLocations = () => {
const { locations } = useContext(LocationContext);
return locations;
}
export const useSetLocation = () => {
const { set } = useContext(LocationContext);
return set;
}
export const useRemoveLocation = () => {
const { remove } = useContext(LocationContext);
return remove;
}
export const useGetTransition = () => {
const { getTransition } = useContext(LocationContext);
return getTransition;
}
export const useLookup = () => {
const { lookup } = useContext(LocationContext);
return lookup;
}
export const useCurrentLocation = (proximity: number = 0.5) => {
const locations = useLocations();
const result = useAsync<UserLocation | undefined>(
async () => {
let { status } = await requestForegroundPermissionsAsync();
if (status !== 'granted') {
return undefined;
}
let position = await getCurrentPositionAsync({});
const withDistance = Object.values(locations).map((location) => {
if (!location.location) {
return;
}
const distance = getDistanceFromLatLonInKm(
position.coords.latitude,
position.coords.longitude,
location.location.latitude,
location.location.longitute,
)
return {
distance,
location,
}
}).filter(Boolean).sort((a, b) => a!.distance - b!.distance)
const current = withDistance.find(d => d!.distance < proximity);
if (!current) {
return {
id: `${position.coords.longitude} ${position.coords.latitude}`,
title: 'Unknown',
location: {
latitude: position.coords.latitude,
longitute: position.coords.longitude,
},
};
}
return current.location;
},
[],
);
return result;
}

View File

@@ -0,0 +1,2 @@
export { LocationProvider } from './provider';
export * from './hooks';

View File

@@ -0,0 +1,72 @@
import { useAsync, useAsyncCallback } from "#/hooks/async";
import { GetTransition, UserLocation } from "#/types/location";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { ReactNode, useState } from "react";
import { LocationContext } from "./context";
type LocationProviderProps = {
children: ReactNode;
lookup: (address: string) => UserLocation[];
getTransition: GetTransition;
}
const LOCATION_STORAGE_KEY = 'location_storage';
const LocationProvider: React.FC<LocationProviderProps> = ({
children,
lookup,
getTransition,
}) => {
const [locations, setLocations] = useState<{[id: string]: UserLocation}>({});
useAsync(
async () => {
const raw = await AsyncStorage.getItem(LOCATION_STORAGE_KEY);
if (raw) {
setLocations(JSON.parse(raw));
}
},
[],
);
const [set] = useAsyncCallback(
async (location: UserLocation) => {
const index = {
...locations,
[location.id]: location,
}
setLocations(index);
await AsyncStorage.setItem(LOCATION_STORAGE_KEY, JSON.stringify(index));
},
[setLocations, locations],
)
const [remove] = useAsyncCallback(
async (id: string) => {
const index = {
...locations,
}
delete index[id];
setLocations(index);
await AsyncStorage.setItem(LOCATION_STORAGE_KEY, JSON.stringify(index));
},
[setLocations, locations],
);
return (
<LocationContext.Provider
value={{
locations,
set,
remove,
lookup,
getTransition,
}}
>
{children}
</LocationContext.Provider>
)
}
export type { LocationProviderProps };
export { LocationProvider };

View File

@@ -0,0 +1,17 @@
export function getDistanceFromLatLonInKm(lat1: number, lon1: number, lat2: number, lon2: number) {
var R = 6371; // Radius of the earth in km
var dLat = deg2rad(lat2-lat1); // deg2rad below
var dLon = deg2rad(lon2-lon1);
var a =
Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) *
Math.sin(dLon/2) * Math.sin(dLon/2)
;
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
var d = R * c; // Distance in km
return d;
}
function deg2rad(deg: number) {
return deg * (Math.PI/180)
}

View File

@@ -0,0 +1,144 @@
import { Context, GraphNode } from "#/types/graph";
import { UserLocation } from "#/types/location";
import { Task } from "#/types/task";
import { getNext } from "./get-next";
enum Strategies {
all = 'all',
allValid = 'all-valid',
firstValid = 'first-valid',
firstComplet = 'first-complete',
}
type RunningStatus = {
current: 'running';
nodes: number;
start: Date;
cancel: () => void;
}
type CompletedStatus = {
current: 'completed';
start: Date;
end: Date;
nodes: number;
}
type Status = RunningStatus | CompletedStatus;
type BuildGraphOptions = {
location: UserLocation;
time: Date;
tasks: Task[];
context: Context;
strategy?: Strategies;
batchSize?: number;
sleepTime?: number;
callback?: (status: Status) => void;
};
const sleep = (time: number) => new Promise(resolve => setTimeout(resolve, time));
const fil = <T>(
fn: ((item: T) => boolean)[],
input: T[],
): T[][] => {
const output: T[][] = new Array(fn.length).fill(undefined).map(() => []);
for (let i = 0; i < input.length; i++) {
for (let b = 0; b < fn.length; b++) {
if (fn[b](input[i])) {
output[b].push(input[i]);
continue;
}
}
}
return output;
};
const buildGraph = async ({
location,
time,
tasks,
context,
strategy = Strategies.allValid,
callback,
batchSize = 1000,
sleepTime = 10,
}: BuildGraphOptions) => {
const start = new Date();
let leafs: GraphNode[] = [{
location,
time: {
end: time,
start: time,
},
score: 0,
remainingTasks: tasks,
impossibeTasks: [],
status: {
dead: false,
completed: false,
},
}];
let nodes = 0;
let running = true;
const final: GraphNode[] = [];
while (true) {
nodes++;
if (!running) {
return [];
}
const node = leafs.pop();
if (!node) {
break;
}
if (nodes % batchSize === 1) {
if (callback) {
callback({
current: 'running',
nodes,
start,
cancel: () => {
running = false;
}
})
}
await sleep(sleepTime);
}
const next = await getNext(node, context);
const [alive, completed] = fil([
n => !n.status.dead && !n.status.completed,
n => !!n.status.completed && !n.status.dead
], next);
leafs.push(...alive);
if (strategy === Strategies.firstValid && completed.length > 0) {
if (callback) {
callback({ current: 'completed', nodes, start, end: new Date() })
}
return completed;
}
if (completed.length > 0) {
final.push(...completed)
}
if (strategy === Strategies.firstComplet) {
const fullComplete = completed.find(c => c.impossibeTasks.length === 0);
if (fullComplete) {
if (callback) {
callback({ current: 'completed', nodes, start, end: new Date() })
}
return [fullComplete];
}
}
}
console.log('nodes', nodes);
if (callback) {
callback({ current: 'completed', nodes, start, end: new Date() })
}
return final
.filter(n => n.status.completed)
.sort((a, b) => b.score - a.score);
}
export type { Status, BuildGraphOptions };
export { buildGraph, Strategies };

View File

@@ -0,0 +1,40 @@
import { GraphNode } from "#/types/graph";
import { PlanItem } from "#/types/plans";
const constructDay = (node: GraphNode) => {
let current: GraphNode | undefined = node;
const plans: PlanItem[] = [];
while(current) {
if (current.task) {
plans.push({
type: 'task',
name: current.task?.name || 'start',
external: current.task?.external,
start: new Date(
current.time.start.getTime()
+ (current.transition?.time || 0),
),
end: current.time.end,
score: current.score,
})
}
if (current.transition) {
plans.push({
type: 'transition',
start: current.time.start,
end: new Date(
current.time.start.getTime()
+ current.transition.time,
),
from: current.transition.from,
to: current.transition.to,
})
}
current = current.parent;
}
return plans.reverse();
}
export { constructDay };

View File

@@ -0,0 +1,146 @@
import { GraphNode, Context } from '#/types/graph';
import { Transition } from '#/types/location';
import { Task } from '#/types/task';
import { getRemainingLocations, listContainLocation } from './utils';
const isDead = (impossible: Task[]) => {
const missingRequered = impossible.find(t => t.required);
return !!missingRequered;
}
type GetImpossibleResult = {
remaining: Task[];
impossible: Task[];
}
const getImpossible = (
tasks: Task[],
time: Date,
) => {
const result: GetImpossibleResult = {
remaining: [],
impossible: [],
}
for (let task of tasks) {
if (time > task.start.max) {
result.impossible.push(task);
} else {
result.remaining.push(task);
}
};
return result;
}
type CalculateScoreOptions = {
tasks?: Task[];
transition?: Transition;
impossible: Task[];
}
const calculateScore = ({
tasks,
transition,
impossible,
}: CalculateScoreOptions) => {
let score = 0;
tasks?.forEach((task) => {
score += task.priority * 10;
impossible.forEach((task) => {
if (task.required) {
score -= 1000;
} else {
score -= task.priority;
}
});
});
if (transition) {
const minutes = transition.time / 1000 / 60
score -= minutes;
}
return score;
}
const getNext = async (
currentNode: GraphNode,
context: Context,
): Promise<GraphNode[]> => {
const nextNodes: GraphNode[] = [];
if (!currentNode.transition) {
const remainingLocations = getRemainingLocations(currentNode.remainingTasks, currentNode.location);
await Promise.all(remainingLocations.map(async(location) => {
const transition = await context.getTransition(currentNode.location, location, currentNode.time.end);
const endTime = new Date(currentNode.time.end.getTime() + transition.time);
const { remaining, impossible } = getImpossible(currentNode.remainingTasks, endTime);
const score = calculateScore({
transition,
impossible,
});
nextNodes.push({
parent: currentNode,
location: transition.to,
remainingTasks: remaining,
transition,
impossibeTasks: [
...impossible,
...currentNode.impossibeTasks,
],
score: currentNode.score + score,
status: {
completed: false,
dead: isDead(impossible),
},
time: {
start: currentNode.time.end,
end: endTime,
},
})
}));
}
const possibleTasks = currentNode.remainingTasks.filter(task => !task.locations || listContainLocation(task.locations, currentNode.location))
await Promise.all(possibleTasks.map(async (orgTask) => {
const task = {...orgTask};
task.count = (task.count || 1) - 1
let startTime = new Date(
Math.max(
currentNode.time.end.getTime(),
task.start.min.getTime(),
),
);
const parentRemainging = currentNode.remainingTasks.filter(t => t !== orgTask);
let endTime = new Date(startTime.getTime() + task.duration.min);
const { remaining, impossible } = getImpossible(
task.count > 0
? [...parentRemainging, task]
: parentRemainging,
endTime,
);
const score = calculateScore({
tasks: [task],
impossible,
});
nextNodes.push({
parent: currentNode,
location: currentNode.location,
task,
remainingTasks: remaining,
impossibeTasks: [
...impossible,
...currentNode.impossibeTasks,
],
score: currentNode.score + score,
status: {
completed: remaining.length === 0,
dead: isDead(impossible),
},
time: {
start: startTime,
end: endTime,
},
})
}));
return nextNodes;
};
export { getNext };

View File

@@ -0,0 +1,38 @@
import { UserLocation } from "#/types/location";
import { Task } from "#/types/task";
export const locationEqual = (a: UserLocation, b: UserLocation) => {
if (a === b) {
return true;
}
if (a.location === b.location) {
return true;
}
if (a.location && b.location && a.location.latitude === b.location.latitude && a.location.longitute === b.location.longitute) {
return true;
}
if (a.title === b.title) {
return true;
}
return false;
}
export const listContainLocation = (list: UserLocation[], target: UserLocation) => {
return !!list.find(l => locationEqual(l, target));
}
export const getRemainingLocations = (tasks: Task[], current: UserLocation) => {
const result: UserLocation[] = [];
tasks.forEach((task) => {
if (!task.locations) {
return;
}
for (let location of task.locations) {
if (!listContainLocation(result, location) && !locationEqual(current, location)) {
result.push(location)
}
}
})
return result;
};

View File

@@ -0,0 +1,65 @@
import { useGetTransition } from "#/features/location";
import { buildGraph, Status, Strategies } from "./algorithm/build-graph";
import { constructDay } from "./algorithm/construct-day";
import { useAsyncCallback } from "#/hooks/async";
import { UserLocation } from "#/types/location";
import { useDate } from "../calendar";
import { useTasksWithContext } from "../agenda-context";
import { useMemo, useState } from "react";
import { PlanItem } from "#/types/plans";
import { Task } from "#/types/task";
export type UsePlanOptions = {
location: UserLocation;
}
export type UsePlan = [
(start?: Date) => Promise<any>,
{
result?: { agenda: PlanItem[], impossible: Task[] };
status?: Status;
loading: boolean;
error?: any;
}
]
export const usePlan = ({
location,
}: UsePlanOptions): UsePlan => {
const today = useDate();
const [status, setStatus] = useState<Status>();
const all = useTasksWithContext();
const enabled = useMemo(() => all.filter(f => f.enabled), [all])
const getTransition = useGetTransition();
const [invoke, options] = useAsyncCallback(
async (start?: Date) => {
const graph = await buildGraph({
location,
time: start || today,
tasks: enabled,
strategy: Strategies.firstComplet,
context: {
getTransition,
},
callback: setStatus,
});
const valid = graph.filter(a => !a.status.dead && a.status.completed).sort((a, b) => b.score - a.score);
const day = constructDay(valid[0]);
return {
impossible: valid[0].impossibeTasks,
agenda: day,
};
},
[today, location, all, setStatus],
);
return [
invoke,
{
result: options.result,
loading: options.loading,
error: options.error,
status: status,
}
];
}

View File

@@ -0,0 +1 @@
export * from './hooks';

View File

@@ -0,0 +1,27 @@
import { UserLocation } from "#/types/location";
import { createContext } from "react"
type Routine = {
id: string;
title: string;
required: boolean;
priority: number;
start: {
min: Date;
max: Date;
};
duration: number;
location?: UserLocation[];
days?: boolean[];
}
type RoutinesContextValue = {
routines: Routine[];
remove: (id: string) => any;
set: (routine: Routine) => any;
}
const RoutinesContext = createContext<RoutinesContextValue>(undefined as any);
export type { Routine, RoutinesContextValue };
export { RoutinesContext };

View File

@@ -0,0 +1,36 @@
import { useCallback, useContext, useMemo } from "react"
import { Routine, RoutinesContext } from "./context"
export const useRoutines = (day?: number) => {
const { routines } = useContext(RoutinesContext);
const current = useMemo(
() => routines.filter(
r => typeof day === undefined
|| !r.days
|| r.days[day!],
),
[routines],
);
return current;
};
export const useSetRoutine = () => {
const { set } = useContext(RoutinesContext);
const setRoutine = useCallback(
(routine: Routine) => set(routine),
[set],
);
return setRoutine;
}
export const useRemoveRoutine = () => {
const { remove } = useContext(RoutinesContext);
const removeRoutine = useCallback(
(id: string) => remove(id),
[remove],
);
return removeRoutine;
}

View File

@@ -0,0 +1,3 @@
export { RoutinesProvider } from './provider';
export { Routine } from './context';
export * from './hooks';

View File

@@ -0,0 +1,74 @@
import { useAsync, useAsyncCallback } from "#/hooks/async";
import AsyncStorage from "@react-native-async-storage/async-storage";
import React, { ReactNode, useMemo, useState } from "react";
import { Routine, RoutinesContext } from "./context";
type RoutinesProviderProps = {
children: ReactNode;
}
const ROUTINES_STORAGE_KEY = 'routines-items';
const RoutinesProvider: React.FC<RoutinesProviderProps> = ({ children }) => {
const [routineIndex, setRoutineIndex] = useState<{[id: string]: Routine}>({});
const routines = useMemo(
() => Object.values(routineIndex),
[routineIndex]
);
useAsync(
async () => {
const raw = await AsyncStorage.getItem(ROUTINES_STORAGE_KEY);
if (!raw) {
return;
}
const result = JSON.parse(raw) as {[name: string]: Routine};
Object.values(result).forEach(item => {
item.start.max = new Date(item.start.max);
item.start.min = new Date(item.start.min);
});
setRoutineIndex(result);
},
[setRoutineIndex],
);
const [set] = useAsyncCallback(
async (routine: Routine) => {
const index = {
...routineIndex,
[routine.id]: routine,
};
setRoutineIndex(index);
await AsyncStorage.setItem(ROUTINES_STORAGE_KEY, JSON.stringify(index));
},
[setRoutineIndex, routineIndex],
);
const [remove] = useAsyncCallback(
async (id: string) => {
const index = {
...routineIndex,
};
delete index[id];
setRoutineIndex(index);
await AsyncStorage.setItem(ROUTINES_STORAGE_KEY, JSON.stringify(index));
},
[setRoutineIndex, routineIndex],
);
return (
<RoutinesContext.Provider
value={{
routines,
set,
remove,
}}
>
{children}
</RoutinesContext.Provider>
)
}
export type { RoutinesProviderProps };
export { RoutinesProvider };

32
src/features/setup.tsx Normal file
View File

@@ -0,0 +1,32 @@
import { GetTransition } from "#/types/location"
import { ReactNode } from "react"
import { AgendaContextProvider } from "./agenda-context"
import { CalendarProvider } from "./calendar"
import { LocationProvider } from "./location"
import { RoutinesProvider } from "./routines"
type SetupProps = {
day: Date;
setDate: (date: Date) => void;
children: ReactNode;
getTransit: GetTransition;
}
const Setup: React.FC<SetupProps> = ({
children,
day,
setDate,
getTransit,
}) => (
<CalendarProvider date={day} setDate={setDate}>
<RoutinesProvider>
<LocationProvider getTransition={getTransit} lookup={() => []}>
<AgendaContextProvider day={day}>
{children}
</AgendaContextProvider>
</LocationProvider>
</RoutinesProvider>
</CalendarProvider>
);
export type { SetupProps };
export { Setup };

View File

@@ -0,0 +1,76 @@
import { useMemo } from "react";
import { useDate, useToday } from "#/features/calendar"
import { useRoutines } from "#/features/routines";
import { Task } from "#/types/task";
import { set } from "date-fns";
const toToday = (today: Date, target: Date) => set(target, {
year: today.getFullYear(),
month: today.getMonth(),
date: today.getDate(),
})
export const useTasks = () => {
const start = useDate();
const day = useMemo(
() => start.getDay(),
[start],
)
const [fromCalendar = []] = useToday(start);
const fromRoutines = useRoutines(day);
const tasksFromCalendar = useMemo<Task[]>(
() => fromCalendar.filter(e => !e.allDay).map(task => {
const start = new Date(task.startDate);
const end = new Date(task.endDate);
const duration = end.getTime() - start.getTime();
return {
id: task.id,
name: task.title,
external: true,
required: true,
start: {
min: start,
max: start,
},
priority: 100,
duration: {
min: duration,
},
};
}),
[fromCalendar],
);
const tasksFromRoutines = useMemo<Task[]>(
() => fromRoutines.map(task => ({
id: task.id,
name: task.title,
locations: task.location,
start: {
min: toToday(start, task.start.min),
max: toToday(start, task.start.max),
},
priority: task.priority,
required: task.required,
duration: {
min: task.duration,
},
})),
[fromRoutines, start],
);
const tasks = useMemo(
() => ({
calendar: tasksFromCalendar,
routines: tasksFromRoutines,
all: [
...tasksFromRoutines,
...tasksFromCalendar,
],
}),
[tasksFromCalendar, tasksFromRoutines],
);
return tasks;
}

View File

@@ -0,0 +1 @@
export * from './hooks';