Skip to content

Commit d8d005e

Browse files
author
openclaw
committed
feat: add keyboard shortcuts for task actions
1 parent 02d3c8a commit d8d005e

1 file changed

Lines changed: 127 additions & 0 deletions

File tree

src/app/page.tsx

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,7 @@ export default function Home() {
884884
const [isListsOpen, setIsListsOpen] = useState(true);
885885
const [expandedQuadrants, setExpandedQuadrants] = useState<Record<string, boolean>>({});
886886
const [showAppMenu, setShowAppMenu] = useState(false);
887+
const [lastRemovedTask, setLastRemovedTask] = useState<Task | null>(null);
887888
const [showAbout, setShowAbout] = useState(false);
888889
const [calendarView, setCalendarView] = useState<'month' | 'week' | 'day' | 'agenda'>('month');
889890
const [showCompletedInCalendar, setShowCompletedInCalendar] = useState(false);
@@ -926,6 +927,8 @@ export default function Home() {
926927
const [notificationPermission, setNotificationPermission] = useState<NotificationPermission>('default');
927928
const [isSecureContext, setIsSecureContext] = useState(true);
928929
const [showLogs, setShowLogs] = useState(false);
930+
const [taskUndoSnapshot, setTaskUndoSnapshot] = useState<Task[] | null>(null);
931+
const [taskUndoLabel, setTaskUndoLabel] = useState('');
929932
const [importMode, setImportMode] = useState<'merge' | 'overwrite'>('merge');
930933
const importInputRef = useRef<HTMLInputElement | null>(null);
931934
const [isFetchingModels, setIsFetchingModels] = useState(false);
@@ -1994,6 +1997,74 @@ export default function Home() {
19941997
return () => window.removeEventListener('click', handleClose);
19951998
}, [showAppMenu]);
19961999

2000+
useEffect(() => {
2001+
const handleGlobalKeydown = async (event: KeyboardEvent) => {
2002+
const modifier = event.metaKey || event.ctrlKey;
2003+
const editable = isEditableShortcutTarget(event.target);
2004+
2005+
if (event.key === 'Escape') {
2006+
if (editingTaskId) {
2007+
setEditingTaskId(null);
2008+
setEditingTaskTitle('');
2009+
return;
2010+
}
2011+
if (showSettings) {
2012+
setShowSettings(false);
2013+
return;
2014+
}
2015+
if (showLogs) {
2016+
setShowLogs(false);
2017+
return;
2018+
}
2019+
if (selectedTask) {
2020+
setSelectedTask(null);
2021+
return;
2022+
}
2023+
}
2024+
2025+
if (!modifier) return;
2026+
2027+
const key = event.key.toLowerCase();
2028+
2029+
if (key === 'c' && selectedTask && !editable) {
2030+
event.preventDefault();
2031+
await copyTaskContent(selectedTask);
2032+
return;
2033+
}
2034+
2035+
if (key === 'v' && !editable) {
2036+
event.preventDefault();
2037+
try {
2038+
const text = await navigator.clipboard.readText();
2039+
if (text.trim()) {
2040+
await createLocalTaskFromInput(text.trim(), activeFilter === 'category' ? activeCategory : null);
2041+
}
2042+
} catch (error) {
2043+
console.error('Failed to paste task from clipboard', error);
2044+
}
2045+
return;
2046+
}
2047+
2048+
if (key === 'z' && !editable) {
2049+
event.preventDefault();
2050+
if (lastRemovedTask) {
2051+
restoreLastRemovedTask();
2052+
}
2053+
}
2054+
};
2055+
2056+
window.addEventListener('keydown', handleGlobalKeydown);
2057+
return () => window.removeEventListener('keydown', handleGlobalKeydown);
2058+
}, [
2059+
activeCategory,
2060+
activeFilter,
2061+
editingTaskId,
2062+
lastRemovedTask,
2063+
selectedTask,
2064+
showLogs,
2065+
showSettings,
2066+
]);
2067+
19972068
const refreshTasks = () => {
19982069
const all = taskStore.getAll();
19992070
const deletedMap = readDeletedMap(DELETED_TASKS_KEY);
@@ -2004,6 +2075,29 @@ export default function Home() {
20042075
setTasks(filtered);
20052076
};
20062077

2078+
const snapshotTasksForUndo = (label: string) => {
2079+
setTaskUndoSnapshot(taskStore.getAll().map((task) => ({
2080+
...task,
2081+
tags: task.tags ? [...task.tags] : [],
2082+
subtasks: task.subtasks ? task.subtasks.map((subtask) => ({ ...subtask })) : [],
2083+
attachments: task.attachments ? task.attachments.map((attachment) => ({ ...attachment })) : [],
2084+
repeat: task.repeat ? { ...task.repeat, weekdays: task.repeat.weekdays ? [...task.repeat.weekdays] : undefined } : undefined,
2085+
})));
2086+
setTaskUndoLabel(label);
2087+
};
2088+
2089+
const restoreTaskSnapshot = () => {
2090+
if (!taskUndoSnapshot) return;
2091+
taskStore.replaceAll(taskUndoSnapshot);
2092+
refreshTasks();
2093+
if (selectedTask) {
2094+
const restoredSelected = taskUndoSnapshot.find((task) => task.id === selectedTask.id) ?? null;
2095+
setSelectedTask(restoredSelected);
2096+
}
2097+
setTaskUndoSnapshot(null);
2098+
setTaskUndoLabel('');
2099+
};
2100+
20072101
const refreshHabits = () => {
20082102
const all = habitStore.getAll();
20092103
const deletedMap = readDeletedMap(DELETED_HABITS_KEY);
@@ -3426,6 +3520,12 @@ export default function Home() {
34263520
}
34273521
};
34283522

3523+
const isEditableShortcutTarget = (target: EventTarget | null) => {
3524+
if (!(target instanceof HTMLElement)) return false;
3525+
const tagName = target.tagName;
3526+
return target.isContentEditable || tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT';
3527+
};
3528+
34293529
const toggleTaskSelected = (taskId: string) => {
34303530
setSelectedTaskIds((prev) => {
34313531
const next = new Set(prev as Set<string>);
@@ -3529,9 +3629,11 @@ export default function Home() {
35293629
};
35303630

35313631
const removeTask = (taskId: string) => {
3632+
const removedTask = taskStore.getAll().find((task) => task.id === taskId);
35323633
taskStore.remove(taskId);
35333634
markDeleted(DELETED_TASKS_KEY, taskId);
35343635
refreshTasks();
3636+
setLastRemovedTask(removedTask ?? null);
35353637

35363638
// 异步同步到 PG
35373639
syncToPg('tasks', 'DELETE', { id: taskId });
@@ -3541,7 +3643,24 @@ export default function Home() {
35413643
}
35423644
};
35433645

3646+
const restoreLastRemovedTask = () => {
3647+
if (!lastRemovedTask) return;
3648+
const restoredTask = { ...lastRemovedTask, updatedAt: new Date().toISOString() };
3649+
const deletedMap = readDeletedMap(DELETED_TASKS_KEY);
3650+
if (deletedMap[restoredTask.id]) {
3651+
const nextDeletedMap = { ...deletedMap };
3652+
delete nextDeletedMap[restoredTask.id];
3653+
persistDeletedMap(DELETED_TASKS_KEY, nextDeletedMap);
3654+
}
3655+
taskStore.add(restoredTask);
3656+
refreshTasks();
3657+
syncToPg('tasks', 'POST', restoredTask);
3658+
setSelectedTask(restoredTask);
3659+
setLastRemovedTask(null);
3660+
};
3661+
35443662
const clearCompletedTasks = () => {
3663+
snapshotTasksForUndo('恢复已清除的已完成任务');
35453664
const completedIds = taskStore.getAll().filter((task) => task.status === 'completed').map((task) => task.id);
35463665
completedIds.forEach((taskId) => markDeleted(DELETED_TASKS_KEY, taskId));
35473666
const remaining = taskStore.getAll().filter((task) => task.status !== 'completed');
@@ -3705,6 +3824,14 @@ export default function Home() {
37053824
});
37063825
};
37073826

3827+
const isTypingTarget = (target: EventTarget | null) => {
3828+
const element = target as HTMLElement | null;
3829+
if (!element) return false;
3830+
const tagName = element.tagName;
3831+
return element.isContentEditable || tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT';
3832+
};
3833+
3834+
37083835
const addTagToTask = () => {
37093836
if (!selectedTask) return;
37103837
const tag = newTagInput.trim();

0 commit comments

Comments
 (0)