Android home-screen widget: top-3 next actions (4x2 'Next up' card) — tap the priority ring to complete via the offline queue, rows deep-link to Clarify, + opens capture; redraws on every store change plus a 30-min self-refresh
Some checks failed
Build & Release APK / build (push) Failing after 24s
Some checks failed
Build & Release APK / build (push) Failing after 24s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
98
app/src/widget/NextUpWidget.tsx
Normal file
98
app/src/widget/NextUpWidget.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
"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 circle 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: 10, 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: 12, letterSpacing: 1, color: C.muted }} />
|
||||
{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: "#FFFFFF" }} />
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user