From 538f4589e2d9255d2763b9ec8ba7afe896433ff1 Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Wed, 21 Oct 2026 00:34:38 +0800 Subject: [PATCH 1/5] feat(linux): batch schedule multiple tasks with date/time clearing --- linux/src/main.cpp | 69 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/linux/src/main.cpp b/linux/src/main.cpp index 93e28d2..a820bcf 100644 --- a/linux/src/main.cpp +++ b/linux/src/main.cpp @@ -398,6 +398,7 @@ struct AppState { GtkLabel* bulk_count{nullptr}; GtkButton* bulk_complete{nullptr}; GtkButton* bulk_move{nullptr}; + GtkButton* bulk_schedule{nullptr}; GtkStack* list_stack{nullptr}; GtkLabel* empty_icon{nullptr}; GtkLabel* empty_text{nullptr}; @@ -987,6 +988,27 @@ void batch_move_selected(BatchMoveCommand const& command) { } } +// Date changes act on task IDs captured at popover creation. Re-resolve +// every task at command activation so items that have disappeared from the +// active view cannot be accidentally overwritten with stale whole records. +struct BulkScheduleCommand { + std::vector ids; + std::optional due_date; +}; + +void batch_schedule_selected(BulkScheduleCommand const& command) { + for (auto const id : command.ids) { + if (auto const* task = find_selected_task(id)) { + auto updated = *task; + updated.due_date = command.due_date; + // Clearing the date also clears the time. Keeping an invisible + // due_time is inconsistent with the single-task Clear action. + if (!command.due_date.has_value()) updated.due_time = std::nullopt; + save_task_edits(std::move(updated)); + } + } +} + void run_search(std::string const& keyword) { if (g_state.api == nullptr) return; g_state.api->search_tasks_async( @@ -2960,6 +2982,7 @@ void apply_language() { if (g_state.bulk_complete != nullptr) { gtk_button_set_label(g_state.bulk_complete, tr("navCompleted").c_str()); gtk_button_set_label(g_state.bulk_move, tr("menuMoveToList").c_str()); + gtk_button_set_label(g_state.bulk_schedule, tr("bulkSchedule").c_str()); } gtk_button_set_label( GTK_BUTTON(g_state.show_completed_toggle), @@ -3630,6 +3653,51 @@ void on_activate(GtkApplication* app, gpointer) { })), nullptr); gtk_box_append(GTK_BOX(bulk_bar), bulk_move); + auto* bulk_schedule = gtk_button_new_with_label(tr("bulkSchedule").c_str()); + g_signal_connect( + bulk_schedule, "clicked", + G_CALLBACK(+[](GtkButton* button, gpointer) { + auto* previous = static_cast( + g_object_get_data(G_OBJECT(button), "batch-date-popover")); + if (previous != nullptr) { + g_object_set_data(G_OBJECT(button), "batch-date-popover", nullptr); + gtk_widget_unparent(previous); + } + auto const ids = selected_task_ids(); + if (ids.size() < 2) return; + auto* popover = gtk_popover_new(); + auto* options = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); + auto add_option = [&](std::string const& title, + std::optional due_date) { + auto* option = gtk_button_new_with_label(title.c_str()); + gtk_widget_add_css_class(option, "flat"); + auto* command = new BulkScheduleCommand{ids, std::move(due_date)}; + g_object_set_data_full( + G_OBJECT(option), "batch-schedule-command", command, + +[](gpointer data) { + delete static_cast(data); + }); + g_signal_connect( + option, "clicked", + G_CALLBACK(+[](GtkButton* pressed, gpointer) { + auto* command = static_cast( + g_object_get_data(G_OBJECT(pressed), "batch-schedule-command")); + if (command != nullptr) batch_schedule_selected(*command); + auto* p = static_cast(g_object_get_data( + G_OBJECT(g_state.bulk_schedule), "batch-date-popover")); + if (p != nullptr) gtk_popover_popdown(GTK_POPOVER(p)); + }), nullptr); + gtk_box_append(GTK_BOX(options), option); + }; + add_option(tr("navToday"), date_string(0)); + add_option(tr("dateTomorrow"), date_string(1)); + add_option(tr("dialogClear"), std::nullopt); + gtk_popover_set_child(GTK_POPOVER(popover), options); + gtk_widget_set_parent(popover, GTK_WIDGET(button)); + g_object_set_data(G_OBJECT(button), "batch-date-popover", popover); + gtk_popover_popup(GTK_POPOVER(popover)); + }), nullptr); + gtk_box_append(GTK_BOX(bulk_bar), bulk_schedule); gtk_box_append(GTK_BOX(pane), bulk_bar); auto* task_scroll = gtk_scrolled_window_new(); @@ -3727,6 +3795,7 @@ void on_activate(GtkApplication* app, gpointer) { g_state.bulk_count = GTK_LABEL(bulk_count); g_state.bulk_complete = GTK_BUTTON(bulk_complete); g_state.bulk_move = GTK_BUTTON(bulk_move); + g_state.bulk_schedule = GTK_BUTTON(bulk_schedule); g_state.list_stack = GTK_STACK(list_stack); g_state.empty_icon = GTK_LABEL(empty_icon); g_state.empty_text = GTK_LABEL(empty_text); From 7f4e1cc21e39366cb8dfdbe533563fb9be848058 Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Wed, 21 Oct 2026 00:35:15 +0800 Subject: [PATCH 2/5] docs: record Linux bulk schedule behavior and remaining release blockers --- docs/UX-BENCHMARKS.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/UX-BENCHMARKS.md b/docs/UX-BENCHMARKS.md index 1479e68..aee2bb0 100644 --- a/docs/UX-BENCHMARKS.md +++ b/docs/UX-BENCHMARKS.md @@ -92,7 +92,9 @@ Scheduled(有日期的)。 - ✅ Linux:基于 GTK 原生多选(#33)增加可见的批量完成和批量移动操作栏, 同时适用于当前列表和全局搜索。刷新时按任务 ID 保留选择。 - ✅ Windows 单条/批量删除采用 6 秒延后提交:先隐藏选中任务,期间撤销可直接恢复原任务 ID 与元数据,超时才发送后端删除 RPC;失败时恢复任务。 -- 🔜 **尚未完成**:三平台按真实快捷键进行实机回归。 +- ✅ Linux 批量排期:多选状态下可设置今天、明天或清除日期; + 清除日期同时清除 due_time,操作按任务 ID 捕获并重新查找真实任务。 +- 🔜 **尚未完成**:Linux 批量删除与可靠撤销,以及三平台按真实快捷键进行实机回归。 ### 2.5 Quick Find From c40b2feee970c80a1da82e2c8d74444f8353a82e Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Wed, 21 Oct 2026 00:53:11 +0800 Subject: [PATCH 3/5] fix(linux): protect GTK signal lambda callbacks from macro comma parsing --- linux/src/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/linux/src/main.cpp b/linux/src/main.cpp index a820bcf..e336455 100644 --- a/linux/src/main.cpp +++ b/linux/src/main.cpp @@ -3656,7 +3656,7 @@ void on_activate(GtkApplication* app, gpointer) { auto* bulk_schedule = gtk_button_new_with_label(tr("bulkSchedule").c_str()); g_signal_connect( bulk_schedule, "clicked", - G_CALLBACK(+[](GtkButton* button, gpointer) { + G_CALLBACK((+[](GtkButton* button, gpointer) { auto* previous = static_cast( g_object_get_data(G_OBJECT(button), "batch-date-popover")); if (previous != nullptr) { @@ -3686,7 +3686,7 @@ void on_activate(GtkApplication* app, gpointer) { auto* p = static_cast(g_object_get_data( G_OBJECT(g_state.bulk_schedule), "batch-date-popover")); if (p != nullptr) gtk_popover_popdown(GTK_POPOVER(p)); - }), nullptr); + })), nullptr); gtk_box_append(GTK_BOX(options), option); }; add_option(tr("navToday"), date_string(0)); From c66bdd5fb71a63dc9571acd3b0cf05a9cb776e15 Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Wed, 21 Oct 2026 02:30:48 +0800 Subject: [PATCH 4/5] fix(linux): correct GTK signal macro for bulk scheduling --- linux/src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linux/src/main.cpp b/linux/src/main.cpp index e336455..4d3bec5 100644 --- a/linux/src/main.cpp +++ b/linux/src/main.cpp @@ -3696,7 +3696,7 @@ void on_activate(GtkApplication* app, gpointer) { gtk_widget_set_parent(popover, GTK_WIDGET(button)); g_object_set_data(G_OBJECT(button), "batch-date-popover", popover); gtk_popover_popup(GTK_POPOVER(popover)); - }), nullptr); + })), nullptr); gtk_box_append(GTK_BOX(bulk_bar), bulk_schedule); gtk_box_append(GTK_BOX(pane), bulk_bar); From f64487e8d897a682f536e1d1c0a6586d820e4fc7 Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Wed, 21 Oct 2026 04:38:55 +0800 Subject: [PATCH 5/5] fix(linux): correct batch scheduling callback closure --- linux/src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linux/src/main.cpp b/linux/src/main.cpp index 4d3bec5..8563c40 100644 --- a/linux/src/main.cpp +++ b/linux/src/main.cpp @@ -3686,7 +3686,7 @@ void on_activate(GtkApplication* app, gpointer) { auto* p = static_cast(g_object_get_data( G_OBJECT(g_state.bulk_schedule), "batch-date-popover")); if (p != nullptr) gtk_popover_popdown(GTK_POPOVER(p)); - })), nullptr); + }), nullptr); gtk_box_append(GTK_BOX(options), option); }; add_option(tr("navToday"), date_string(0));