Files
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

110 lines
4.2 KiB
Swift

import Foundation
/// Persisted reference to one app inside a custom layout.
///
/// Identity is the resolved absolute path (`LaunchpadAppItem.id`) — the same key used by
/// `LaunchpadPreferences.hiddenAppIDs` and the icon cache, so hide / order / icon all agree
/// on one identity (design §2). `bundleID` is a reserved, currently-unused field kept only
/// so a future migration could add a secondary match key without a format break; v1 always
/// encodes `nil` and never reads it for matching.
struct LaunchpadAppRef: Codable, Hashable {
var id: String
var bundleID: String?
var name: String
init(id: String, bundleID: String? = nil, name: String) {
self.id = id
self.bundleID = bundleID
self.name = name
}
}
/// One node in the (single-level) custom layout tree.
///
/// `folder.children` holds `LaunchpadAppRef` (not nodes), so "folder inside folder" is
/// unrepresentable at the type level — the same two-level constraint as macOS Launchpad.
/// 19a only ever produces `.app` nodes; the `.folder` case and its Codable exist now so the
/// persistence format is fixed once and 19b needs no second migration (design §3).
enum LaunchpadLayoutNode: Codable, Hashable {
case app(LaunchpadAppRef)
case folder(id: String, name: String, children: [LaunchpadAppRef])
/// Root-level identity used for ordering and lookups: an `.app`'s path id, or a
/// `.folder`'s UUID. App ids always start with `/`, folder ids are UUIDs, so the two
/// namespaces never collide.
var rootID: String {
switch self {
case .app(let ref): return ref.id
case .folder(let id, _, _): return id
}
}
/// Every app id this node references (the app id, or a folder's children ids). Used to
/// tell which visible apps are already captured in the layout.
var appIDs: [String] {
switch self {
case .app(let ref): return [ref.id]
case .folder(_, _, let children): return children.map(\.id)
}
}
// Hand-written, kind-discriminated coding (not Swift's automatic enum encoding) so new
// fields or kinds can be added compatibly later.
private enum Kind: String, Codable {
case app
case folder
}
private enum CodingKeys: String, CodingKey {
case kind
case app // .app payload
case id, name, children // .folder payload
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
switch try container.decode(Kind.self, forKey: .kind) {
case .app:
self = .app(try container.decode(LaunchpadAppRef.self, forKey: .app))
case .folder:
self = .folder(
id: try container.decode(String.self, forKey: .id),
name: try container.decode(String.self, forKey: .name),
children: try container.decode([LaunchpadAppRef].self, forKey: .children)
)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .app(let ref):
try container.encode(Kind.app, forKey: .kind)
try container.encode(ref, forKey: .app)
case .folder(let id, let name, let children):
try container.encode(Kind.folder, forKey: .kind)
try container.encode(id, forKey: .id)
try container.encode(name, forKey: .name)
try container.encode(children, forKey: .children)
}
}
}
/// The user's custom ordering / grouping.
///
/// Its mere existence ("layout present") *is* the "custom sort" flag — there is no separate
/// `isCustomSorted` bool to fall out of sync (design §3). Absent (`nil`) == alphabetical.
/// `version` starts at 2 — version 1 is reserved for a hypothetical plain-`[String]` format
/// that never shipped, so any stored `version < currentVersion` is treated as unreadable.
struct LaunchpadLayout: Codable, Equatable {
static let currentVersion = 2
var version: Int
var nodes: [LaunchpadLayoutNode]
init(version: Int = LaunchpadLayout.currentVersion, nodes: [LaunchpadLayoutNode]) {
self.version = version
self.nodes = nodes
}
}