mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Update extensions
This commit is contained in:
parent
bfb36cb9d1
commit
db38661da8
3 changed files with 150 additions and 79 deletions
|
|
@ -7,7 +7,7 @@
|
||||||
"location" : "https://github.com/Ebullioscopic/AtollExtensionKit",
|
"location" : "https://github.com/Ebullioscopic/AtollExtensionKit",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch" : "main",
|
"branch" : "main",
|
||||||
"revision" : "ebe35277572a349fad26abeb6fc552cb5251d584"
|
"revision" : "e2d30afedbbcc259bec7eeb1bea0757e52f4ca01"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1661,10 +1661,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"±10 Seconds" : {
|
|
||||||
"comment" : "Displayed as the name of the music skip behavior option.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"0" : {
|
"0" : {
|
||||||
"comment" : "A label displayed in a circular gauge to indicate the minimum battery level.",
|
"comment" : "A label displayed in a circular gauge to indicate the minimum battery level.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
|
|
@ -2017,10 +2013,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Accent" : {
|
|
||||||
"comment" : "Display name for the \"Accent\" caps lock indicator tint mode.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Accent color" : {
|
"Accent color" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
@ -2366,10 +2358,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Adaptive gradient" : {
|
|
||||||
"comment" : "Name of the timer icon color mode option that uses an adaptive gradient.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Add" : {
|
"Add" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
@ -3989,10 +3977,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Apple Music" : {
|
|
||||||
"comment" : "Names for the different media player types.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Applies the notch-style parallax effect to the lock screen media widget album art." : {
|
"Applies the notch-style parallax effect to the lock screen media widget album art." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
|
|
@ -4437,10 +4421,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Bar" : {
|
|
||||||
"comment" : "Localized name for the \"Bar\" timer progress style.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Battery" : {
|
"Battery" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
@ -8816,10 +8796,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Digital" : {
|
|
||||||
"comment" : "The display name for the \"Digital\" reminder presentation style.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Dimensions" : {
|
"Dimensions" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
|
|
@ -12367,10 +12343,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Green" : {
|
|
||||||
"comment" : "Display name for the \"Green\" Caps Lock Indicator Tint Mode.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Height:" : {
|
"Height:" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -16134,6 +16106,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Minutes" : {
|
"Minutes" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -18365,10 +18338,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Now Playing" : {
|
|
||||||
"comment" : "Localized name for each media controller type.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Number of lines kept in the scrollback buffer. Higher values use more memory." : {
|
"Number of lines kept in the scrollback buffer. Higher values use more memory." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
@ -18771,10 +18740,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Panel" : {
|
|
||||||
"comment" : "Name for the \"Panel\" clipboard display mode.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Panel mode shows clipboard in a floating window near the notch." : {
|
"Panel mode shows clipboard in a floating window near the notch." : {
|
||||||
"comment" : "A description of the \"Panel\" clipboard display mode.",
|
"comment" : "A description of the \"Panel\" clipboard display mode.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
|
|
@ -20480,10 +20445,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Ring" : {
|
|
||||||
"comment" : "Localized name for the \"Ring\" timer progress style.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Rotation:" : {
|
"Rotation:" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -21328,10 +21289,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Separate Tab" : {
|
|
||||||
"comment" : "Name of the clipboard display mode option that opens the clipboard in a separate tab.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Separate Tab mode integrates Copied Items and Notes into a single view. If both are enabled, Notes appear on the right and Clipboard on the left." : {
|
"Separate Tab mode integrates Copied Items and Notes into a single view. If both are enabled, Notes appear on the right and Clipboard on the left." : {
|
||||||
"comment" : "A description of the \"Separate Tab\" clipboard display mode.",
|
"comment" : "A description of the \"Separate Tab\" clipboard display mode.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
|
|
@ -22423,14 +22380,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Shows screen assistant as a dropdown attached to the AI button" : {
|
|
||||||
"comment" : "Description of the \"popover\" display mode for the screen assistant.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Shows screen assistant in a floating panel near the notch" : {
|
|
||||||
"comment" : "Description of the \"Panel\" screen assistant display mode.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Shows the model's reasoning process before the final answer" : {
|
"Shows the model's reasoning process before the final answer" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
|
|
@ -22505,10 +22454,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Skip forward or backward by ten seconds" : {
|
|
||||||
"comment" : "Description of the \"Skip forward or backward by ten seconds\" music skip behavior option.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Slider color" : {
|
"Slider color" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
@ -23077,10 +23022,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Spotify" : {
|
|
||||||
"comment" : "Name of a media controller type for Spotify.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Square" : {
|
"Square" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
@ -23193,10 +23134,6 @@
|
||||||
},
|
},
|
||||||
"Standard notch media controls are hidden. Re-enable the toggle above to restore them." : {
|
"Standard notch media controls are hidden. Re-enable the toggle above to restore them." : {
|
||||||
|
|
||||||
},
|
|
||||||
"Standard previous/next track controls" : {
|
|
||||||
"comment" : "Description of a music skip behavior option that uses standard previous/next track controls.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
},
|
||||||
"Start" : {
|
"Start" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -24608,10 +24545,6 @@
|
||||||
},
|
},
|
||||||
"shouldTranslate" : false
|
"shouldTranslate" : false
|
||||||
},
|
},
|
||||||
"Track Skip" : {
|
|
||||||
"comment" : "Name of the music skip behavior option that skips to the next track.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Transform" : {
|
"Transform" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -26616,10 +26549,6 @@
|
||||||
"comment" : "A footer note explaining the app's funding.",
|
"comment" : "A footer note explaining the app's funding.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Youtube Music" : {
|
|
||||||
"comment" : "Name for the \"Youtube Music\" media controller type in settings.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"YouTube Music requires this third-party app to be installed: " : {
|
"YouTube Music requires this third-party app to be installed: " : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,18 @@ final class ExtensionRPCService {
|
||||||
private let decoder = JSONDecoder()
|
private let decoder = JSONDecoder()
|
||||||
private let encoder = JSONEncoder()
|
private let encoder = JSONEncoder()
|
||||||
|
|
||||||
|
// Keys whose values represent Swift enums that use {"type":...} in client format.
|
||||||
|
// These need to be transformed to Swift Codable format: {"caseName": {params}}.
|
||||||
|
private static let enumKeys: Set<String> = [
|
||||||
|
"leadingIcon", "trailingContent", "progressIndicator",
|
||||||
|
"badgeIcon", "leadingContent", "icon", "tint"
|
||||||
|
]
|
||||||
|
|
||||||
|
// Enum-typed fields that can appear inside arrays (like content elements)
|
||||||
|
private static let contentElementTypeFields: Set<String> = [
|
||||||
|
"text", "icon", "progress", "graph", "gauge", "spacer", "divider", "webView"
|
||||||
|
]
|
||||||
|
|
||||||
init(bundleIdentifier: String, server: ExtensionRPCServer) {
|
init(bundleIdentifier: String, server: ExtensionRPCServer) {
|
||||||
self.bundleIdentifier = bundleIdentifier
|
self.bundleIdentifier = bundleIdentifier
|
||||||
self.server = server
|
self.server = server
|
||||||
|
|
@ -146,7 +158,8 @@ final class ExtensionRPCService {
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let descriptor = try decoder.decode(AtollLiveActivityDescriptor.self, from: descriptorData)
|
let transformed = Self.transformClientJSON(descriptorData)
|
||||||
|
let descriptor = try decoder.decode(AtollLiveActivityDescriptor.self, from: transformed)
|
||||||
try ExtensionDescriptorValidator.validate(descriptor)
|
try ExtensionDescriptorValidator.validate(descriptor)
|
||||||
try liveActivityManager.present(descriptor: descriptor, bundleIdentifier: descriptor.bundleIdentifier)
|
try liveActivityManager.present(descriptor: descriptor, bundleIdentifier: descriptor.bundleIdentifier)
|
||||||
logDiagnostics("RPC: Presented live activity \(descriptor.id) for \(descriptor.bundleIdentifier)")
|
logDiagnostics("RPC: Presented live activity \(descriptor.id) for \(descriptor.bundleIdentifier)")
|
||||||
|
|
@ -154,6 +167,8 @@ final class ExtensionRPCService {
|
||||||
} catch let error as ExtensionValidationError {
|
} catch let error as ExtensionValidationError {
|
||||||
return errorResponse(from: error, id: id)
|
return errorResponse(from: error, id: id)
|
||||||
} catch {
|
} catch {
|
||||||
|
logDiagnostics("RPC: Decode error for presentLiveActivity: \(error)")
|
||||||
|
logDiagnostics("RPC: Raw payload: \(String(data: descriptorData, encoding: .utf8) ?? "<binary>")")
|
||||||
return errorResponse(code: RPCErrorCode.internalError, message: error.localizedDescription, id: id)
|
return errorResponse(code: RPCErrorCode.internalError, message: error.localizedDescription, id: id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -164,13 +179,15 @@ final class ExtensionRPCService {
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let descriptor = try decoder.decode(AtollLiveActivityDescriptor.self, from: descriptorData)
|
let transformed = Self.transformClientJSON(descriptorData)
|
||||||
|
let descriptor = try decoder.decode(AtollLiveActivityDescriptor.self, from: transformed)
|
||||||
try liveActivityManager.update(descriptor: descriptor, bundleIdentifier: descriptor.bundleIdentifier)
|
try liveActivityManager.update(descriptor: descriptor, bundleIdentifier: descriptor.bundleIdentifier)
|
||||||
logDiagnostics("RPC: Updated live activity \(descriptor.id) for \(descriptor.bundleIdentifier)")
|
logDiagnostics("RPC: Updated live activity \(descriptor.id) for \(descriptor.bundleIdentifier)")
|
||||||
return RPCSuccessResponse(result: ["success": .bool(true)], id: id)
|
return RPCSuccessResponse(result: ["success": .bool(true)], id: id)
|
||||||
} catch let error as ExtensionValidationError {
|
} catch let error as ExtensionValidationError {
|
||||||
return errorResponse(from: error, id: id)
|
return errorResponse(from: error, id: id)
|
||||||
} catch {
|
} catch {
|
||||||
|
logDiagnostics("RPC: Decode error for updateLiveActivity: \(error)")
|
||||||
return errorResponse(code: RPCErrorCode.internalError, message: error.localizedDescription, id: id)
|
return errorResponse(code: RPCErrorCode.internalError, message: error.localizedDescription, id: id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -193,7 +210,8 @@ final class ExtensionRPCService {
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let descriptor = try decoder.decode(AtollLockScreenWidgetDescriptor.self, from: descriptorData)
|
let transformed = Self.transformClientJSON(descriptorData)
|
||||||
|
let descriptor = try decoder.decode(AtollLockScreenWidgetDescriptor.self, from: transformed)
|
||||||
try ExtensionDescriptorValidator.validate(descriptor)
|
try ExtensionDescriptorValidator.validate(descriptor)
|
||||||
try widgetManager.present(descriptor: descriptor, bundleIdentifier: descriptor.bundleIdentifier)
|
try widgetManager.present(descriptor: descriptor, bundleIdentifier: descriptor.bundleIdentifier)
|
||||||
logDiagnostics("RPC: Presented widget \(descriptor.id) for \(descriptor.bundleIdentifier)")
|
logDiagnostics("RPC: Presented widget \(descriptor.id) for \(descriptor.bundleIdentifier)")
|
||||||
|
|
@ -201,6 +219,8 @@ final class ExtensionRPCService {
|
||||||
} catch let error as ExtensionValidationError {
|
} catch let error as ExtensionValidationError {
|
||||||
return errorResponse(from: error, id: id)
|
return errorResponse(from: error, id: id)
|
||||||
} catch {
|
} catch {
|
||||||
|
logDiagnostics("RPC: Decode error for presentWidget: \(error)")
|
||||||
|
logDiagnostics("RPC: Raw payload: \(String(data: descriptorData, encoding: .utf8) ?? "<binary>")")
|
||||||
return errorResponse(code: RPCErrorCode.internalError, message: error.localizedDescription, id: id)
|
return errorResponse(code: RPCErrorCode.internalError, message: error.localizedDescription, id: id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -211,13 +231,15 @@ final class ExtensionRPCService {
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let descriptor = try decoder.decode(AtollLockScreenWidgetDescriptor.self, from: descriptorData)
|
let transformed = Self.transformClientJSON(descriptorData)
|
||||||
|
let descriptor = try decoder.decode(AtollLockScreenWidgetDescriptor.self, from: transformed)
|
||||||
try widgetManager.update(descriptor: descriptor, bundleIdentifier: descriptor.bundleIdentifier)
|
try widgetManager.update(descriptor: descriptor, bundleIdentifier: descriptor.bundleIdentifier)
|
||||||
logDiagnostics("RPC: Updated widget \(descriptor.id) for \(descriptor.bundleIdentifier)")
|
logDiagnostics("RPC: Updated widget \(descriptor.id) for \(descriptor.bundleIdentifier)")
|
||||||
return RPCSuccessResponse(result: ["success": .bool(true)], id: id)
|
return RPCSuccessResponse(result: ["success": .bool(true)], id: id)
|
||||||
} catch let error as ExtensionValidationError {
|
} catch let error as ExtensionValidationError {
|
||||||
return errorResponse(from: error, id: id)
|
return errorResponse(from: error, id: id)
|
||||||
} catch {
|
} catch {
|
||||||
|
logDiagnostics("RPC: Decode error for updateWidget: \(error)")
|
||||||
return errorResponse(code: RPCErrorCode.internalError, message: error.localizedDescription, id: id)
|
return errorResponse(code: RPCErrorCode.internalError, message: error.localizedDescription, id: id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -240,7 +262,8 @@ final class ExtensionRPCService {
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let descriptor = try decoder.decode(AtollNotchExperienceDescriptor.self, from: descriptorData)
|
let transformed = Self.transformClientJSON(descriptorData)
|
||||||
|
let descriptor = try decoder.decode(AtollNotchExperienceDescriptor.self, from: transformed)
|
||||||
try ExtensionDescriptorValidator.validate(descriptor)
|
try ExtensionDescriptorValidator.validate(descriptor)
|
||||||
try notchManager.present(descriptor: descriptor, bundleIdentifier: descriptor.bundleIdentifier)
|
try notchManager.present(descriptor: descriptor, bundleIdentifier: descriptor.bundleIdentifier)
|
||||||
logDiagnostics("RPC: Presented notch experience \(descriptor.id) for \(descriptor.bundleIdentifier)")
|
logDiagnostics("RPC: Presented notch experience \(descriptor.id) for \(descriptor.bundleIdentifier)")
|
||||||
|
|
@ -248,6 +271,8 @@ final class ExtensionRPCService {
|
||||||
} catch let error as ExtensionValidationError {
|
} catch let error as ExtensionValidationError {
|
||||||
return errorResponse(from: error, id: id)
|
return errorResponse(from: error, id: id)
|
||||||
} catch {
|
} catch {
|
||||||
|
logDiagnostics("RPC: Decode error for presentNotchExperience: \(error)")
|
||||||
|
logDiagnostics("RPC: Raw payload: \(String(data: descriptorData, encoding: .utf8) ?? "<binary>")")
|
||||||
return errorResponse(code: RPCErrorCode.internalError, message: error.localizedDescription, id: id)
|
return errorResponse(code: RPCErrorCode.internalError, message: error.localizedDescription, id: id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -258,13 +283,15 @@ final class ExtensionRPCService {
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let descriptor = try decoder.decode(AtollNotchExperienceDescriptor.self, from: descriptorData)
|
let transformed = Self.transformClientJSON(descriptorData)
|
||||||
|
let descriptor = try decoder.decode(AtollNotchExperienceDescriptor.self, from: transformed)
|
||||||
try notchManager.update(descriptor: descriptor, bundleIdentifier: descriptor.bundleIdentifier)
|
try notchManager.update(descriptor: descriptor, bundleIdentifier: descriptor.bundleIdentifier)
|
||||||
logDiagnostics("RPC: Updated notch experience \(descriptor.id) for \(descriptor.bundleIdentifier)")
|
logDiagnostics("RPC: Updated notch experience \(descriptor.id) for \(descriptor.bundleIdentifier)")
|
||||||
return RPCSuccessResponse(result: ["success": .bool(true)], id: id)
|
return RPCSuccessResponse(result: ["success": .bool(true)], id: id)
|
||||||
} catch let error as ExtensionValidationError {
|
} catch let error as ExtensionValidationError {
|
||||||
return errorResponse(from: error, id: id)
|
return errorResponse(from: error, id: id)
|
||||||
} catch {
|
} catch {
|
||||||
|
logDiagnostics("RPC: Decode error for updateNotchExperience: \(error)")
|
||||||
return errorResponse(code: RPCErrorCode.internalError, message: error.localizedDescription, id: id)
|
return errorResponse(code: RPCErrorCode.internalError, message: error.localizedDescription, id: id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -309,4 +336,119 @@ final class ExtensionRPCService {
|
||||||
guard Defaults[.extensionDiagnosticsLoggingEnabled] else { return }
|
guard Defaults[.extensionDiagnosticsLoggingEnabled] else { return }
|
||||||
Logger.log(message, category: .extensions)
|
Logger.log(message, category: .extensions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Client JSON Transformation
|
||||||
|
|
||||||
|
/// Converts client wire format to Swift Codable format for enum types.
|
||||||
|
///
|
||||||
|
/// Client sends: `{ "type": "symbol", "name": "timer", "size": 16 }`
|
||||||
|
/// Swift expects: `{ "symbol": { "name": "timer", "size": 16 } }`
|
||||||
|
///
|
||||||
|
/// For enums with unnamed parameters (e.g., `case text(String, font:, color:)`):
|
||||||
|
/// Client sends: `{ "type": "text", "text": "LIVE", "font": {...} }`
|
||||||
|
/// Swift expects: `{ "text": { "_0": "LIVE", "font": {...} } }`
|
||||||
|
static func transformClientJSON(_ data: Data) -> Data {
|
||||||
|
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
let transformed = transformObject(json)
|
||||||
|
return (try? JSONSerialization.data(withJSONObject: transformed)) ?? data
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func transformObject(_ obj: [String: Any]) -> [String: Any] {
|
||||||
|
var result: [String: Any] = [:]
|
||||||
|
|
||||||
|
for (key, value) in obj {
|
||||||
|
if let dict = value as? [String: Any] {
|
||||||
|
if enumKeys.contains(key), let typeName = dict["type"] as? String {
|
||||||
|
// Transform: { "type": "symbol", ... } → { "symbol": { ... } }
|
||||||
|
result[key] = transformEnumValue(dict, typeName: typeName)
|
||||||
|
} else {
|
||||||
|
result[key] = transformObject(dict)
|
||||||
|
}
|
||||||
|
} else if let arr = value as? [[String: Any]] {
|
||||||
|
// Arrays like "content", "elements", "sections"
|
||||||
|
result[key] = arr.map { item in
|
||||||
|
if let typeName = item["type"] as? String,
|
||||||
|
(key == "content" || key == "elements") {
|
||||||
|
return transformEnumValue(item, typeName: typeName)
|
||||||
|
}
|
||||||
|
return transformObject(item)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transforms `{ "type": "caseName", ...params }` → `{ "caseName": { ...params } }`.
|
||||||
|
/// Handles special cases where Swift enum cases have unnamed first parameters:
|
||||||
|
/// - `text` / `marquee`: `text` field → `_0`
|
||||||
|
/// - `icon`: `icon` field → `_0` (AtollIconDescriptor)
|
||||||
|
/// - `progress`: `indicator` field → `_0` (AtollProgressIndicator)
|
||||||
|
/// - `webView`: `content` field → `_0` (AtollWidgetWebContentDescriptor)
|
||||||
|
/// - `animation`: `data` field → `_0` (Data)
|
||||||
|
private static func transformEnumValue(_ dict: [String: Any], typeName: String) -> [String: Any] {
|
||||||
|
var inner: [String: Any] = [:]
|
||||||
|
|
||||||
|
for (k, v) in dict where k != "type" {
|
||||||
|
if let nestedDict = v as? [String: Any] {
|
||||||
|
// Check if this nested dict is an enum-typed field
|
||||||
|
if (enumKeys.contains(k) || k == "indicator"),
|
||||||
|
let nestedType = nestedDict["type"] as? String {
|
||||||
|
inner[k] = transformEnumValue(nestedDict, typeName: nestedType)
|
||||||
|
} else {
|
||||||
|
inner[k] = transformObject(nestedDict)
|
||||||
|
}
|
||||||
|
} else if let nestedArr = v as? [[String: Any]] {
|
||||||
|
inner[k] = nestedArr.map { item in
|
||||||
|
if let t = item["type"] as? String {
|
||||||
|
return transformEnumValue(item, typeName: t)
|
||||||
|
}
|
||||||
|
return transformObject(item)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
inner[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle unnamed first parameter for specific enum cases.
|
||||||
|
// Swift's auto-synthesized Codable encodes unnamed associated values as `_0`, `_1`, etc.
|
||||||
|
switch typeName {
|
||||||
|
case "text":
|
||||||
|
// AtollTrailingContent.text(String, ...) AND AtollWidgetContentElement.text(String, ...)
|
||||||
|
if let textValue = inner.removeValue(forKey: "text") {
|
||||||
|
inner["_0"] = textValue
|
||||||
|
}
|
||||||
|
case "marquee":
|
||||||
|
// AtollTrailingContent.marquee(String, ...)
|
||||||
|
if let textValue = inner.removeValue(forKey: "text") {
|
||||||
|
inner["_0"] = textValue
|
||||||
|
}
|
||||||
|
case "icon":
|
||||||
|
// AtollWidgetContentElement.icon(AtollIconDescriptor, tint:)
|
||||||
|
if let iconValue = inner.removeValue(forKey: "icon") {
|
||||||
|
inner["_0"] = iconValue
|
||||||
|
}
|
||||||
|
case "progress":
|
||||||
|
// AtollWidgetContentElement.progress(AtollProgressIndicator, value:, color:)
|
||||||
|
if let indicatorValue = inner.removeValue(forKey: "indicator") {
|
||||||
|
inner["_0"] = indicatorValue
|
||||||
|
}
|
||||||
|
case "webView":
|
||||||
|
// AtollWidgetContentElement.webView(AtollWidgetWebContentDescriptor)
|
||||||
|
if let contentValue = inner.removeValue(forKey: "content") {
|
||||||
|
inner["_0"] = contentValue
|
||||||
|
}
|
||||||
|
case "animation":
|
||||||
|
// AtollTrailingContent.animation(data:, size:) — data is named but let's be safe
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return [typeName: inner]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue