Add quick capture widget

This commit is contained in:
Mo Tarbin
2026-07-28 02:02:44 -04:00
parent ce49e4afc6
commit bb87dc2e14
12 changed files with 350 additions and 8 deletions

View File

@@ -63,9 +63,20 @@ const handleNFCChoreDeepLink = (url, isColdStart) => {
const handleUrlOpen = (url, isColdStart = false) => {
console.log('[NFC] handleUrlOpen:', url)
if (url.startsWith('donetick://chores/add')) {
// Widget "+" button: land on the chore list with the quick-add modal open
// (MyChores watches for the add_task param and consumes it).
routerNavigate('/chores?add_task=1')
// Widget "+" / quick-capture buttons: land on the chore list with the
// quick-add modal open (MyChores watches for the add_task param and
// consumes it). ?mode=scan|voice opens straight into that capture panel.
let mode = null
try {
mode = new URL(url).searchParams.get('mode')
} catch {
// malformed URL — fall back to plain text capture
}
routerNavigate(
mode === 'scan' || mode === 'voice'
? `/chores?add_task=1&mode=${mode}`
: '/chores?add_task=1',
)
} else if (url.startsWith('donetick://chores/')) {
handleNFCChoreDeepLink(url, isColdStart)
} else if (url.startsWith('donetick://auth/')) {

View File

@@ -97,6 +97,8 @@ const MyChores = () => {
const [filteredChores, setFilteredChores] = useState([])
const [choreSections, setChoreSections] = useState([])
const [addTaskModalOpen, setAddTaskModalOpen] = useState(false)
// 'voice' | 'scan' | null — set by the quick-capture widget deep links
const [addTaskInitialMode, setAddTaskInitialMode] = useState(null)
const [taskInputFocus, setTaskInputFocus] = useState(0)
const searchInputRef = useRef(null)
const [searchInputFocus, setSearchInputFocus] = useState(0)
@@ -439,14 +441,18 @@ const MyChores = () => {
setSelectedProjectWithCache,
])
// Widget "+" deep link (donetick://chores/add → /chores?add_task=1):
// open the quick-add modal once and strip the param so back/refresh
// doesn't re-trigger it.
// Widget deep links (donetick://chores/add[?mode=scan|voice] →
// /chores?add_task=1[&mode=…]): open the quick-add modal once, in the
// requested capture mode, and strip the params so back/refresh doesn't
// re-trigger it.
useEffect(() => {
if (searchParams.get('add_task') === '1') {
const mode = searchParams.get('mode')
setAddTaskInitialMode(mode === 'voice' || mode === 'scan' ? mode : null)
setAddTaskModalOpen(true)
const params = new URLSearchParams(searchParams)
params.delete('add_task')
params.delete('mode')
setSearchParams(params, { replace: true })
}
}, [searchParams, setSearchParams])
@@ -1455,8 +1461,10 @@ const MyChores = () => {
autoFocus={taskInputFocus}
onChoreUpdate={updateChores}
isModalOpen={addTaskModalOpen}
initialMode={addTaskInitialMode}
onClose={forceRefresh => {
setAddTaskModalOpen(false)
setAddTaskInitialMode(null)
if (forceRefresh) {
refetchChores()
}

View File

@@ -147,17 +147,19 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose, initialMode }) => {
}
if (appliedInitialModeRef.current) return
// Availability resolves async — wait for the answer before deciding; if it
// never turns true the modal simply stays in plain text mode.
if (initialMode === 'voice') {
// isSupported() resolves async — wait for the answer before deciding.
if (!voiceAvailable) return
appliedInitialModeRef.current = true
setShowVoice(true)
} else if (initialMode === 'scan') {
if (!llmAvailable) return
appliedInitialModeRef.current = true
setScanAutoCapture(true)
setShowScan(true)
}
}, [isModalOpen, initialMode, voiceAvailable])
}, [isModalOpen, initialMode, voiceAvailable, llmAvailable])
// Priority colors
const priorityColors = {