Files
buzz/mobile/ios/Runner/NativeSkinToneControl.swift
klopez4212 a0298539f7 Add mobile profile editing (#6583)
## Summary

- add mobile editing for display name, profile description, and profile
photo
- support image positioning, emoji backgrounds, and animated avatar
capture with native iOS controls
- refine settings navigation, profile motion, and the connection
identity row

## Testing

- `just mobile-check`
- `just mobile-test` (1,685 tests)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
2026-08-24 20:26:28 +01:00

138 lines
4.4 KiB
Swift

import Flutter
import UIKit
final class NativeSkinToneControlFactory: NSObject, FlutterPlatformViewFactory {
private let messenger: FlutterBinaryMessenger
init(messenger: FlutterBinaryMessenger) {
self.messenger = messenger
super.init()
}
func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
FlutterStandardMessageCodec.sharedInstance()
}
func create(
withFrame frame: CGRect,
viewIdentifier viewId: Int64,
arguments args: Any?
) -> FlutterPlatformView {
NativeSkinToneControlPlatformView(
frame: frame,
viewIdentifier: viewId,
arguments: args,
messenger: messenger
)
}
}
private final class NativeSkinToneControlPlatformView: NSObject, FlutterPlatformView {
private let containerView: UIView
private let channel: FlutterMethodChannel
private let button = UIButton(type: .system)
private var colors: [UIColor] = []
private var labels: [String] = []
private var value = 0
init(
frame: CGRect,
viewIdentifier viewId: Int64,
arguments args: Any?,
messenger: FlutterBinaryMessenger
) {
containerView = UIView(frame: frame)
channel = FlutterMethodChannel(
name: "buzz/native_skin_tone_control/\(viewId)",
binaryMessenger: messenger
)
super.init()
let arguments = args as? [String: Any]
colors = (arguments?["colors"] as? [NSNumber] ?? []).map {
Self.color(fromARGB: $0.uint32Value)
}
labels = arguments?["labels"] as? [String] ?? []
value = (arguments?["value"] as? NSNumber)?.intValue ?? 0
let style = arguments?["brightness"] as? String == "dark"
? UIUserInterfaceStyle.dark
: UIUserInterfaceStyle.light
containerView.overrideUserInterfaceStyle = style
containerView.backgroundColor = .clear
containerView.isOpaque = false
button.translatesAutoresizingMaskIntoConstraints = false
button.accessibilityLabel = "Skin tone"
button.showsMenuAsPrimaryAction = true
containerView.addSubview(button)
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
button.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
button.widthAnchor.constraint(equalToConstant: 44),
button.heightAnchor.constraint(equalToConstant: 44),
])
channel.setMethodCallHandler { [weak self] call, result in
guard call.method == "setValue", let value = call.arguments as? NSNumber else {
result(FlutterMethodNotImplemented)
return
}
self?.value = value.intValue
self?.refresh()
result(nil)
}
refresh()
}
func view() -> UIView { containerView }
private func refresh() {
guard !colors.isEmpty else { return }
value = min(max(0, value), colors.count - 1)
var configuration: UIButton.Configuration
if #available(iOS 26.0, *) {
configuration = .glass()
} else {
configuration = .gray()
configuration.baseBackgroundColor = .secondarySystemBackground
}
configuration.cornerStyle = .capsule
configuration.image = Self.toneImage(color: colors[value], size: 20)
button.configuration = configuration
button.menu = UIMenu(children: colors.indices.map { index in
UIAction(
title: index < labels.count ? labels[index] : "Skin tone \(index + 1)",
image: Self.toneImage(color: colors[index], size: 18),
state: index == value ? .on : .off
) { [weak self] _ in
self?.value = index
self?.refresh()
self?.channel.invokeMethod("changed", arguments: index)
}
})
}
private static func toneImage(color: UIColor, size: CGFloat) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: CGSize(width: size, height: size))
return renderer.image { context in
let rect = CGRect(x: 0.5, y: 0.5, width: size - 1, height: size - 1)
color.setFill()
context.cgContext.fillEllipse(in: rect)
UIColor.label.withAlphaComponent(0.35).setStroke()
context.cgContext.setLineWidth(1)
context.cgContext.strokeEllipse(in: rect)
}.withRenderingMode(.alwaysOriginal)
}
private static func color(fromARGB value: UInt32) -> UIColor {
UIColor(
red: CGFloat((value >> 16) & 0xff) / 255,
green: CGFloat((value >> 8) & 0xff) / 255,
blue: CGFloat(value & 0xff) / 255,
alpha: CGFloat((value >> 24) & 0xff) / 255
)
}
deinit { channel.setMethodCallHandler(nil) }
}