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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user