Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/UX-BENCHMARKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ Scheduled(有日期的)。
- ✅ Linux:基于 GTK 原生多选(#33)增加可见的批量完成和批量移动操作栏,
同时适用于当前列表和全局搜索。刷新时按任务 ID 保留选择。
- ✅ Windows 单条/批量删除采用 6 秒延后提交:先隐藏选中任务,期间撤销可直接恢复原任务 ID 与元数据,超时才发送后端删除 RPC;失败时恢复任务。
- 🔜 **尚未完成**:三平台按真实快捷键进行实机回归。
- ✅ Linux 批量排期:多选状态下可设置今天、明天或清除日期;
清除日期同时清除 due_time,操作按任务 ID 捕获并重新查找真实任务。
- 🔜 **尚未完成**:Linux 批量删除与可靠撤销,以及三平台按真实快捷键进行实机回归。

### 2.5 Quick Find

Expand Down
69 changes: 69 additions & 0 deletions linux/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<std::int64_t> ids;
std::optional<std::string> 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(
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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<GtkWidget*>(
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<std::string> 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<BulkScheduleCommand*>(data);
});
g_signal_connect(
option, "clicked",
G_CALLBACK(+[](GtkButton* pressed, gpointer) {
auto* command = static_cast<BulkScheduleCommand*>(
g_object_get_data(G_OBJECT(pressed), "batch-schedule-command"));
if (command != nullptr) batch_schedule_selected(*command);
auto* p = static_cast<GtkWidget*>(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();
Expand Down Expand Up @@ -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);
Expand Down
Loading