99 lines
4.3 KiB
TypeScript
99 lines
4.3 KiB
TypeScript
"use no memo"; // widget trees are built by calling the component as a plain function — the React Compiler must not transform it
|
||
// Home-screen widget: the same top-three "Next up" list as the home screen.
|
||
// Built only from react-native-android-widget primitives (no RN views, no
|
||
// hooks) — it renders to Android RemoteViews in a headless task.
|
||
//
|
||
// Interactions mirror the app: tap the checkbox to complete, tap the row to
|
||
// open Clarify, tap + to capture, tap anywhere else to open the app.
|
||
import { FlexWidget, TextWidget } from "react-native-android-widget";
|
||
import { dueMeta, type DueTone } from "../lib/dates";
|
||
import { formatEstimate, PRIORITY_COLOR } from "../lib/priority";
|
||
import { C } from "../lib/theme";
|
||
import type { Task } from "../lib/types";
|
||
|
||
const DUE_COLOR: Record<DueTone, `#${string}`> = { overdue: C.danger, today: C.warn, later: C.muted };
|
||
|
||
function Row({ task }: { task: Task }) {
|
||
const due = task.due_date ? dueMeta(task.due_date) : null;
|
||
// One line per row so three rows fit the 2-cell height: right-side meta is
|
||
// the due label when there is one, otherwise context/estimate.
|
||
const side = due ? due.label : [task.context, formatEstimate(task.estimate_min)].filter(Boolean).join(" · ");
|
||
return (
|
||
<FlexWidget
|
||
style={{ width: "match_parent", flex: 1, flexDirection: "row", alignItems: "center" }}
|
||
clickAction="OPEN_URI"
|
||
clickActionData={{ uri: `gtd://task/${task.id}` }}
|
||
>
|
||
<FlexWidget
|
||
clickAction="COMPLETE_TASK"
|
||
clickActionData={{ taskId: task.id }}
|
||
accessibilityLabel={`Complete ${task.title}`}
|
||
style={{ width: 32, height: 32, alignItems: "center", justifyContent: "center" }}
|
||
>
|
||
<FlexWidget
|
||
style={{ width: 19, height: 19, borderRadius: 6, borderWidth: 2, borderColor: PRIORITY_COLOR[task.priority] }}
|
||
/>
|
||
</FlexWidget>
|
||
<FlexWidget style={{ flex: 1, marginLeft: 6 }}>
|
||
<TextWidget text={task.title} truncate="END" maxLines={1} style={{ fontSize: 15, color: C.text }} />
|
||
</FlexWidget>
|
||
{!!side && <TextWidget text={side} truncate="END" maxLines={1} style={{ fontSize: 11, color: due ? DUE_COLOR[due.tone] : C.muted, marginLeft: 8 }} />}
|
||
</FlexWidget>
|
||
);
|
||
}
|
||
|
||
export function NextUpWidget({ tasks, totalNext, overdueCount }: { tasks: Task[]; totalNext: number; overdueCount: number }) {
|
||
return (
|
||
<FlexWidget
|
||
clickAction="OPEN_APP"
|
||
style={{
|
||
width: "match_parent",
|
||
height: "match_parent",
|
||
flexDirection: "column",
|
||
backgroundColor: C.surface,
|
||
borderRadius: 24,
|
||
paddingHorizontal: 16,
|
||
paddingVertical: 12,
|
||
}}
|
||
>
|
||
<FlexWidget
|
||
style={{ width: "match_parent", flexDirection: "row", alignItems: "center", justifyContent: "space-between", marginBottom: 4 }}
|
||
>
|
||
<FlexWidget style={{ flexDirection: "row", alignItems: "center" }}>
|
||
<TextWidget text="Next up" style={{ fontSize: 13, fontWeight: "600", letterSpacing: 0.3, color: C.textSecondary }} />
|
||
{totalNext > tasks.length && (
|
||
<TextWidget text={` +${totalNext - tasks.length} more`} style={{ fontSize: 12, color: C.muted }} />
|
||
)}
|
||
{overdueCount > 0 && (
|
||
<TextWidget
|
||
text={` · ${overdueCount} overdue`}
|
||
clickAction="OPEN_URI"
|
||
clickActionData={{ uri: "gtd://today" }}
|
||
style={{ fontSize: 12, color: C.danger }}
|
||
/>
|
||
)}
|
||
</FlexWidget>
|
||
<FlexWidget
|
||
clickAction="OPEN_URI"
|
||
clickActionData={{ uri: "gtd://add" }}
|
||
accessibilityLabel="Add task"
|
||
style={{ width: 28, height: 28, borderRadius: 14, backgroundColor: C.accent, alignItems: "center", justifyContent: "center" }}
|
||
>
|
||
<TextWidget text="+" style={{ fontSize: 16, color: C.onAccent }} />
|
||
</FlexWidget>
|
||
</FlexWidget>
|
||
{tasks.length === 0 ? (
|
||
<FlexWidget style={{ width: "match_parent", flex: 1, alignItems: "center", justifyContent: "center" }}>
|
||
<TextWidget text="Nothing next. Tap + to add." style={{ fontSize: 14, color: C.muted }} />
|
||
</FlexWidget>
|
||
) : (
|
||
<FlexWidget style={{ width: "match_parent", flex: 1, flexDirection: "column" }}>
|
||
{tasks.map((t) => (
|
||
<Row key={t.id} task={t} />
|
||
))}
|
||
</FlexWidget>
|
||
)}
|
||
</FlexWidget>
|
||
);
|
||
}
|