diff --git a/docs/UX-BENCHMARKS.md b/docs/UX-BENCHMARKS.md
index 37566c2..52a6fd7 100644
--- a/docs/UX-BENCHMARKS.md
+++ b/docs/UX-BENCHMARKS.md
@@ -84,8 +84,12 @@ Scheduled(有日期的)。
- ✅ Windows 批量操作(分阶段):选择多条任务时显示共享操作栏;
支持一次批量标记完成、批量移动到目标列表;使用稳定任务 ID,列表刷新
与搜索不会把操作错误应用到其他任务。打包验收覆盖两条任务批量完成。
-- 🔜 **尚未完成**:Windows 批量排期/删除及撤销、行内详情展开;
- Linux 批量操作;以及三平台按真实快捷键进行实机回归。
+- ✅ Windows 批量排期:多选时支持今天、明天、清除三种日期操作;
+ 清除日期同时清除时间,避免任务残留不可见的 due_time。
+- ✅ Linux:基于 GTK 原生多选(#33)增加可见的批量完成和批量移动操作栏,
+ 同时适用于当前列表和全局搜索。刷新时按任务 ID 保留选择。
+- 🔜 **尚未完成**:Windows 批量删除及撤销、行内详情展开;
+ Linux 批量排期/删除及撤销;以及三平台按真实快捷键进行实机回归。
### 2.5 Quick Find
diff --git a/scripts/e2e/e2e-windows.ps1 b/scripts/e2e/e2e-windows.ps1
index 5d0255f..cc78518 100644
--- a/scripts/e2e/e2e-windows.ps1
+++ b/scripts/e2e/e2e-windows.ps1
@@ -250,6 +250,12 @@ try {
$batchComplete = Wait-Until 'multi-selection action bar' {
Find-ById $window 'bulk-complete'
}
+ $batchSchedule = Wait-Until 'multi-selection scheduling action' {
+ Find-ById $window 'bulk-schedule'
+ }
+ if (-not $batchSchedule.Current.IsEnabled) {
+ throw 'batch scheduling is disabled for two selected tasks'
+ }
$batchComplete.GetCurrentPattern(
[System.Windows.Automation.InvokePattern]::Pattern).Invoke()
[void](Wait-Until 'both selected tasks completed by a single command' {
@@ -296,6 +302,7 @@ try {
'view switches preserve task list ownership',
'global search shows the real owning list for every task',
'Ctrl/Shift multi-selection completes two tasks in one command',
+ 'batch scheduling is visible and enabled when multiple tasks are selected',
'native window close stopped the process'
)
} | ConvertTo-Json -Depth 5 |
diff --git a/shared/i18n/en.json b/shared/i18n/en.json
index 5e6a084..07ba631 100644
--- a/shared/i18n/en.json
+++ b/shared/i18n/en.json
@@ -11,6 +11,7 @@
"menuLangEn": "English",
"menuLangZh": "Simplified Chinese",
"bulkSelectedCount": "{0} selected",
+ "bulkSchedule": "Schedule Selected",
"bulkComplete": "Complete Selected",
"menuMoveToList": "Move to List",
"menuToggleCompleted": "Mark Completed / Uncompleted",
diff --git a/shared/i18n/zh.json b/shared/i18n/zh.json
index e7d2932..68c19ee 100644
--- a/shared/i18n/zh.json
+++ b/shared/i18n/zh.json
@@ -11,6 +11,7 @@
"menuLangEn": "English",
"menuLangZh": "简体中文",
"bulkSelectedCount": "已选择 {0} 项",
+ "bulkSchedule": "批量排期",
"bulkComplete": "全部完成",
"menuMoveToList": "移动到列表",
"menuToggleCompleted": "标记完成/未完成",
diff --git a/windows/MainWindow.xaml b/windows/MainWindow.xaml
index e22f1c5..99deb27 100644
--- a/windows/MainWindow.xaml
+++ b/windows/MainWindow.xaml
@@ -352,6 +352,9 @@
+
diff --git a/windows/MainWindow.xaml.cpp b/windows/MainWindow.xaml.cpp
index 6ea1351..edc9c3a 100644
--- a/windows/MainWindow.xaml.cpp
+++ b/windows/MainWindow.xaml.cpp
@@ -754,6 +754,7 @@ void MainWindow::ApplyLanguage() {
ShowCompletedCheck().Content(winrt::box_value(t("showCompletedToggle")));
BulkCompleteButton().Content(winrt::box_value(t("bulkComplete")));
BulkMoveButton().Content(winrt::box_value(t("menuMoveToList")));
+ BulkScheduleButton().Content(winrt::box_value(t("bulkSchedule")));
RenderSidebar();
RenderTasks();
}
@@ -1758,6 +1759,9 @@ winrt::fire_and_forget MainWindow::SetTaskDueAsync(std::int64_t id,
updated.due_date =
due.empty() ? std::nullopt
: std::optional(winrt::to_string(due));
+ // A date-less task cannot retain an invisible due time. Match Linux
+ // date-clear semantics and the shared "clear schedule" contract.
+ if (due.empty()) updated.due_time = std::nullopt;
auto const dispatcher = DispatcherQueue();
auto const weak = get_weak();
auto backend = backend_;
@@ -2377,6 +2381,43 @@ void MainWindow::OnBulkMoveClicked(
menu.ShowAt(BulkMoveButton());
}
+void MainWindow::ScheduleSelectedTasks(
+ std::vector const& ids, std::wstring const& due) {
+ // Freeze the selection when opening the flyout, not when an asynchronous
+ // view refresh eventually invokes a menu callback.
+ for (auto const id : ids) {
+ if (FindTaskForAction(id) != nullptr) {
+ SetTaskDueAsync(id, due);
+ }
+ }
+}
+
+void MainWindow::OnBulkScheduleClicked(
+ winrt::Windows::Foundation::IInspectable const&,
+ Microsoft::UI::Xaml::RoutedEventArgs const&) {
+ auto const ids = SelectedTaskIds();
+ if (ids.size() < 2) return;
+ auto menu = mxc::MenuFlyout{};
+ auto const weak = get_weak();
+ auto add_option = [&](std::wstring const& text,
+ std::wstring const& due) {
+ auto option = mxc::MenuFlyoutItem{};
+ option.Text(text);
+ option.Click([weak, ids, due](
+ winrt::Windows::Foundation::IInspectable const&,
+ Microsoft::UI::Xaml::RoutedEventArgs const&) {
+ if (auto window = weak.get()) {
+ window->ScheduleSelectedTasks(ids, due);
+ }
+ });
+ menu.Items().Append(option);
+ };
+ add_option(t("navToday"), date_string(0));
+ add_option(t("dateTomorrow"), date_string(1));
+ add_option(t("dialogClear"), L"");
+ menu.ShowAt(BulkScheduleButton());
+}
+
void MainWindow::OnTaskListKeyDown(
winrt::Windows::Foundation::IInspectable const& sender,
Microsoft::UI::Xaml::Input::KeyRoutedEventArgs const& args) {
diff --git a/windows/MainWindow.xaml.h b/windows/MainWindow.xaml.h
index d6e9b7f..8d81aab 100644
--- a/windows/MainWindow.xaml.h
+++ b/windows/MainWindow.xaml.h
@@ -82,6 +82,8 @@ struct MainWindow : MainWindowT {
Microsoft::UI::Xaml::RoutedEventArgs const&);
void OnBulkMoveClicked(winrt::Windows::Foundation::IInspectable const&,
Microsoft::UI::Xaml::RoutedEventArgs const&);
+ void OnBulkScheduleClicked(winrt::Windows::Foundation::IInspectable const&,
+ Microsoft::UI::Xaml::RoutedEventArgs const&);
// Update download action wired from XAML; event handlers must be public
// because generated Connect() binds them through MainWindowT.
@@ -119,6 +121,8 @@ struct MainWindow : MainWindowT {
std::vector SelectedTaskIds();
void MoveSelectedTasks(std::vector const& ids,
std::int64_t target_list_id);
+ void ScheduleSelectedTasks(std::vector const& ids,
+ std::wstring const& due);
void RefreshQuickAddPreview();
void ApplyTitleBarTheme();
void OnEffectiveThemeChanged();