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
24 changes: 23 additions & 1 deletion App/Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,21 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
noteBackupRootURL: root.appendingPathComponent(NoteBackupRing.subdirectoryName),
startsAtBlankEntries: true)
}()
static let updates = WeiBeiUpdateService()
static let updates: WeiBeiUpdateService = {
let service = WeiBeiUpdateService()
service.prepareForInstallation = {
let store = AppDelegate.workspace
store.commitCurrentReaderLocation()
guard await store.freshActiveNoteEditorSnapshot() else { return false }
store.flushPendingNotePersistence(flushWorkspace: false)
guard await store.flushPendingWorkspaceSaveAsync() else { return false }
// A file conflict/write failure must not be mistaken for a saved note.
return !store.noteEditingSession.dirty
&& store.noteEditingSession.saveStatus != .failed
&& store.noteEditingSession.saveStatus != .externallyModified
}
return service
}()
private var lifecycleObservers: [NSObjectProtocol] = []
private var saveTask: Task<Void, Never>?
private var backgroundTask: UIBackgroundTaskIdentifier = .invalid
Expand All @@ -42,6 +56,14 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
WeiBeiTypography.registerBundledFonts()
guard !Self.usesFixture else { return true }
_ = Self.workspace
let defaults = UserDefaults.standard
if let pendingBuild = defaults.string(forKey: WeiBeiUpdateService.pendingInstallationBuildKey),
pendingBuild == Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String,
let version = defaults.string(forKey: WeiBeiUpdateService.pendingInstallationVersionKey) {
Self.workspace.showTransientNoteStatus(Self.workspace.ui("已更新到 \(version)", "Updated to \(version)"))
defaults.removeObject(forKey: WeiBeiUpdateService.pendingInstallationBuildKey)
defaults.removeObject(forKey: WeiBeiUpdateService.pendingInstallationVersionKey)
}
lifecycleObservers.append(NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: .main) { [weak self] _ in
MainActor.assumeIsolated { self?.saveWorkspace() }
})
Expand Down
4 changes: 4 additions & 0 deletions App/WeiBei.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@
E3C57A914FAF086CBC2EE87E /* NativeToolSchemaValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B804330B9F87807BF475EB80 /* NativeToolSchemaValidation.swift */; };
E498931F372E542246CCF1E8 /* lanting-observe-kinds.png in Resources */ = {isa = PBXBuildFile; fileRef = A19EDFF7CF12F407B045AAFC /* lanting-observe-kinds.png */; };
E56FDBD0C5978CC30B934430 /* WorkspacePersistStatusDot.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E51498E496D5A61D998523 /* WorkspacePersistStatusDot.swift */; };
EAA3DEB378944BAE947BD215 /* WeiBeiUpdateControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA12C4B125CC4E2394E84785 /* WeiBeiUpdateControl.swift */; };
E60BC6DE707366A0549B937C /* NativeSessionTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 433E5657BF2C1DCD29347D42 /* NativeSessionTitle.swift */; };
E7939285E4E0C9937364F5C3 /* AgentReplyMemoryUpdateTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963BCB482D54E37E6DE3C842 /* AgentReplyMemoryUpdateTag.swift */; };
E818370843CF10A7F41BD707 /* AgentReplyProfileUpdateTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 672C78FE92DA58E70D7B494E /* AgentReplyProfileUpdateTag.swift */; };
Expand Down Expand Up @@ -472,6 +473,7 @@
CD62172095286419347BAD3C /* ReaderWebRemarkMarks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderWebRemarkMarks.swift; sourceTree = "<group>"; };
CFA8BD37F526A567FC7AF59A /* ConversationLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationLayout.swift; sourceTree = "<group>"; };
D1E51498E496D5A61D998523 /* WorkspacePersistStatusDot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspacePersistStatusDot.swift; sourceTree = "<group>"; };
FA12C4B125CC4E2394E84785 /* WeiBeiUpdateControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeiBeiUpdateControl.swift; sourceTree = "<group>"; };
D4DC97F117C4C9EF8A872EAA /* SelectionAnchorContentPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionAnchorContentPoint.swift; sourceTree = "<group>"; };
D9660747C198993E15D3D640 /* NativeLLMAdapterFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeLLMAdapterFactory.swift; sourceTree = "<group>"; };
DBE910F7A08873EDEB2E1769 /* DiagramView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagramView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -789,6 +791,7 @@
B215538091FA28517FF16935 /* StableDocumentWorkspace.swift */,
6B078F9A88BC6C5982B3722B /* WeiBeiSegmentedControl.swift */,
D1E51498E496D5A61D998523 /* WorkspacePersistStatusDot.swift */,
FA12C4B125CC4E2394E84785 /* WeiBeiUpdateControl.swift */,
A41AD505070D316B0402B46D /* Settings */,
);
path = Views;
Expand Down Expand Up @@ -1354,6 +1357,7 @@
2CD678E8BE84257E5938AAAD /* WorkspaceInteractionState.swift in Sources */,
E94B46A70080CB4FDC671F71 /* WorkspacePaneState.swift in Sources */,
E56FDBD0C5978CC30B934430 /* WorkspacePersistStatusDot.swift in Sources */,
EAA3DEB378944BAE947BD215 /* WeiBeiUpdateControl.swift in Sources */,
0D8AC2AC2806968EF9F4C86A /* WorkspaceStore+AppearancePreference.swift in Sources */,
BA6C858DA3558EC483AC6E6C /* WorkspaceStore+CourseContent.swift in Sources */,
C21F9B2F6453AD3F0A0A8FD7 /* WorkspaceStore+CourseLibrary.swift in Sources */,
Expand Down
29 changes: 23 additions & 6 deletions App/WindowBridge/NativeWindowBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -327,15 +327,32 @@ final class NativeWindowBridge: NSObject, CatalystWindowBridge {
code: code
))
}
@MainActor func observeUpdates(_ observer: @escaping (String, String?, [String], Bool, URL?) -> Void) {
updateObservation = updateService.$status.combineLatest(updateService.$availableUpdate)
.sink { status, update in
observer(status.rawValue, update?.version, update?.releaseNotesLines ?? [],
update?.informationOnly ?? false, update?.informationURL)
@MainActor func observeUpdates(_ observer: @escaping (NSDictionary) -> Void) {
updateObservation = updateService.$status.combineLatest(
updateService.$availableUpdate, updateService.$downloadProgress, updateService.$errorDescription
).sink { status, update, progress, error in
var snapshot: [String: Any] = ["status": status.rawValue]
if let update {
snapshot["version"] = update.version
snapshot["notes"] = update.releaseNotesLines
snapshot["informationOnly"] = update.informationOnly
snapshot["informationURL"] = update.informationURL
snapshot["date"] = update.publishedDate
}
snapshot["progress"] = progress
snapshot["error"] = error
observer(snapshot as NSDictionary)
}
}
@MainActor func checkForUpdates() { updateService.checkForUpdates() }
@MainActor func installAvailableUpdate() { updateService.installAvailableUpdate() }
@MainActor func installAvailableUpdate(_ save: @escaping (@escaping (Bool) -> Void) -> Void) {
updateService.prepareForInstallation = {
await withCheckedContinuation { continuation in
save { continuation.resume(returning: $0) }
}
}
updateService.installAvailableUpdate()
}
deinit { observers.forEach(NotificationCenter.default.removeObserver) }
}

Expand Down
4 changes: 2 additions & 2 deletions App/WindowBridge/WindowBridgeContract.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import Foundation
presentationToolbar: NSObject?,
completion: @MainActor @escaping ([URL], NSError?) -> Void
)
@MainActor func observeUpdates(_ observer: @escaping (String, String?, [String], Bool, URL?) -> Void)
@MainActor func observeUpdates(_ observer: @escaping (NSDictionary) -> Void)
@MainActor func checkForUpdates()
@MainActor func installAvailableUpdate()
@MainActor func installAvailableUpdate(_ save: @escaping (@escaping (Bool) -> Void) -> Void)
}
45 changes: 45 additions & 0 deletions Docs/update-experience.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# 魏碑更新体验设计建议(按用户批注修订)

