diff --git a/README.md b/README.md index 3a0ab94..15fcb8a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## iOS Native 应用 -`apps/ios` 提供不依赖 8788 控制台的原生 iPhone 应用,当前版本 **0.2.2**,构建号 9,运行在 iOS 16 及以上,产物为未签名 IPA,供 TrollStore 或自有签名流程安装。iOS 开发在 `ios-native-ipa` 分支进行。 +`apps/ios` 提供不依赖 8788 控制台的原生 iPhone 应用,当前版本 **0.2.2**,构建号 10,运行在 iOS 16 及以上,产物为未签名 IPA,供 TrollStore 或自有签名流程安装。iOS 开发在 `ios-native-ipa` 分支进行。 底部为节点、消息、设置三个入口: diff --git a/apps/ios/Design/generate_app_icon.swift b/apps/ios/Design/generate_app_icon.swift index 6507623..dc0ac3d 100644 --- a/apps/ios/Design/generate_app_icon.swift +++ b/apps/ios/Design/generate_app_icon.swift @@ -6,9 +6,12 @@ // light source from the upper left, no metal texture, so it still reads at the // 29pt settings size. The plate keeps the classic single SIM cut, top-left. // -// The field is deliberately neutral (R = G = B): the moment it drifts cool the -// whole icon reads as "blue icon" on a lit home screen. The gold is pushed warm -// and deep enough that it stays metal rather than mustard. +// The field is warm-neutral, not mathematically neutral. A perfectly grey R=G=B +// plate sitting next to saturated gold loses the comparison and reads cool on a +// lit home screen — that is how "there is no blue in it" still looked "blue" to +// the eye. Red runs 2..4 levels over blue instead, which is invisible as a cast +// but impossible to misread. The gold stays warm and deep so it reads metal +// rather than mustard. // // Every coordinate is in visual space (origin top-left, y grows down). import AppKit @@ -133,10 +136,10 @@ func directionalRim(_ path: CGPath, _ rect: CGRect, _ width: CGFloat, _ stops: [ fillLinear( rectPath(CGRect(x: 0, y: 0, width: side, height: side)), [ - // 石墨底严格等色。原先 B 比 R 高 1 级,缩到 60pt 后边缘像素仍会落在蓝色波段。 - (0.00, (32, 32, 32, 255)), - (0.55, (19, 19, 19, 255)), - (1.00, (10, 10, 10, 255)), + // 石墨底走暖一档:R 比 B 高 4 级,肉眼看不出偏色,但再也不可能被读成冷调。 + (0.00, (34, 32, 30, 255)), + (0.55, (20, 19, 18, 255)), + (1.00, (11, 10, 10, 255)), ] as [Stop], CGPoint(x: side * 0.06, y: 0), CGPoint(x: side * 0.94, y: side) @@ -161,7 +164,7 @@ let plateRect = CGRect( let plate = simPath(plateRect, corner: plateCorner, chamfer: plateChamfer) ctx.saveGState() -ctx.setShadow(offset: CGSize(width: 0, height: -22), blur: 46, color: rgb(0, 3, 2, 150)) +ctx.setShadow(offset: CGSize(width: 0, height: -22), blur: 46, color: rgb(2, 1, 0, 150)) ctx.addPath(plate) ctx.setFillColor(rgb(0, 0, 0)) ctx.fillPath() @@ -211,8 +214,9 @@ for (index, height) in slotHeights.enumerated() { fillLinear( path, [ - (0.00, (16, 16, 16, 255)), - (1.00, (32, 32, 32, 255)), + // 插槽与底同色温,避免金面板上的洞看起来发青。 + (0.00, (17, 16, 14, 255)), + (1.00, (34, 32, 30, 255)), ] as [Stop], CGPoint(x: rect.minX, y: rect.minY), CGPoint(x: rect.minX, y: rect.maxY) diff --git a/apps/ios/Sources/MultiSimAdmin/AppMetadata.swift b/apps/ios/Sources/MultiSimAdmin/AppMetadata.swift index 2a3f81d..786830f 100644 --- a/apps/ios/Sources/MultiSimAdmin/AppMetadata.swift +++ b/apps/ios/Sources/MultiSimAdmin/AppMetadata.swift @@ -2,5 +2,5 @@ public enum AppMetadata { public static let name = "MultiSimAdmin" public static let bundleIdentifier = "fun.chick.multi-simadmin" public static let marketingVersion = "0.2.2" - public static let buildVersion = "9" + public static let buildVersion = "10" } diff --git a/apps/ios/Sources/MultiSimAdmin/Features/Fleet/FleetTagBatchSheet.swift b/apps/ios/Sources/MultiSimAdmin/Features/Fleet/FleetTagBatchSheet.swift index 0fc74ae..d236bf3 100644 --- a/apps/ios/Sources/MultiSimAdmin/Features/Fleet/FleetTagBatchSheet.swift +++ b/apps/ios/Sources/MultiSimAdmin/Features/Fleet/FleetTagBatchSheet.swift @@ -71,11 +71,9 @@ struct FleetTagBatchSheet: View { .font(.caption) .foregroundStyle(AITheme.faint) } else { - LazyVGrid( - columns: [GridItem(.adaptive(minimum: 112), alignment: .leading)], - alignment: .leading, - spacing: 8 - ) { + // 与节点卡片的标签行同一排法:胶囊按标签长度取宽。栅格会把每枚拉到 + // 同一列宽,短标签右侧空出一片,看起来像控件没加载完。 + AIFlowLayout(spacing: 8) { ForEach(model.allTags, id: \.self) { tag in CoverageChip( title: tag, @@ -87,6 +85,7 @@ struct FleetTagBatchSheet: View { } } } + .frame(maxWidth: .infinity, alignment: .leading) } } } @@ -182,7 +181,6 @@ struct FleetTagBatchSheet: View { .monospacedDigit() .opacity(0.72) } - Spacer(minLength: 0) } .padding(.horizontal, 10) .frame(height: 38) diff --git a/apps/ios/Sources/MultiSimAdmin/Features/Fleet/FleetView.swift b/apps/ios/Sources/MultiSimAdmin/Features/Fleet/FleetView.swift index ae297de..e1767bf 100644 --- a/apps/ios/Sources/MultiSimAdmin/Features/Fleet/FleetView.swift +++ b/apps/ios/Sources/MultiSimAdmin/Features/Fleet/FleetView.swift @@ -1170,7 +1170,9 @@ extension FleetView { .padding(.vertical, 9) .accessibilityLabel("正在读取运行信息") } else { - Text("节点未返回运行信息") + // 这一区只放设备与容量读数;信号、延迟这些运行指标在上方的量表里, + // 说「未返回运行信息」会和同一张卡片上的延迟读数自相矛盾。 + Text("节点未返回设备信息") .font(.caption) .foregroundStyle(AITheme.faint) .padding(.vertical, 9) @@ -1479,59 +1481,8 @@ extension FleetView { } } - struct FlowLayout: Layout { - let spacing: CGFloat - - func sizeThatFits( - proposal: ProposedViewSize, - subviews: Subviews, - cache: inout () - ) -> CGSize { - let maxWidth = proposal.width ?? .infinity - var x: CGFloat = 0 - var y: CGFloat = 0 - var rowHeight: CGFloat = 0 - var rowWidth: CGFloat = 0 - - for subview in subviews { - let size = subview.sizeThatFits(.unspecified) - if x > 0, x + size.width > maxWidth { - x = 0 - y += rowHeight + spacing - rowHeight = 0 - rowWidth = 0 - } - x += size.width + spacing - rowHeight = max(rowHeight, size.height) - rowWidth = max(rowWidth, x - spacing) - } - - return CGSize(width: rowWidth, height: max(0, y + rowHeight - (rowHeight > 0 ? 0 : spacing))) - } - - func placeSubviews( - in bounds: CGRect, - proposal: ProposedViewSize, - subviews: Subviews, - cache: inout () - ) { - var x = bounds.minX - var y = bounds.minY - var rowHeight: CGFloat = 0 - - for subview in subviews { - let size = subview.sizeThatFits(.unspecified) - if x > bounds.minX, x + size.width > bounds.maxX { - x = bounds.minX - y += rowHeight + spacing - rowHeight = 0 - } - subview.place(at: CGPoint(x: x, y: y), proposal: .unspecified) - x += size.width + spacing - rowHeight = max(rowHeight, size.height) - } - } - } + /// 流式排布已提到主题层,这里保留原名,卡片标签行不用改调用点。 + typealias FlowLayout = AIFlowLayout struct FleetMetric: View { let label: String diff --git a/apps/ios/Sources/MultiSimAdmin/Features/Fleet/InstanceFormView.swift b/apps/ios/Sources/MultiSimAdmin/Features/Fleet/InstanceFormView.swift index 5619101..a10d805 100644 --- a/apps/ios/Sources/MultiSimAdmin/Features/Fleet/InstanceFormView.swift +++ b/apps/ios/Sources/MultiSimAdmin/Features/Fleet/InstanceFormView.swift @@ -73,6 +73,9 @@ struct InstanceFormView: View { .background(AITheme.canvas.ignoresSafeArea()) .navigationTitle(instance == nil ? "添加实例" : "编辑实例") #if os(iOS) + // 表单表统一用行内标题:大标题会在卡片上方留出一整条空白, + // 而「批量标签」等其它表已经是行内样式。 + .navigationBarTitleDisplayMode(.inline) .scrollDismissesKeyboard(.interactively) #endif } diff --git a/apps/ios/Sources/MultiSimAdmin/Features/Messages/MessagesView.swift b/apps/ios/Sources/MultiSimAdmin/Features/Messages/MessagesView.swift index cb117b0..6dcba54 100644 --- a/apps/ios/Sources/MultiSimAdmin/Features/Messages/MessagesView.swift +++ b/apps/ios/Sources/MultiSimAdmin/Features/Messages/MessagesView.swift @@ -5,7 +5,15 @@ struct MessagesView: View { @State private var pendingDeleteMessage: SMSMessage? init(model: MessagesViewModel? = nil) { - _model = StateObject(wrappedValue: model ?? DefaultContainer.makeMessagesModel()) + // StateObject 的 wrappedValue 是 autoclosure,在 init 里改它会写到另一个实例上, + // 所以调试开关必须先落到这个具体的 model 对象。 + let target = model ?? DefaultContainer.makeMessagesModel() + #if DEBUG + if DebugLaunch.has("--sheet-send") { + target.showingSendForm = true + } + #endif + _model = StateObject(wrappedValue: target) } var body: some View { @@ -147,6 +155,9 @@ struct MessagesView: View { .foregroundStyle(AITheme.ink) .lineSpacing(2) .fixedSize(horizontal: false, vertical: true) + // 删除按钮的点击区是 44pt,留在行内会把这一行撑到 44 高,正文与落款之间 + // 因此凭空多出一条约 22pt 的空带。这里让落款按自身高度排,点击区挂在 + // 行尾并垂直居中,多出的 12pt 落进卡片内边距,正文右缘留出等宽避让。 HStack(alignment: .center, spacing: 8) { Label(message.instanceName, systemImage: "antenna.radiowaves.left.and.right") .font(.caption) @@ -162,10 +173,15 @@ struct MessagesView: View { .foregroundStyle(delivery == "发送失败" ? AITheme.danger : AITheme.faint) .lineLimit(1) } - Spacer(minLength: 8) + } + .padding(.trailing, 44) + .frame(maxWidth: .infinity, alignment: .leading) + .overlay(alignment: .trailing) { AISymbolAction("删除这条消息", systemImage: "trash", tint: AITheme.danger, quiet: true) { pendingDeleteMessage = message } + // 图标在 44pt 点击区里居中,右移半个内边距让图标右缘与正文右缘对齐。 + .offset(x: 12) } } } diff --git a/apps/ios/Sources/MultiSimAdmin/Features/Messages/SendMessageFormView.swift b/apps/ios/Sources/MultiSimAdmin/Features/Messages/SendMessageFormView.swift index 83b2488..ef1b7eb 100644 --- a/apps/ios/Sources/MultiSimAdmin/Features/Messages/SendMessageFormView.swift +++ b/apps/ios/Sources/MultiSimAdmin/Features/Messages/SendMessageFormView.swift @@ -101,6 +101,8 @@ struct SendMessageFormView: View { .background(AITheme.canvas.ignoresSafeArea()) .navigationTitle("发送消息") #if os(iOS) + // 与「添加实例」一致:表行内标题,避免大标题在表单上方留出一整条空白。 + .navigationBarTitleDisplayMode(.inline) .scrollDismissesKeyboard(.interactively) #endif } diff --git a/apps/ios/Sources/MultiSimAdmin/Features/Settings/SettingsView.swift b/apps/ios/Sources/MultiSimAdmin/Features/Settings/SettingsView.swift index d2308b3..2998557 100644 --- a/apps/ios/Sources/MultiSimAdmin/Features/Settings/SettingsView.swift +++ b/apps/ios/Sources/MultiSimAdmin/Features/Settings/SettingsView.swift @@ -63,6 +63,8 @@ struct SettingsView: View { AISectionTitle("应用信息", systemImage: "info.circle") if let runtime = model.runtime { AIKeyValueRow("版本", value: runtime.marketingVersion) + // 构建号单独一行:同一营销版本会连续出多个包,只写版本号分不清装的是哪个。 + AIKeyValueRow("构建", value: runtime.buildVersion) AIKeyValueRow("系统", value: runtime.systemVersion) } else { Text("正在读取应用信息…") diff --git a/apps/ios/Sources/MultiSimAdmin/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon1024.png b/apps/ios/Sources/MultiSimAdmin/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon1024.png index 8e3e724..7474b93 100644 Binary files a/apps/ios/Sources/MultiSimAdmin/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon1024.png and b/apps/ios/Sources/MultiSimAdmin/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon1024.png differ diff --git a/apps/ios/Sources/MultiSimAdmin/Resources/Info.plist b/apps/ios/Sources/MultiSimAdmin/Resources/Info.plist index 6fb57fc..2265c80 100644 --- a/apps/ios/Sources/MultiSimAdmin/Resources/Info.plist +++ b/apps/ios/Sources/MultiSimAdmin/Resources/Info.plist @@ -10,7 +10,7 @@ CFBundleNameMultiSimAdmin CFBundlePackageTypeAPPL CFBundleShortVersionString0.2.2 - CFBundleVersion9 + CFBundleVersion10 LSRequiresIPhoneOS MinimumOSVersion16.0 NSAppTransportSecurity diff --git a/apps/ios/Sources/MultiSimAdmin/Theme/AICard.swift b/apps/ios/Sources/MultiSimAdmin/Theme/AICard.swift index d10d80b..2620f41 100644 --- a/apps/ios/Sources/MultiSimAdmin/Theme/AICard.swift +++ b/apps/ios/Sources/MultiSimAdmin/Theme/AICard.swift @@ -862,3 +862,63 @@ public struct AISymbolAction: View { .accessibilityLabel(title) } } + +/// 按固有尺寸换行排布的流式布局:标签胶囊这类「宽度取决于文字」的控件用它。 +/// 栅格会把每枚胶囊拉到同一列宽,短标签右侧就空出一片,读起来像没加载完。 +public struct AIFlowLayout: Layout { + public let spacing: CGFloat + + public init(spacing: CGFloat = 8) { + self.spacing = spacing + } + + public func sizeThatFits( + proposal: ProposedViewSize, + subviews: Subviews, + cache: inout () + ) -> CGSize { + let maxWidth = proposal.width ?? .infinity + var x: CGFloat = 0 + var y: CGFloat = 0 + var rowHeight: CGFloat = 0 + var widestRow: CGFloat = 0 + + for subview in subviews { + let size = subview.sizeThatFits(.unspecified) + if x > 0, x + size.width > maxWidth { + widestRow = max(widestRow, x - spacing) + y += rowHeight + spacing + x = 0 + rowHeight = 0 + } + x += size.width + spacing + rowHeight = max(rowHeight, size.height) + } + widestRow = max(widestRow, x - spacing) + + return CGSize(width: max(0, widestRow), height: max(0, y + rowHeight)) + } + + public func placeSubviews( + in bounds: CGRect, + proposal: ProposedViewSize, + subviews: Subviews, + cache: inout () + ) { + var x = bounds.minX + var y = bounds.minY + var rowHeight: CGFloat = 0 + + for subview in subviews { + let size = subview.sizeThatFits(.unspecified) + if x > bounds.minX, x + size.width > bounds.maxX { + x = bounds.minX + y += rowHeight + spacing + rowHeight = 0 + } + subview.place(at: CGPoint(x: x, y: y), proposal: .unspecified) + x += size.width + spacing + rowHeight = max(rowHeight, size.height) + } + } +} diff --git a/apps/ios/Tests/MultiSimAdminTests/AppMetadataTests.swift b/apps/ios/Tests/MultiSimAdminTests/AppMetadataTests.swift index 4784460..6a5081f 100644 --- a/apps/ios/Tests/MultiSimAdminTests/AppMetadataTests.swift +++ b/apps/ios/Tests/MultiSimAdminTests/AppMetadataTests.swift @@ -7,6 +7,6 @@ final class AppMetadataTests: XCTestCase { XCTAssertEqual(AppMetadata.name, "MultiSimAdmin") XCTAssertEqual(AppMetadata.bundleIdentifier, "fun.chick.multi-simadmin") XCTAssertEqual(AppMetadata.marketingVersion, "0.2.2") - XCTAssertEqual(AppMetadata.buildVersion, "9") + XCTAssertEqual(AppMetadata.buildVersion, "10") } } diff --git a/apps/ios/Tests/MultiSimAdminTests/SettingsViewModelTests.swift b/apps/ios/Tests/MultiSimAdminTests/SettingsViewModelTests.swift index c401ebc..b0af111 100644 --- a/apps/ios/Tests/MultiSimAdminTests/SettingsViewModelTests.swift +++ b/apps/ios/Tests/MultiSimAdminTests/SettingsViewModelTests.swift @@ -12,7 +12,7 @@ final class SettingsViewModelTests: XCTestCase { XCTAssertEqual(model.runtime?.appName, "MultiSimAdmin") XCTAssertEqual(model.runtime?.marketingVersion, "0.2.2") - XCTAssertEqual(model.runtime?.buildVersion, "9") + XCTAssertEqual(model.runtime?.buildVersion, "10") XCTAssertEqual(model.runtime?.platform, "iOS") XCTAssertTrue(model.runtime?.systemVersion.hasPrefix("iOS") ?? false) diff --git a/docs/ios-native-0.2.2.md b/docs/ios-native-0.2.2.md index 01d1aff..a37edad 100644 --- a/docs/ios-native-0.2.2.md +++ b/docs/ios-native-0.2.2.md @@ -2,7 +2,9 @@ 0.2.2 把「分组」这个实体从应用里彻底删掉,归类只靠标签;同时完成第二轮配色与图标重构,界面上 不再出现蓝色。构建 9 是一轮收尾打磨:修掉消息中心发不出去信的按钮遮挡,补齐按钮层级,统一缺数据 -读数的写法,并把中性灰阶压成严格等色。页面能力、上游白名单与本地数据布局见现行说明 +读数的写法,并把中性灰阶压成严格等色。构建 10 继续收细节:消息卡片右侧的空洞、批量标签胶囊的 +假宽度、设置页读不出构建号、两张表单表的标题层级,以及图标石墨底的冷调。页面能力、上游白名单与 +本地数据布局见现行说明 [docs/ios-native.md](ios-native.md), 本文只记录本次发布的变化。 @@ -63,6 +65,24 @@ 失败,现按中性标签原样显示,只把「新建标签并贴上」的输入与按钮置灰并说明原因。 - 版本号显示去掉括号:设置页「应用信息」只显示 `0.2.2`。 +## 构建 10 + +- 消息卡片右侧的空洞修掉:删除按钮此前和摘要挤在同一行 `HStack` 里,它自带 44pt 点击区,等于把 + 这一行撑高约 22pt,卡片下半截全是空白,一屏只能放三张。现在摘要行按正文列宽排版,删除按钮用 + `overlay` 悬在右侧并溢出到卡片留白里,点击区仍是 44pt,一屏回到四张。 +- 批量标签的胶囊改成贴合文字宽度:原先用 `LazyVGrid` 等宽栅格,短标签被拉到 112pt 一格,看起来 + 像整片禁用的灰块。现在换成流式布局(`AIFlowLayout`),胶囊只占文字加内边距,节点页的标签条 + 复用同一份实现,删掉了原来那份重复的布局代码。 +- 设置页显示构建号:同一营销版本会连续出多个包,只写 `0.2.2` 分不清设备上装的是哪个。应用信息 + 现在是「版本 0.2.2 / 构建 10 / 系统 iOS 26.3.1」三行,全程不带括号。 +- 两张表单表统一行内标题:「添加实例」和「发送消息」用大标题,标题下方留出一整条空白,而同类的 + 「批量标签」已经是行内样式。现在三张表一致。 +- 节点卡片的空态文案不再自相矛盾:卡片下方的量表条确实会返回运行时数据,空态却写「节点未返回 + 运行信息」。改为「节点未返回设备信息」,与实际缺少的字段一致。 +- 图标底色压暖:石墨底原先是数学意义上的中性灰(R=G=B),在 iOS 浅色壁纸旁仍会被对比成冷调, + 这是用户反馈「图标是蓝色」的直接来源(旧包 0.2.1 的图标确实有 72% 像素落在蓝色波段)。现在 + 底色、面板阴影与插槽统一带 3~4 级暖偏移,取色为 `rgb(47,45,42)`,全图 0 蓝色像素。 + ## 文档同步 - 新增本文,[docs/ios-native.md](ios-native.md) 同步到 0.2.2:导航表去掉分组视图,节点总览与 @@ -90,10 +110,10 @@ - `build/ipa/MultiSimAdmin.ipa` - `build/ipa/MultiSimAdmin.ipa.sha256` -0.2.2 构建 9 校验值: +0.2.2 构建 10 校验值: ```text -a0b90a3754bd5cb8d9b95906eb711b7d140d603b6f821ee838edb273e62e3d75 MultiSimAdmin.ipa +5018b9b4a1b236892b539746994176e1421a2cbe6ec24fdaac540d9bcbcf5fdd MultiSimAdmin.ipa ``` IPA 目标为 iOS 16.0 及以上,`arm64`,Bundle ID 为 `fun.chick.multi-simadmin`,未签名, diff --git a/docs/ios-native.md b/docs/ios-native.md index a5da656..f6a5fd5 100644 --- a/docs/ios-native.md +++ b/docs/ios-native.md @@ -3,7 +3,7 @@ 本文是 iPhone 应用的唯一现行说明,内容与仓库代码一致。历史设计稿和执行计划保留在 `docs/superpowers/` 下,其中已被实现推翻的决策在本文第 9 节逐条列出。 -- 当前版本:`0.2.2`,构建号 9 +- 当前版本:`0.2.2`,构建号 10 - 承载分支:`ios-native-ipa` - 运行要求:iOS 16.0 及以上,`arm64`,Bundle ID `fun.chick.multi-simadmin` - 产物:未签名 `MultiSimAdmin.ipa`,由 TrollStore 或用户自有签名流程安装 @@ -198,7 +198,7 @@ xcodebuild -scheme MultiSimAdmin \ ## 11. 文档索引 - [docs/ios-native.md](ios-native.md):本文,现行说明 -- [docs/ios-native-0.2.2.md](ios-native-0.2.2.md):0.2.2 构建 9 发布说明与校验值 +- [docs/ios-native-0.2.2.md](ios-native-0.2.2.md):0.2.2 构建 10 发布说明与校验值 - [docs/ios-native-0.2.1.md](ios-native-0.2.1.md):0.2.1 构建 6 发布说明与校验值 - [docs/ios-native-0.2.0.md](ios-native-0.2.0.md):0.2.0 发布说明 - [docs/superpowers/specs/2026-09-08-ios-native-ipa-design.md](superpowers/specs/2026-09-08-ios-native-ipa-design.md):历史设计稿