From 7057e11e9677909e541d99ac7234954987a5fda7 Mon Sep 17 00:00:00 2001 From: Morten Olsen Date: Fri, 6 May 2022 14:39:49 +0200 Subject: [PATCH] update --- src/features/planner/algorithm/build-graph.ts | 78 +++++++++++-------- src/features/planner/algorithm/get-next.ts | 8 +- src/features/planner/context.ts | 15 ++++ src/features/planner/hooks.ts | 18 ++++- src/features/planner/index.ts | 3 + src/features/planner/provider.tsx | 32 ++++++++ src/features/setup.tsx | 5 +- src/ui/components/button/index.tsx | 2 +- src/ui/components/form/selector/index.tsx | 56 +++++++++++++ src/ui/components/index.ts | 2 + src/ui/components/modal/index.tsx | 24 ++++++ src/ui/components/popup/index.tsx | 2 +- src/ui/router/index.tsx | 2 + src/ui/screens/locations/set.tsx | 3 +- src/ui/screens/plan/day.tsx | 12 ++- src/ui/screens/plan/set.tsx | 3 +- src/ui/screens/plan/settings.tsx | 40 ++++++++++ src/ui/screens/routines/set.tsx | 3 +- 18 files changed, 257 insertions(+), 51 deletions(-) create mode 100644 src/features/planner/context.ts create mode 100644 src/features/planner/provider.tsx create mode 100644 src/ui/components/form/selector/index.tsx create mode 100644 src/ui/components/modal/index.tsx create mode 100644 src/ui/screens/plan/settings.tsx diff --git a/src/features/planner/algorithm/build-graph.ts b/src/features/planner/algorithm/build-graph.ts index c29797e..0138a7a 100644 --- a/src/features/planner/algorithm/build-graph.ts +++ b/src/features/planner/algorithm/build-graph.ts @@ -1,7 +1,7 @@ import { Context, GraphNode } from "#/types/graph"; import { UserLocation } from "#/types/location"; import { Task } from "#/types/task"; -import { getNext } from "./get-next"; +import { getImpossible, getNext } from "./get-next"; enum Strategies { all = 'all', @@ -13,6 +13,7 @@ type RunningStatus = { current: 'running'; nodes: number; start: Date; + strategy: Strategies, cancel: () => void; } @@ -21,6 +22,7 @@ type CompletedStatus = { start: Date; end: Date; nodes: number; + strategy: Strategies, } type Status = RunningStatus | CompletedStatus; @@ -65,38 +67,62 @@ const buildGraph = async ({ sleepTime = 10, }: BuildGraphOptions) => { const start = new Date(); - let leafs: GraphNode[] = [{ + let nodeCount = 0; + let running = true; + const { remaining, impossible } = getImpossible(tasks, time); + let leafList: GraphNode[] = [{ location, time: { end: time, start: time, }, score: 0, - remainingTasks: tasks, - impossibeTasks: [], + remainingTasks: remaining, + impossibeTasks: impossible, status: { dead: false, completed: false, }, }]; - let nodes = 0; - let running = true; - const final: GraphNode[] = []; + const completedList: GraphNode[] = []; + const deadList: GraphNode[] = []; + + const complete = (nodes: GraphNode[]) => { + if (callback) { + callback({ + current: 'completed', + nodes: nodeCount, + start, + end: new Date(), + strategy, + }); + } + return nodes.sort((a, b) => b.score - a.score); + } while (true) { - nodes++; + nodeCount++; if (!running) { return []; } - const node = leafs.pop(); + if ( + leafList.length === 0 + && completedList.length === 0 + && strategy !== Strategies.all + ) { + strategy = Strategies.all; + leafList.push(...deadList); + } + const node = leafList.pop(); if (!node) { break; } - if (nodes % batchSize === 1) { + if (nodeCount % batchSize === 0) { if (callback) { callback({ current: 'running', - nodes, + nodes: nodeCount, + strategy, start, cancel: () => { running = false; @@ -106,38 +132,28 @@ const buildGraph = async ({ 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 + const [alive, completed, dead] = fil([ + n => (strategy === Strategies.all || !n.status.dead) && !n.status.completed, + n => !!n.status.completed && (strategy === Strategies.all || !n.status.dead), + n => n.status.dead, ], next); - leafs.push(...alive); + leafList.push(...alive); if (strategy === Strategies.firstValid && completed.length > 0) { - if (callback) { - callback({ current: 'completed', nodes, start, end: new Date() }) - } - return completed; + return complete(completed); } if (completed.length > 0) { - final.push(...completed) + completedList.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]; + return complete([fullComplete]); } } + deadList.push(...dead); } - 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); + return complete(completedList); } export type { Status, BuildGraphOptions }; diff --git a/src/features/planner/algorithm/get-next.ts b/src/features/planner/algorithm/get-next.ts index cc0fef0..0a1667d 100644 --- a/src/features/planner/algorithm/get-next.ts +++ b/src/features/planner/algorithm/get-next.ts @@ -13,7 +13,7 @@ type GetImpossibleResult = { impossible: Task[]; } -const getImpossible = ( +export const getImpossible = ( tasks: Task[], time: Date, ) => { @@ -50,15 +50,15 @@ const calculateScore = ({ score += task.priority * 10; impossible.forEach((task) => { if (task.required) { - score -= 1000; + score -= 10000 + (1 * task.priority); } else { - score -= task.priority; + score -= 100 + (1 * task.priority); } }); }); if (transition) { const minutes = transition.time / 1000 / 60 - score -= minutes; + score -= 10 + (1 * minutes); } return score; } diff --git a/src/features/planner/context.ts b/src/features/planner/context.ts new file mode 100644 index 0000000..d035e42 --- /dev/null +++ b/src/features/planner/context.ts @@ -0,0 +1,15 @@ +import { createContext } from 'react'; +import { Strategies } from "./algorithm/build-graph"; + +type PlannerOptions = { + strategy: Strategies; +} +type PlannerContextValue = { + options: PlannerOptions; + setOptions: (options: Partial) => void; +} + +const PlannerContext = createContext(undefined as any); + +export type { PlannerContextValue, PlannerOptions }; +export { PlannerContext }; diff --git a/src/features/planner/hooks.ts b/src/features/planner/hooks.ts index 18357f7..2f978da 100644 --- a/src/features/planner/hooks.ts +++ b/src/features/planner/hooks.ts @@ -5,9 +5,10 @@ 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 { useContext, useMemo, useState } from "react"; import { PlanItem } from "#/types/plans"; import { Task } from "#/types/task"; +import { PlannerContext } from "./context"; export type UsePlanOptions = { location: UserLocation; @@ -23,10 +24,21 @@ export type UsePlan = [ } ] +export const usePlanOptions = () => { + const { options } = useContext(PlannerContext); + return options; +} + +export const useSetPlanOptions = () => { + const { setOptions } = useContext(PlannerContext); + return setOptions; +} + export const usePlan = ({ location, }: UsePlanOptions): UsePlan => { const today = useDate(); + const planOptions = usePlanOptions(); const [status, setStatus] = useState(); const all = useTasksWithContext(); const enabled = useMemo(() => all.filter(f => f.enabled), [all]) @@ -37,7 +49,7 @@ export const usePlan = ({ location, time: start || today, tasks: enabled, - strategy: Strategies.firstComplet, + strategy: planOptions.strategy, context: { getTransition, }, @@ -50,7 +62,7 @@ export const usePlan = ({ agenda: day, }; }, - [today, location, all, setStatus], + [today, location, all, setStatus, planOptions], ); return [ diff --git a/src/features/planner/index.ts b/src/features/planner/index.ts index 4cc90d0..3507be0 100644 --- a/src/features/planner/index.ts +++ b/src/features/planner/index.ts @@ -1 +1,4 @@ +export { PlannerProvider } from './provider'; +export type { PlannerOptions } from './context'; +export { Strategies } from './algorithm/build-graph'; export * from './hooks'; diff --git a/src/features/planner/provider.tsx b/src/features/planner/provider.tsx new file mode 100644 index 0000000..4e5efda --- /dev/null +++ b/src/features/planner/provider.tsx @@ -0,0 +1,32 @@ +import { ReactNode, useCallback, useState } from 'react'; +import { Strategies } from './algorithm/build-graph'; +import { PlannerContext, PlannerOptions } from './context'; + +type PlannerProviderProps = { + children: ReactNode; +}; + +const PlannerProvider: React.FC = ({ children }) => { + const [options, setOwnOptions] = useState({ + strategy: Strategies.firstComplet, + }) + + const setOptions = useCallback( + (next: Partial) => { + setOwnOptions(current => ({ + ...current, + ...next, + })) + }, + [setOwnOptions], + ); + + return ( + + {children} + + ); +} + +export type { PlannerProviderProps }; +export { PlannerProvider }; diff --git a/src/features/setup.tsx b/src/features/setup.tsx index 77e0525..b8bd948 100644 --- a/src/features/setup.tsx +++ b/src/features/setup.tsx @@ -3,6 +3,7 @@ import { ReactNode } from "react" import { AgendaContextProvider } from "./agenda-context" import { CalendarProvider } from "./calendar" import { LocationProvider } from "./location" +import { PlannerProvider } from "./planner" import { RoutinesProvider } from "./routines" type SetupProps = { @@ -21,7 +22,9 @@ const Setup: React.FC = ({ []}> - {children} + + {children} + diff --git a/src/ui/components/button/index.tsx b/src/ui/components/button/index.tsx index 114b024..9934061 100644 --- a/src/ui/components/button/index.tsx +++ b/src/ui/components/button/index.tsx @@ -1,7 +1,7 @@ import React, { ReactNode } from 'react'; import styled from 'styled-components/native'; import { TouchableOpacity } from 'react-native'; -import { IconNames, Icon } from '#/ui/components'; +import { IconNames, Icon } from '../icon'; import { Theme } from '#/ui/theme'; import { Link } from '#/ui/typography'; diff --git a/src/ui/components/form/selector/index.tsx b/src/ui/components/form/selector/index.tsx new file mode 100644 index 0000000..954378b --- /dev/null +++ b/src/ui/components/form/selector/index.tsx @@ -0,0 +1,56 @@ +import { cmyk } from "chroma-js"; +import { ReactNode, useCallback, useMemo, useState } from "react"; +import { Modal } from "../../modal"; +import { Row } from "../../row"; + +type SelectorProps = { + label: string; + items: { + display: ReactNode; + value: T; + }[]; + getId: (item: T) => string; + selected?: T; + setSelected: (item?: T) => void; +} + +function Selector({ + items, + label, + getId, + selected, + setSelected, +}: SelectorProps) { + const [visible, setVisible] = useState(false); + const selectedItem = useMemo( + () => selected ? items.find(i => getId(i.value) === getId(selected)) : undefined, + [selected, items], + ); + const select = useCallback( + (item: T) => { + setSelected(item); + setVisible(false); + }, + [setSelected, setVisible], + ); + return ( + <> + setVisible(false)}> + {items.map((item) => ( + select(item.value)} + title={item.display} + /> + ))} + + setVisible(true)} + title={selectedItem?.display || 'Select'} + /> + + ) +} + +export { Selector }; diff --git a/src/ui/components/index.ts b/src/ui/components/index.ts index 4e95bd1..c1ecf15 100644 --- a/src/ui/components/index.ts +++ b/src/ui/components/index.ts @@ -1,4 +1,6 @@ export * from './icon'; +export * from './modal'; +export * from './icon'; export * from './form'; export * from './page'; export * from './popup'; diff --git a/src/ui/components/modal/index.tsx b/src/ui/components/modal/index.tsx new file mode 100644 index 0000000..de0a7f6 --- /dev/null +++ b/src/ui/components/modal/index.tsx @@ -0,0 +1,24 @@ +import { ReactNode } from 'react'; +import { Modal as Wrapper } from 'react-native'; +import { Popup } from '../popup'; +type ModalProps = { + visible: boolean; + onClose: () => void; + children: ReactNode; +} + +const Modal: React.FC = ({ visible, onClose, children }) => ( + + + {children} + + +); + +export { Modal }; diff --git a/src/ui/components/popup/index.tsx b/src/ui/components/popup/index.tsx index 99cacc8..72f3159 100644 --- a/src/ui/components/popup/index.tsx +++ b/src/ui/components/popup/index.tsx @@ -51,4 +51,4 @@ const Popup: React.FC = ({ visible, children, onClose }) => { ); }; -export default Popup; +export { Popup }; diff --git a/src/ui/router/index.tsx b/src/ui/router/index.tsx index 3104893..006f4a5 100644 --- a/src/ui/router/index.tsx +++ b/src/ui/router/index.tsx @@ -12,6 +12,7 @@ import { RoutineSetScreen } from '../screens/routines/set'; import { TaskListScreen } from '../screens/plan/tasks'; import { AgendaContextSetScreen } from '../screens/plan/set'; import { Icon } from '../components'; +import { PlanSettingsScreen } from '../screens/plan/settings'; const MainTabsNvaigator = createBottomTabNavigator(); @@ -79,6 +80,7 @@ const Root: React.FC = () => ( + ); diff --git a/src/ui/screens/locations/set.tsx b/src/ui/screens/locations/set.tsx index 19be980..f41e63c 100644 --- a/src/ui/screens/locations/set.tsx +++ b/src/ui/screens/locations/set.tsx @@ -1,9 +1,8 @@ import { useLocations, useSetLocation } from "#/features/location"; import { useNavigation, useRoute } from '@react-navigation/native'; -import { Button, TextInput } from "#/ui/components"; +import { Popup, Button, TextInput } from "#/ui/components"; import { useCallback, useEffect, useMemo, useState } from "react"; import { nanoid } from 'nanoid'; -import Popup from "#/ui/components/popup"; const LocationSetScreen: React.FC = () => { const { params = {} } = useRoute() as any; diff --git a/src/ui/screens/plan/day.tsx b/src/ui/screens/plan/day.tsx index 7163912..6072d76 100644 --- a/src/ui/screens/plan/day.tsx +++ b/src/ui/screens/plan/day.tsx @@ -8,19 +8,19 @@ import { useCommit, useDate } from "#/features/calendar"; import { format, formatDistance, formatDistanceToNow, set } from "date-fns"; import styled from "styled-components/native"; import { Status } from "#/features/planner/algorithm/build-graph"; +import { useNavigation } from "@react-navigation/native"; const Wrapper = styled.ScrollView` `; const getStats = (status: Status) => { - console.log('status', status); if (status.current === 'running') { const runTime = formatDistanceToNow(status.start, { includeSeconds: true }) - return `calulated ${status.nodes} nodes in ${runTime}`; + return `calulated ${status.nodes} nodes in ${runTime} using ${status.strategy}`; } const runTime = formatDistance(status.start, status.end, { includeSeconds: true }) - return `calulated ${status.nodes} nodes in ${runTime}`; + return `calulated ${status.nodes} nodes in ${runTime} using ${status.strategy}`; }; const PlanDayScreen: React.FC = () => { @@ -28,10 +28,11 @@ const PlanDayScreen: React.FC = () => { const [location] = useCurrentLocation(); const [startTime, setStartTime] = useState('06:00'); const [commit] = useCommit(); + const { navigate } = useNavigation(); const current = useMemo( () => location || { id: 'unknown', - title: 'foo', + title: 'Unknown', }, [location] ) @@ -70,6 +71,9 @@ const PlanDayScreen: React.FC = () => {