2026-10-10。状态:实现中,真实应用交互尚待验收。本版取代之前的点击打开面板、面板底部操作和继续使用按钮方案。

## 核心交互

悬停阅读说明,点击执行操作,过程和阶段在更新按钮上体现。说明面板不承担更新操作。

| 状态 | 更新按钮 | 悬停面板 | 点击行为 |
|---|---|---|---|
| 有更新 | 下载图标;默认不展示“有更新”常驻文字 | 版本、日期与本次实际更新说明 | 立即下载 |
| 下载中 | 图标区域的环形真实进度,旁边百分比 | 保留本次更新说明 | 不重复触发下载 |
| 解压、准备 | 同一位置显示阶段,如“准备中” | 保留本次更新说明 | 等待准备完成 |
| 就绪 | “安装并重启” | 保留本次更新说明 | 先保存最新编辑内容与工作区,成功后安装重启 |
| 失败 | 重试入口 | 简短具体原因,再保留更新说明 | 重试失败的操作 |

鼠标悬停按钮才打开说明面板;移入面板仍可阅读、滚动;离开按钮和面板后收起。点击按钮不会把面板锁定。后台状态变化不主动弹出面板。

入口保留在窗口左上角独立区:阅读工作区紧随前进/后退之后、保持清晰分组间距,向右自然展开而不挤占导航按键;资料库与课程页位于返回工作台按钮旁。采用现代 Apple Pro / Linear 精工双层微岛架构:常态为 24pt 高清纯色徽标(电力珊瑚色底与白色精准符号 arrow.down.to.line),微光描边与柔和环境阴影,悬停时微放大,点击带有 0.95 机械触感按压微反馈。下载中平滑过渡为紧凑胶囊,内嵌白色环形进度与等宽百分比;准备就绪时平滑展开为翡翠绿微标与「安装并重启」胶囊,点击直接保存现场并安全重启。悬停时平滑呼出更新说明面板(220ms 跨指针防抖),面板顶部摒弃重复图标与多余胶囊、以纯正排版呈现版本与日期,更新条目支持行内 Markdown 与彩色分组导轨,长条目配备视口底部渐隐滚动遮罩,彻底杜绝所有伪古风陈旧概念与多余新手引导文案。

