release(ios): 0.2.2 移除分组改用标签归类并完成石墨配色重构

- 删除分组实体:分组视图/编辑器/仓储与 groupId 全部移除,升级首启清理
  instance-groups.json 与 fleet.groupMode
- 新增批量标签表:三态覆盖度(未贴 / N/M / 全部),整批加减与新建标签
- 配色重构,全应用零蓝色:石墨墨色承担主操作与选中态,颜色只留给状态语义;
  浅色改中性冷灰,深色改低明度石墨三层
- 选中态改实心主色 + onPrimary,替换不可读的低透明度色雾(主题块、图例胶囊)
- 应用图标重做为扁平石墨底 + 黄铜 SIM 面板,参数化光栅化脚本与参考稿
- 运行概览:检测进度并入副标题,读数栅格固定八格两行并保留不可用占位
- 实例详情:仪表改两列发丝线栅格,缺数据留轨道,修掉头部高度回弹;
  补齐设备名称/存储总量/可用存储/使用率四个字段名
- 版本号显示去掉括号,设置页只显示 0.2.2
- 文档:新增 docs/ios-native-0.2.2.md,同步 ios-native.md 与两处 README
This commit is contained in:
chick
2026-09-15 02:23:24 +08:00
parent ec3ffb9db3
commit eb880e0d14
28 changed files with 757 additions and 1524 deletions
@@ -1,130 +0,0 @@
import Foundation
public struct InstanceGroup: Codable, Equatable, Identifiable, Sendable {
public var id: UUID
public var name: String
public var createdAt: Date
public init(id: UUID, name: String, createdAt: Date) {
self.id = id
self.name = name
self.createdAt = createdAt
}
}
public struct InstanceGroupInput: Sendable {
public var name: String
public init(name: String) {
self.name = name
}
}
public protocol InstanceGroupStore: Sendable {
func list() async throws -> [InstanceGroup]
func create(_ input: InstanceGroupInput) async throws -> InstanceGroup
func update(_ id: UUID, _ name: String) async throws -> InstanceGroup
func delete(_ id: UUID) async throws
}
public final class FileInstanceGroupStore: @unchecked Sendable, InstanceGroupStore {
private struct FileModel: Codable {
var groups: [InstanceGroup] = []
}
private let location: URL
private let encoder: JSONEncoder
private let decoder: JSONDecoder
private let lock = NSLock()
private var fileModel = FileModel()
private let queue = DispatchQueue(label: "fun.chick.multi-simadmin.instance-group-store")
public init(location: URL) {
self.location = URL(fileURLWithPath: location.path)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
self.decoder = decoder
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
self.encoder = encoder
lock.withLock {
if let data = try? Data(contentsOf: location),
let decoded = try? decoder.decode(FileModel.self, from: data) {
fileModel = decoded
}
}
}
public func list() async throws -> [InstanceGroup] {
lock.withLock {
fileModel.groups.sorted { $0.name.localizedCompare($1.name) == .orderedAscending }
}
}
public func create(_ input: InstanceGroupInput) async throws -> InstanceGroup {
try await run { model in
let name = try Self.normalized(input.name)
guard !model.groups.contains(where: { $0.name == name }) else {
throw RepositoryError.duplicateOrigin
}
let group = InstanceGroup(id: UUID(), name: name, createdAt: Date())
model.groups.append(group)
return group
}
}
public func update(_ id: UUID, _ name: String) async throws -> InstanceGroup {
try await run { model in
let normalized = try Self.normalized(name)
guard let index = model.groups.firstIndex(where: { $0.id == id }) else {
throw RepositoryError.notFound
}
guard !model.groups.contains(where: { $0.id != id && $0.name == normalized }) else {
throw RepositoryError.duplicateOrigin
}
model.groups[index].name = normalized
return model.groups[index]
}
}
public func delete(_ id: UUID) async throws {
try await run { model in
model.groups.removeAll { $0.id == id }
}
}
private func run<T: Sendable>(
_ transform: @escaping @Sendable (inout FileModel) throws -> T
) async throws -> T {
try await withCheckedThrowingContinuation { continuation in
queue.async {
do {
var model = self.lock.withLock { self.fileModel }
let output = try transform(&model)
try self.write(model)
self.lock.withLock { self.fileModel = model }
continuation.resume(returning: output)
} catch {
continuation.resume(throwing: error)
}
}
}
}
private func write(_ model: FileModel) throws {
let directory = location.deletingLastPathComponent()
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
try encoder.encode(model).write(to: location, options: [.atomic])
}
private static func normalized(_ value: String) throws -> String {
let name = value.trimmingCharacters(in: .whitespacesAndNewlines)
guard !name.isEmpty, name.count <= 100 else {
throw RepositoryError.invalidOrigin
}
return name
}
}
@@ -35,9 +35,6 @@ public struct FileInstanceRepository: InstanceRepository {
if let tags = query.tags, !tags.isEmpty {
instances = instances.filter { !Set($0.tags).isDisjoint(with: tags) }
}
if let groupId = query.groupId {
instances = instances.filter { $0.groupId == groupId }
}
return instances.sorted { $0.name.localizedCompare($1.name) == .orderedAscending }
}
}
@@ -61,7 +58,6 @@ public struct FileInstanceRepository: InstanceRepository {
name: input.name.trimmingCharacters(in: .whitespacesAndNewlines),
origin: origin,
tags: Self.normalizedTags(input.tags),
groupId: input.groupId,
credentialConfigured: !(input.password ?? "").isEmpty,
createdAt: now,
updatedAt: now
@@ -96,9 +92,6 @@ public struct FileInstanceRepository: InstanceRepository {
if let tags = patch.tags {
instance.tags = Self.normalizedTags(tags)
}
if patch.groupId != nil || instance.groupId != nil {
instance.groupId = patch.groupId
}
if let password = patch.password {
instance.credentialConfigured = !password.isEmpty
}