diff --git a/CHANGELOG.md b/CHANGELOG.md index 292e419..555e554 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- About dialog becomes the update/link/feedback portal on every platform: + User Manual and Changelog links join Website and Report an Issue, and a + diagnostics block (release identity, platform, database and config + paths) carries a Copy Diagnostics button so a bug report can include + the full context verbatim (#58). - Release-kind acceptance: the update chain — signed-feed rejection, download cancellation, the N→N+1 lifecycle, data preservation, and the Linux AppImage in-place swap — is now an executable `release` diff --git a/docs/UX-BENCHMARKS.md b/docs/UX-BENCHMARKS.md index adc6bb1..39b5506 100644 --- a/docs/UX-BENCHMARKS.md +++ b/docs/UX-BENCHMARKS.md @@ -146,3 +146,45 @@ Scheduled(有日期的)。 - Manual acceptance: move a task from 工作 to 生活, confirm it disappears from 工作, appears in 生活 and keeps all other fields; then restart and verify ownership persists. Exercise both languages and all smart-list views. + +## Raycast 对照(商业标杆,2026-10-23 实拆 2.9.0.0) + +> 对象:Raycast(macOS 启动器,收费商业软件)。实拆官方 DMG:Info.plist、 +> 自研更新器(Updater/UpdaterDaemon,无 Sparkle)、IPC 面、Settings 前端 +> 包、InternetAccessPolicy.plist,对照 manual.raycast.com。结论按 +> 「学 / 参考 / 不学」记录,落地见 PRODUCT-SPEC §8 与对应切片。 + +### 学到的(已落地或已排期) + +- **About = 更新/链接/反馈门户**:Raycast 的 Settings → About 收口 + Raycast Updates、Open Changelog、Website、社区、Send Feedback 五个入口。 + Taskly 采纳:About 增加 User Manual 与 Changelog 链接(Website/Report an + Issue 已有),构成「更新(菜单)+ 手册 + 日志 + 反馈」门户。 +- **复制诊断信息**:Raycast IPC 有 CopyLogFiles。Taskly 采纳:About 增加 + 诊断块(版本/平台/架构/OS/DB 路径/配置路径,英文固定标签,可选中)+ + 「复制诊断信息」按钮,粘贴进 issue 即可带全上下文。 +- **网络透明度**:Raycast 随包分发 InternetAccessPolicy.plist,按主机声明 + 用途与拒绝后果(Little Snitch 等防火墙直接呈现给用户)。Taskly 采纳为 + 安装链切片:声明 github.com 更新源——把「本地优先、只连更新源」的信任 + 故事写进包里。 +- **更新行为显性化**:Raycast 把更新入口明示在 About。Taskly 排期:设置页 + 增加「自动检查更新」开关(默认开,写 config),把已有的 4h 静默检查 + 显性化;下载后仍确认再装(与 Raycast 的退出静默安装分叉——任务管理器 + 是数据工具,保留用户控制感)。 +- **手册结构**:manual.raycast.com 的 Keyboard Shortcuts / Import & + Export / Troubleshooting 独立成章、changelog 挂首页。Taskly 排期并入 + 手册切片:快捷键全表(§8 面向用户版)与「数据在哪/如何备份」章节。 + +### 参考但不照搬 + +- 退出时静默自动安装(CanShutDownForAutoInstall):不采纳,理由同上。 +- Sentry 崩溃上报内置于包:v1 无遥测、crash 仅 opt-in,按自己的清单走。 +- 社区/Slack 入口:单机 + 云盘同步路线,GitHub issues 即反馈渠道。 +- 更新后一次性亮点提示(changelog 一等公民):条件比 Raycast 更好 + (结构化 CHANGELOG 自动进 release),列为后续增量切片。 + +### 明确不学 + +账号/Store/云同步/AI(与本地优先正面冲突);纯英文零本地化(双语是 +Taskly 契约优势);设置页做 WebKit 壳(宿主原生 + i18n 单源);macOS 26 +最低版本门槛(14+ 是真实覆盖面差异)。 diff --git a/linux/src/main.cpp b/linux/src/main.cpp index 4d714cb..beab5d8 100644 --- a/linux/src/main.cpp +++ b/linux/src/main.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -1813,11 +1814,55 @@ void show_about_dialog() { gtk_button_set_label(GTK_BUTTON(website), tr("aboutWebsite").c_str()); gtk_widget_set_hexpand(website, FALSE); gtk_box_append(GTK_BOX(links), website); + auto* manual = gtk_link_button_new("https://taskly.jrtx.site/manual.html"); + gtk_button_set_label(GTK_BUTTON(manual), tr("aboutManual").c_str()); + gtk_widget_set_hexpand(manual, FALSE); + gtk_box_append(GTK_BOX(links), manual); + auto* links_row2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 24); + gtk_widget_set_halign(links_row2, GTK_ALIGN_CENTER); + gtk_box_append(GTK_BOX(content), links_row2); + auto* changelog = gtk_link_button_new( + "https://github.com/turinglambdaai/taskly/blob/main/CHANGELOG.md"); + gtk_button_set_label(GTK_BUTTON(changelog), tr("aboutChangelog").c_str()); + gtk_widget_set_hexpand(changelog, FALSE); + gtk_box_append(GTK_BOX(links_row2), changelog); auto* issue = gtk_link_button_new( "https://github.com/turinglambdaai/taskly/issues"); gtk_button_set_label(GTK_BUTTON(issue), tr("aboutReportIssue").c_str()); gtk_widget_set_hexpand(issue, FALSE); - gtk_box_append(GTK_BOX(links), issue); + gtk_box_append(GTK_BOX(links_row2), issue); + // Diagnostics block (PRODUCT-SPEC §8): the release identity and the + // ~/.taskly contract paths a bug report starts from. Labels are English + // fixed strings — machine-oriented like the CLI's --json. + static std::string diagnostics = [] { + utsname names{}; + uname(&names); + return std::string("Taskly v") + rivet_app::kVersion + " (" + + names.sysname + " " + names.release + ", " + names.machine + ")" + + "\nDatabase: " + g_get_home_dir() + "/.taskly/tasks.db" + + "\nConfig: " + g_get_home_dir() + "/.taskly/config.ini"; + }(); + auto* diagnostics_label = gtk_label_new(diagnostics.c_str()); + gtk_label_set_selectable(GTK_LABEL(diagnostics_label), TRUE); + gtk_label_set_justify(GTK_LABEL(diagnostics_label), GTK_JUSTIFY_LEFT); + PangoAttrList* mono = pango_attr_list_new(); + pango_attr_list_insert(mono, pango_attr_family_new("monospace")); + gtk_label_set_attributes(GTK_LABEL(diagnostics_label), mono); + pango_attr_list_unref(mono); + gtk_widget_set_halign(diagnostics_label, GTK_ALIGN_CENTER); + gtk_box_append(GTK_BOX(content), diagnostics_label); + auto* copy_diagnostics = gtk_button_new_with_label( + tr("aboutCopyDiagnostics").c_str()); + gtk_widget_set_halign(copy_diagnostics, GTK_ALIGN_CENTER); + g_signal_connect( + copy_diagnostics, "clicked", + G_CALLBACK(+[](GtkButton*, gpointer user_data) { + auto* text = static_cast(user_data); + auto* clipboard = gdk_display_get_clipboard(gdk_display_get_default()); + gdk_clipboard_set_text(clipboard, text->c_str()); + }), + &diagnostics); + gtk_box_append(GTK_BOX(content), copy_diagnostics); g_signal_connect( dialog, "response", G_CALLBACK(+[](GtkDialog* dialog, gint, gpointer) { diff --git a/macos-host/Sources/RivetHost/Sheets.swift b/macos-host/Sources/RivetHost/Sheets.swift index c8aee33..7a967ab 100644 --- a/macos-host/Sources/RivetHost/Sheets.swift +++ b/macos-host/Sources/RivetHost/Sheets.swift @@ -1,3 +1,4 @@ +import AppKit import SwiftUI /// Signed ARGB int as stored in the DB color column (DATA-FORMAT §2): @@ -244,7 +245,8 @@ private struct ColorSwatchGrid: View { } } -/// About box (Help ▸ About) — `Taskly v` (PRODUCT-SPEC §8). +/// About box (Help ▸ About) — the update/link/feedback hub and the +/// diagnostics surface (PRODUCT-SPEC §8). struct AboutSheet: View { @Environment(AppModel.self) private var model @Environment(\.dismiss) private var dismiss @@ -254,6 +256,34 @@ struct AboutSheet: View { ?? RivetGeneratedConfig.version } + /// The diagnostics block a bug report starts from (PRODUCT-SPEC §8): + /// release identity plus the `~/.taskly` contract paths. Labels are + /// English fixed strings — machine-oriented like the CLI's --json. + private var diagnosticsText: String { + let os = ProcessInfo.processInfo.operatingSystemVersion + let arch: String + #if arch(arm64) + arch = "arm64" + #elseif arch(x86_64) + arch = "x86_64" + #else + arch = "unknown" + #endif + let home = FileManager.default.homeDirectoryForCurrentUser.path + return """ + Taskly \(version) (macOS \(os.majorVersion).\(os.minorVersion), \(arch)) + Database: \(home)/.taskly/tasks.db + Config: \(home)/.taskly/config.ini + """ + } + + private func copyDiagnostics() { + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + pasteboard.setString(diagnosticsText, forType: .string) + model.flashStatus(model.t("statusDiagnosticsCopied")) + } + var body: some View { VStack(spacing: 12) { Image(systemName: "checklist") @@ -275,11 +305,29 @@ struct AboutSheet: View { HStack(spacing: 20) { Link(model.t("aboutWebsite"), destination: URL(string: "https://taskly.jrtx.site")!) + Link(model.t("aboutManual"), + destination: URL(string: "https://taskly.jrtx.site/manual.html")!) + } + .font(.system(size: 12)) + .tint(model.theme.accent) + HStack(spacing: 20) { + Link(model.t("aboutChangelog"), + destination: URL(string: "https://github.com/turinglambdaai/taskly/blob/main/CHANGELOG.md")!) Link(model.t("aboutReportIssue"), destination: URL(string: "https://github.com/turinglambdaai/taskly/issues")!) } .font(.system(size: 12)) .tint(model.theme.accent) + Text(diagnosticsText) + .font(.system(size: 10, design: .monospaced)) + .foregroundStyle(model.theme.secondaryText) + .textSelection(.enabled) + .multilineTextAlignment(.leading) + .lineLimit(3) + Button(model.t("aboutCopyDiagnostics")) { copyDiagnostics() } + .font(.system(size: 12)) + .buttonStyle(.bordered) + .tint(model.theme.accent) Button(model.t("dialogConfirm")) { dismiss() } .keyboardShortcut(.defaultAction) .buttonStyle(.borderedProminent) diff --git a/shared/i18n/en.json b/shared/i18n/en.json index 07ba631..d19d0b2 100644 --- a/shared/i18n/en.json +++ b/shared/i18n/en.json @@ -131,5 +131,9 @@ "aboutLicense": "Released under the GNU AGPL-3.0", "aboutWebsite": "Website", "aboutReportIssue": "Report an Issue", + "aboutManual": "User Manual", + "aboutChangelog": "Changelog", + "aboutCopyDiagnostics": "Copy Diagnostics", + "statusDiagnosticsCopied": "Diagnostics copied — paste it into your issue", "updateCopyError": "Copy error" } diff --git a/shared/i18n/zh.json b/shared/i18n/zh.json index 68c19ee..c287cc0 100644 --- a/shared/i18n/zh.json +++ b/shared/i18n/zh.json @@ -131,5 +131,9 @@ "aboutLicense": "以 GNU AGPL-3.0 授权发布", "aboutWebsite": "访问官网", "aboutReportIssue": "问题反馈", + "aboutManual": "使用手册", + "aboutChangelog": "更新日志", + "aboutCopyDiagnostics": "复制诊断信息", + "statusDiagnosticsCopied": "已复制诊断信息,可直接粘贴到 issue", "updateCopyError": "复制错误信息" } diff --git a/shared/spec/PRODUCT-SPEC.md b/shared/spec/PRODUCT-SPEC.md index 9d1efba..73db8dc 100644 --- a/shared/spec/PRODUCT-SPEC.md +++ b/shared/spec/PRODUCT-SPEC.md @@ -166,12 +166,20 @@ pickers. Save (blank text silently ignored), Delete (confirm first), Cancel Check for Updates… (manual online-update check; auto-check runs at launch and every 4 h — contract: `UPDATE.md`). - Help: About — `Taskly v` + `© 2026 Taskly Team` + - `aboutContent` + license line (`aboutLicense`) + two link buttons on - every host: Website (`aboutWebsite` → taskly.jrtx.site) and Report an - Issue (`aboutReportIssue` → the GitHub issue tracker). macOS and - Windows render the fields inside their sheet/dialog; Linux builds the - same layout on a plain GtkDialog — GtkAboutDialog cannot carry a - second link, so the native about dialog is not used. + `aboutContent` + license line (`aboutLicense`) + link buttons on every + host: Website (`aboutWebsite` → taskly.jrtx.site), User Manual + (`aboutManual` → taskly.jrtx.site/manual.html), Changelog + (`aboutChangelog` → the CHANGELOG on GitHub), and Report an Issue + (`aboutReportIssue` → the GitHub issue tracker). Below the links, a + diagnostics block shows the release identity and the data locations — + app version, platform + architecture, OS version, database path, + config path — with a `aboutCopyDiagnostics` button that puts the same + block on the clipboard so a bug report can include it verbatim; the + copy confirmation is a transient status message. Diagnostics labels + are English fixed strings (machine-oriented, like the CLI's `--json`). + macOS and Windows render the fields inside their sheet/dialog; Linux + builds the same layout on a plain GtkDialog — GtkAboutDialog cannot + carry multiple links, so the native about dialog is not used. - All accelerators work window-wide without opening the menu. ## 9. Reminders (all platforms) diff --git a/windows/MainWindow.xaml.cpp b/windows/MainWindow.xaml.cpp index 91066eb..43e4bd7 100644 --- a/windows/MainWindow.xaml.cpp +++ b/windows/MainWindow.xaml.cpp @@ -2454,16 +2454,80 @@ winrt::fire_and_forget MainWindow::OnAbout( L"https://taskly.jrtx.site"}); website.HorizontalAlignment(mx::HorizontalAlignment::Center); website.Padding({0, 0, 0, 0}); + auto manual = mxc::HyperlinkButton{}; + manual.Content(winrt::box_value(t("aboutManual"))); + manual.NavigateUri(winrt::Windows::Foundation::Uri{ + L"https://taskly.jrtx.site/manual.html"}); + manual.HorizontalAlignment(mx::HorizontalAlignment::Center); + manual.Padding({0, 0, 0, 0}); + auto changelog = mxc::HyperlinkButton{}; + changelog.Content(winrt::box_value(t("aboutChangelog"))); + changelog.NavigateUri(winrt::Windows::Foundation::Uri{ + L"https://github.com/turinglambdaai/taskly/blob/main/CHANGELOG.md"}); + changelog.HorizontalAlignment(mx::HorizontalAlignment::Center); + changelog.Padding({0, 0, 0, 0}); auto issue = mxc::HyperlinkButton{}; issue.Content(winrt::box_value(t("aboutReportIssue"))); issue.NavigateUri(winrt::Windows::Foundation::Uri{ L"https://github.com/turinglambdaai/taskly/issues"}); issue.HorizontalAlignment(mx::HorizontalAlignment::Center); issue.Padding({0, 0, 0, 0}); + // Diagnostics block (PRODUCT-SPEC §8): the release identity and the + // ~/.taskly contract paths a bug report starts from. Labels are English + // fixed strings — machine-oriented like the CLI's --json. + std::wstring profile; + wchar_t profile_buf[MAX_PATH]; + const DWORD profile_len = GetEnvironmentVariableW( + L"USERPROFILE", profile_buf, MAX_PATH); + if (profile_len > 0 && profile_len < MAX_PATH) { + profile.assign(profile_buf, profile_len); + } + const std::wstring diagnostics = + std::wstring(L"Taskly v") + wide(rivet_app::kVersion) + + L" (Windows, x64)\nDatabase: " + profile + L"\\.taskly\\tasks.db" + + L"\nConfig: " + profile + L"\\.taskly\\config.ini"; + auto diagnostics_text = mxc::TextBlock{}; + diagnostics_text.Text(diagnostics); + diagnostics_text.FontFamily(mx::Media::FontFamily{L"Consolas"}); + diagnostics_text.FontSize(11); + diagnostics_text.Margin({0, 8, 0, 0}); + diagnostics_text.IsTextSelectionEnabled(true); + auto copy_diagnostics = mxc::Button{}; + copy_diagnostics.Content(winrt::box_value(t("aboutCopyDiagnostics"))); + copy_diagnostics.HorizontalAlignment(mx::HorizontalAlignment::Center); + copy_diagnostics.Padding({12, 4, 12, 4}); + copy_diagnostics.Click( + [text = winrt::weak_ref(copy_diagnostics), diagnostics]( + auto&&, auto&&) { + auto package = + winrt::Windows::ApplicationModel::DataTransfer::DataPackage{}; + package.RequestedOperation( + winrt::Windows::ApplicationModel::DataTransfer:: + DataPackageOperation::Copy); + package.SetText(diagnostics); + winrt::Windows::ApplicationModel::DataTransfer::Clipboard::SetContent( + package); + if (auto button = text.get()) { + button.Content(winrt::box_value(t("statusDiagnosticsCopied"))); + auto restore = mx::DispatcherTimer(); + restore.Interval(std::chrono::milliseconds{2500}); + restore.Tick([restore, text](auto&&, auto&&) { + if (auto b = text.get()) { + b.Content(winrt::box_value(t("aboutCopyDiagnostics"))); + } + restore.Stop(); + }); + restore.Start(); + } + }); panel.Children().Append(tagline); panel.Children().Append(license); panel.Children().Append(website); + panel.Children().Append(manual); + panel.Children().Append(changelog); panel.Children().Append(issue); + panel.Children().Append(diagnostics_text); + panel.Children().Append(copy_diagnostics); auto dialog = mxc::ContentDialog{}; dialog.Title(winrt::box_value(L"Taskly v" + wide(rivet_app::kVersion)));