下载完成后保留“安装并重启”,等待用户点击。删除“继续使用”按钮;删除退出时安装的说明文案。更新完成后恢复原资料、阅读位置、笔记与工作区,并给出轻量完成反馈。错误不清空工作现场。

## 说明面板内容

版本和发布日期放在头部,下面直接列本次真实变化。新增、改进、修复在本次内容需要时分组。只有一项修复,就写这一项。必要的使用变化、系统要求或已知问题按实际发布内容说明。

采用魏碑既有主题、字体和细分隔线,靠文字排版和留白保证阅读。不添加宣传标题、收益导语、装饰插画、额外操作底栏,也不规定每次条目数量。长说明自然滚动,鼠标进入面板可以继续读。

## 更贴近本版的成熟参考

- [待办工具 Things 的 Mac 更新说明](https://culturedcode.com/things/support/articles/1100684/):版本、日期、适用系统和直接的变更条目。其 3.22.12 只有一项简短内部调整。参考其信息量按实际变化决定的写法,不照搬系统要求栏,也不据此声称它用悬停更新面板。
- [Mac 编辑器 Nova 的版本记录](https://nova.app/releases/):日常修复版本按具体问题逐条列出。参考正文的具体性,不照搬大版本的功能宣传结构。
- [开发工具 IntelliJ IDEA 的悬停文档](https://www.jetbrains.com/help/idea/documentation-tool-window.html?keymap=primary_default_for_windows):支持鼠标悬停查看说明。仅作为悬停读信息的交互参考,不是其更新界面的证据。

没有找到足以证明某款成熟应用完整采用“悬停更新说明、点击直接下载、同一按钮进度和重启”的官方资料。本版是按用户批注组合后的魏碑设计决策,不冒充竞品复刻。

## 必须同步实现的行为

- 下载和解压使用更新组件提供的实际数据。下载总量不可用时显示真实阶段,不制造百分比。
- 下载完成不得直接批准重启,应等待用户点击安装。没有操作时正常使用魏碑。
- 安装前等待最新笔记与工作区保存成功;失败停在错误状态,允许重试。正常退出路径也要保证保存完成。
- 重启恢复实际阅读位置和编辑现场,需要真实应用验证,设计预览不等于已经实现。
- 按实际安装阶段接通延期和重新进入安装的行为,不通过取消更新伪造就绪,也不依靠额外文案补救错误流程。

实现采用一个工具栏更新组件,共用原有更新服务。构建与检查结果记录于合并请求;未操控用户正在使用的应用。
Loading
Loading