diff --git a/README.md b/README.md index d570a7a..d480978 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## iOS Native 应用 -`apps/ios` 提供不依赖 8788 控制台的原生 iPhone 应用,当前版本 **0.2.2**,构建号 7,运行在 iOS 16 及以上,产物为未签名 IPA,供 TrollStore 或自有签名流程安装。iOS 开发在 `ios-native-ipa` 分支进行。 +`apps/ios` 提供不依赖 8788 控制台的原生 iPhone 应用,当前版本 **0.2.2**,构建号 8,运行在 iOS 16 及以上,产物为未签名 IPA,供 TrollStore 或自有签名流程安装。iOS 开发在 `ios-native-ipa` 分支进行。 底部为节点、消息、设置三个入口: diff --git a/apps/ios/Design/AppIcon.svg b/apps/ios/Design/AppIcon.svg index ed183fc..4eeb7b5 100644 --- a/apps/ios/Design/AppIcon.svg +++ b/apps/ios/Design/AppIcon.svg @@ -2,18 +2,18 @@ - - - + + + - - - + + + - - + + diff --git a/apps/ios/Design/generate_app_icon.swift b/apps/ios/Design/generate_app_icon.swift index f72274f..6d4bdb3 100644 --- a/apps/ios/Design/generate_app_icon.swift +++ b/apps/ios/Design/generate_app_icon.swift @@ -1,10 +1,14 @@ // Renders the 1024x1024 MultiSimAdmin app icon with CoreGraphics only. // Usage: swift Design/generate_app_icon.swift // -// The mark is the web favicon grown up: a flat brass SIM plate with three -// ascending slots cut out of it, on a graphite field. Two colours, one 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 mark is the web favicon grown up: a flat gold SIM plate with three +// ascending slots cut out of it, on a neutral graphite field. Two colours, one +// 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. // // Every coordinate is in visual space (origin top-left, y grows down). import AppKit @@ -129,16 +133,16 @@ func directionalRim(_ path: CGPath, _ rect: CGRect, _ width: CGFloat, _ stops: [ fillLinear( rectPath(CGRect(x: 0, y: 0, width: side, height: side)), [ - (0.00, (26, 31, 30, 255)), - (0.55, (17, 21, 20, 255)), - (1.00, (11, 14, 13, 255)), + (0.00, (32, 32, 33, 255)), + (0.55, (19, 19, 20, 255)), + (1.00, (10, 10, 11, 255)), ] as [Stop], CGPoint(x: side * 0.06, y: 0), CGPoint(x: side * 0.94, y: side) ) fillRadial( CGPoint(x: side * 0.22, y: -side * 0.10), CGFloat(side * 0.86), - [(0.0, (214, 226, 218, 26)), (0.60, (214, 226, 218, 7)), (1.0, (214, 226, 218, 0))] as [Stop] + [(0.0, (232, 226, 214, 24)), (0.60, (232, 226, 214, 6)), (1.0, (232, 226, 214, 0))] as [Stop] ) // MARK: - Plate @@ -165,9 +169,9 @@ ctx.restoreGState() fillLinear( plate, [ - (0.00, (248, 213, 124, 255)), - (0.50, (236, 189, 86, 255)), - (1.00, (212, 158, 54, 255)), + (0.00, (250, 221, 152, 255)), + (0.46, (238, 191, 100, 255)), + (1.00, (199, 141, 47, 255)), ] as [Stop], CGPoint(x: plateRect.minX, y: plateRect.minY), CGPoint(x: plateRect.maxX, y: plateRect.maxY) @@ -175,9 +179,9 @@ fillLinear( directionalRim( plate, plateRect, plateSide * 0.007, [ - (0.00, (255, 244, 214, 132)), - (0.36, (255, 238, 198, 30)), - (0.68, (255, 234, 186, 0)), + (0.00, (255, 246, 224, 140)), + (0.36, (255, 240, 206, 32)), + (0.68, (255, 236, 196, 0)), ] as [Stop] ) @@ -203,8 +207,8 @@ for (index, height) in slotHeights.enumerated() { fillLinear( path, [ - (0.00, (15, 19, 18, 255)), - (1.00, (29, 35, 33, 255)), + (0.00, (16, 16, 17, 255)), + (1.00, (32, 32, 33, 255)), ] as [Stop], CGPoint(x: rect.minX, y: rect.minY), CGPoint(x: rect.minX, y: rect.maxY) @@ -213,9 +217,9 @@ for (index, height) in slotHeights.enumerated() { directionalRim( path, rect, radius * 0.30, [ - (0.00, (120, 84, 18, 96)), - (0.52, (120, 84, 18, 18)), - (1.00, (255, 232, 176, 58)), + (0.00, (112, 74, 16, 100)), + (0.52, (112, 74, 16, 18)), + (1.00, (255, 236, 190, 60)), ] as [Stop] ) } diff --git a/apps/ios/Sources/MultiSimAdmin/AppMetadata.swift b/apps/ios/Sources/MultiSimAdmin/AppMetadata.swift index 1f5dc2e..3d2fbde 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 = "7" + public static let buildVersion = "8" } diff --git a/apps/ios/Sources/MultiSimAdmin/Features/Fleet/FleetTagBatchSheet.swift b/apps/ios/Sources/MultiSimAdmin/Features/Fleet/FleetTagBatchSheet.swift index efba790..0fc74ae 100644 --- a/apps/ios/Sources/MultiSimAdmin/Features/Fleet/FleetTagBatchSheet.swift +++ b/apps/ios/Sources/MultiSimAdmin/Features/Fleet/FleetTagBatchSheet.swift @@ -102,10 +102,16 @@ struct FleetTagBatchSheet: View { .accessibilityLabel("新标签名称") .submitLabel(.done) .onSubmit { submitDraft() } - .disabled(isBusy) + .disabled(isBusy || targets.isEmpty) Button(isBusy ? "处理中" : "添加") { submitDraft() } .buttonStyle(AIButtonStyle(kind: .primary, size: .regular, fixedWidth: 84)) - .disabled(draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || isBusy) + .disabled(draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || isBusy || targets.isEmpty) + } + // 没有勾选节点时这个表单按下去只会清空输入,先说清楚为什么不能动。 + if targets.isEmpty { + Text("勾选节点后即可新建标签并批量贴上。") + .font(.caption2) + .foregroundStyle(AITheme.faint) } } } @@ -135,44 +141,87 @@ struct FleetTagBatchSheet: View { private var isComplete: Bool { total > 0 && covered == total } private var isPartial: Bool { covered > 0 && covered < total } + // 没有勾选任何节点时这一整片都是不可操作状态,此时不该用「未贴」这种带暗示的 + // 灰字,直接按中性标签渲染,避免看起来像加载失败的禁用控件。 + private var isInert: Bool { total == 0 } private var stateText: String { + if isInert { return "" } if isComplete { return "全部" } if isPartial { return "\(covered)/\(total)" } return "未贴" } var body: some View { - Button(action: action) { - HStack(spacing: 6) { - Image(systemName: isComplete ? "checkmark.circle.fill" : (isPartial ? "circle.lefthalf.filled" : "circle")) - .font(.system(size: 12, weight: .semibold)) - .accessibilityHidden(true) - Text(title) - .font(.footnote.weight(isComplete || isPartial ? .semibold : .medium)) - .lineLimit(1) - .truncationMode(.middle) + if isInert { + // 不可操作时不渲染 Button:系统禁用态会把整枚胶囊压成灰色, + // 看起来像加载失败,而标签本身是需要读清楚的内容。 + label + .accessibilityLabel("标签 \(title),勾选节点后可贴上") + } else { + Button(action: action) { label } + .buttonStyle(.plain) + .disabled(isBusy) + .accessibilityLabel(isComplete ? "从已选节点移除标签 \(title)" : "给已选节点贴上标签 \(title)") + .accessibilityAddTraits(isComplete ? [.isButton, .isSelected] : .isButton) + } + } + + private var label: some View { + HStack(spacing: 6) { + Image(systemName: iconName) + .font(.system(size: 12, weight: .semibold)) + .accessibilityHidden(true) + Text(title) + .font(.footnote.weight(isComplete || isPartial ? .semibold : .medium)) + .lineLimit(1) + .truncationMode(.middle) + if stateText.isEmpty == false { Text(stateText) .font(.caption2.weight(.semibold)) .monospacedDigit() .opacity(0.72) - Spacer(minLength: 0) } - .padding(.horizontal, 10) - .frame(height: 38) - .foregroundStyle(isComplete ? AITheme.onPrimary : (isPartial ? AITheme.ink : AITheme.muted)) - .background(isComplete ? AITheme.primary : (isPartial ? AITheme.tile : AITheme.surface)) - .clipShape(Capsule(style: .continuous)) - .overlay { - Capsule(style: .continuous) - .strokeBorder(isComplete ? AITheme.primary : (isPartial ? AITheme.primary.opacity(0.42) : AITheme.line)) - } - .contentShape(Capsule(style: .continuous)) + Spacer(minLength: 0) } - .buttonStyle(.plain) - .disabled(isBusy || total == 0) - .accessibilityLabel(isComplete ? "从已选节点移除标签 \(title)" : "给已选节点贴上标签 \(title)") - .accessibilityAddTraits(isComplete ? [.isButton, .isSelected] : .isButton) + .padding(.horizontal, 10) + .frame(height: 38) + .foregroundStyle(foreground) + .background(background) + .clipShape(Capsule(style: .continuous)) + .overlay { + Capsule(style: .continuous) + .strokeBorder(border) + } + .contentShape(Capsule(style: .continuous)) + } + + private var iconName: String { + if isInert { return "tag" } + if isComplete { return "checkmark.circle.fill" } + if isPartial { return "circle.lefthalf.filled" } + return "circle" + } + + private var foreground: Color { + if isInert { return AITheme.ink } + if isComplete { return AITheme.onPrimary } + if isPartial { return AITheme.ink } + return AITheme.muted + } + + private var background: Color { + if isInert { return AITheme.tile } + if isComplete { return AITheme.primary } + if isPartial { return AITheme.tile } + return AITheme.surface + } + + private var border: Color { + if isInert { return AITheme.tileLine } + if isComplete { return AITheme.primary } + if isPartial { return AITheme.primary.opacity(0.42) } + return AITheme.line } } } 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 fe8f355..d3b8bff 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 c434eeb..1444831 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 - CFBundleVersion7 + CFBundleVersion8 LSRequiresIPhoneOS MinimumOSVersion16.0 NSAppTransportSecurity diff --git a/apps/ios/Tests/MultiSimAdminTests/AppMetadataTests.swift b/apps/ios/Tests/MultiSimAdminTests/AppMetadataTests.swift index f384891..4f970e6 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, "7") + XCTAssertEqual(AppMetadata.buildVersion, "8") } } diff --git a/apps/ios/Tests/MultiSimAdminTests/SettingsViewModelTests.swift b/apps/ios/Tests/MultiSimAdminTests/SettingsViewModelTests.swift index 6c929e6..655ab61 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, "7") + XCTAssertEqual(model.runtime?.buildVersion, "8") 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 85739d9..1f1c958 100644 --- a/docs/ios-native-0.2.2.md +++ b/docs/ios-native-0.2.2.md @@ -1,6 +1,6 @@ # iOS Native 0.2.2 -0.2.2 构建 7 把「分组」这个实体从应用里彻底删掉,归类只靠标签;同时完成第二轮配色与图标重构, +0.2.2 构建 8 把「分组」这个实体从应用里彻底删掉,归类只靠标签;同时完成第二轮配色与图标重构, 界面上不再出现蓝色。页面能力、上游白名单与本地数据布局见现行说明 [docs/ios-native.md](ios-native.md), 本文只记录本次发布的变化。 @@ -18,6 +18,8 @@ 改为实心主色加 `onPrimary` 文字,两套主题都一眼可读,且选中不改变控件尺寸。 - 应用图标重做:放弃立体卡与投影,改扁平石墨底加黄铜 SIM 面板,三根底部对齐的递增信号条, 29pt 下轮廓仍可读;面板边长、圆角、切角与信号条间距可用环境变量调参后重新光栅化。 + 底色与插槽改为严格等值的 R=G=B 中性石墨(原先偏青绿,在亮色壁纸上会被读成蓝),金色 + 下压饱和度、提高明度跨度,从浅金到深古铜,去掉「芥末黄」的塑料感。 - 运行概览收紧:检测进度从读数格移进卡片副标题(`刚刚更新 · 已检测 6/6 台`);读数栅格固定 八格两行,温度与电量即使整群节点都没上报也保留「不可用」占位,栅格不再在七格与九格之间跳; 概览量表健康时用墨色,只在告警与危险时上色。 @@ -32,6 +34,8 @@ 卡片同一形状同一图标;消息中心的「收件 / 发件」由绿色改回中性胶囊配上下箭头,绿色重新只 表示健康状态。 - 批量标签表在没有任何勾选时给出「先在节点列表勾选要归类的节点」提示,不再留一行空白。 + 同时这段状态下标签胶囊不再渲染成 `Button`:系统禁用态会把整枚胶囊压成灰块,看起来像加载 + 失败,现按中性标签原样显示,只把「新建标签并贴上」的输入与按钮置灰并说明原因。 - 版本号显示去掉括号:设置页「应用信息」只显示 `0.2.2`。 ## 文档同步 @@ -61,10 +65,10 @@ - `build/ipa/MultiSimAdmin.ipa` - `build/ipa/MultiSimAdmin.ipa.sha256` -0.2.2 构建 7 校验值: +0.2.2 构建 8 校验值: ```text -0e4ffb74d03c2da5648e7bc039f67b14798e6d3aec723c206bce17da5b8f06fb MultiSimAdmin.ipa +a40e8e308f667edfe69902d5665a9ad85bdb52f72b5a248a671b6eef3591d847 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 07677cd..bbc5a8c 100644 --- a/docs/ios-native.md +++ b/docs/ios-native.md @@ -3,7 +3,7 @@ 本文是 iPhone 应用的唯一现行说明,内容与仓库代码一致。历史设计稿和执行计划保留在 `docs/superpowers/` 下,其中已被实现推翻的决策在本文第 9 节逐条列出。 -- 当前版本:`0.2.2`,构建号 7 +- 当前版本:`0.2.2`,构建号 8 - 承载分支:`ios-native-ipa` - 运行要求:iOS 16.0 及以上,`arm64`,Bundle ID `fun.chick.multi-simadmin` - 产物:未签名 `MultiSimAdmin.ipa`,由 TrollStore 或用户自有签名流程安装 @@ -197,7 +197,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 构建 7 发布说明与校验值 +- [docs/ios-native-0.2.2.md](ios-native-0.2.2.md):0.2.2 构建 8 发布说明与校验值 - [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):历史设计稿