336 lines
10 KiB
Swift
336 lines
10 KiB
Swift
//
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import SignalServiceKit
|
|
import SignalUI
|
|
|
|
protocol UsernameLinkQRCodeColorPickerDelegate: SheetDismissalDelegate {
|
|
func didFinalizeSelectedColor(color: QRCodeColor)
|
|
}
|
|
|
|
class UsernameLinkQRCodeColorPickerViewController: OWSTableViewController2 {
|
|
private let startingColor: QRCodeColor
|
|
private var currentColor: QRCodeColor
|
|
|
|
private let username: String
|
|
private let qrCode: UIImage
|
|
|
|
private weak var colorPickerDelegate: UsernameLinkQRCodeColorPickerDelegate?
|
|
|
|
init(
|
|
currentColor: QRCodeColor,
|
|
username: String,
|
|
qrCode: UIImage,
|
|
delegate: UsernameLinkQRCodeColorPickerDelegate
|
|
) {
|
|
self.startingColor = currentColor
|
|
self.currentColor = currentColor
|
|
|
|
self.username = username
|
|
self.qrCode = qrCode
|
|
|
|
self.colorPickerDelegate = delegate
|
|
|
|
super.init()
|
|
}
|
|
|
|
// MARK: - Table contents
|
|
|
|
/// Build a view containing the QR code, username, and colored background.
|
|
///
|
|
/// This view has a fixed width, built around the fixed-width QR code.
|
|
private func buildQRCodeView() -> UIView {
|
|
let qrCodeView: QRCodeView = {
|
|
let view = QRCodeView(
|
|
qrCodeTintColor: currentColor,
|
|
contentInset: 16
|
|
)
|
|
view.autoSetDimensions(to: .square(184))
|
|
view.setQRCode(image: qrCode)
|
|
return view
|
|
}()
|
|
|
|
let usernameLabel: UILabel = {
|
|
let label = UILabel()
|
|
|
|
label.textColor = currentColor.username
|
|
label.numberOfLines = 0
|
|
label.lineBreakMode = .byCharWrapping
|
|
label.textAlignment = .center
|
|
label.font = .dynamicTypeHeadline.semibold()
|
|
label.text = username
|
|
|
|
return label
|
|
}()
|
|
|
|
let backgroundView = UIView()
|
|
backgroundView.backgroundColor = currentColor.background
|
|
backgroundView.layer.cornerRadius = 24
|
|
backgroundView.layoutMargins = UIEdgeInsets(hMargin: 40, vMargin: 32)
|
|
|
|
backgroundView.addSubview(qrCodeView)
|
|
backgroundView.addSubview(usernameLabel)
|
|
|
|
qrCodeView.autoPinEdges(toSuperviewMarginsExcludingEdge: .bottom)
|
|
|
|
qrCodeView.autoPinEdge(.bottom, to: .top, of: usernameLabel, withOffset: -16)
|
|
|
|
usernameLabel.autoPinEdges(toSuperviewMarginsExcludingEdge: .top)
|
|
|
|
return backgroundView
|
|
}
|
|
|
|
private func buildColorOptionsView() -> UIView {
|
|
let colorOptionButtons: [QRCodeColor: ColorOptionButton] = {
|
|
return QRCodeColor.allCases.reduce(into: [:]) { partial, color in
|
|
let button = ColorOptionButton(
|
|
size: 56,
|
|
color: color.background,
|
|
selected: color == currentColor
|
|
) { [weak self] in
|
|
self?.didSelectColor(color: color)
|
|
}
|
|
|
|
partial[color] = button
|
|
}
|
|
}()
|
|
|
|
func stack(colors: [QRCodeColor]) -> UIStackView {
|
|
let stack = UIStackView(arrangedSubviews: colors.map { color in
|
|
return colorOptionButtons[color]!
|
|
})
|
|
|
|
stack.layoutMargins = .zero
|
|
stack.axis = .horizontal
|
|
stack.alignment = .center
|
|
stack.distribution = .equalSpacing
|
|
|
|
return stack
|
|
}
|
|
|
|
let topStack = stack(colors: [.blue, .white, .grey, .olive])
|
|
let bottomStack = stack(colors: [.green, .orange, .pink, .purple])
|
|
|
|
let view = UIView()
|
|
view.addSubview(topStack)
|
|
view.addSubview(bottomStack)
|
|
|
|
topStack.autoPinEdges(toSuperviewEdgesExcludingEdge: .bottom)
|
|
topStack.autoPinEdge(.bottom, to: .top, of: bottomStack, withOffset: -26)
|
|
bottomStack.autoPinEdges(toSuperviewEdgesExcludingEdge: .top)
|
|
|
|
return view
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
navigationItem.leftBarButtonItem = .cancelButton(dismissingFrom: self)
|
|
|
|
navigationItem.rightBarButtonItem = .doneButton { [weak self] in
|
|
self?.didTapDone()
|
|
}
|
|
|
|
navigationItem.title = OWSLocalizedString(
|
|
"USERNAME_LINK_QR_CODE_COLOR_PICKER_VIEW_TITLE_COLOR",
|
|
comment: "A title for a view that allows you to pick a color for a QR code for your username link."
|
|
)
|
|
|
|
buildTableContents()
|
|
}
|
|
|
|
override func viewDidDisappear(_ animated: Bool) {
|
|
super.viewDidDisappear(animated)
|
|
colorPickerDelegate?.didDismissPresentedSheet()
|
|
}
|
|
|
|
private func buildTableContents() {
|
|
let section = OWSTableSection(items: [
|
|
.itemWrappingView(
|
|
viewBlock: { [weak self] in
|
|
guard let self else { return nil }
|
|
|
|
let qrCodeView = self.buildQRCodeView()
|
|
|
|
// The QR code view has a fixed width, so wrap it in a view
|
|
// that can stretch.
|
|
let wrapper = UIView()
|
|
wrapper.addSubview(qrCodeView)
|
|
|
|
qrCodeView.autoPinEdge(toSuperviewEdge: .top)
|
|
qrCodeView.autoPinEdge(toSuperviewEdge: .bottom)
|
|
qrCodeView.autoHCenterInSuperview()
|
|
|
|
return wrapper
|
|
},
|
|
margins: UIEdgeInsets(top: 20, leading: 32, bottom: 24, trailing: 32)
|
|
),
|
|
.itemWrappingView(
|
|
viewBlock: { [weak self] in
|
|
self?.buildColorOptionsView()
|
|
},
|
|
margins: UIEdgeInsets(top: 24, leading: 36, bottom: 16, trailing: 36)
|
|
)
|
|
])
|
|
|
|
section.hasBackground = false
|
|
section.hasSeparators = false
|
|
|
|
contents = OWSTableContents(sections: [section])
|
|
}
|
|
|
|
private func reloadTableContents() {
|
|
self.tableView.reloadData()
|
|
}
|
|
|
|
// MARK: - Events
|
|
|
|
private func didTapDone() {
|
|
if startingColor != currentColor {
|
|
colorPickerDelegate?.didFinalizeSelectedColor(color: currentColor)
|
|
}
|
|
|
|
dismiss(animated: true)
|
|
}
|
|
|
|
private func didSelectColor(color selectedColor: QRCodeColor) {
|
|
currentColor = selectedColor
|
|
reloadTableContents()
|
|
}
|
|
}
|
|
|
|
// MARK: - ColorOptionButton
|
|
|
|
private extension UsernameLinkQRCodeColorPickerViewController {
|
|
/// Represents a single color that can be selected by the user.
|
|
class ColorOptionButton: UIButton {
|
|
private let size: CGFloat
|
|
private let color: UIColor
|
|
|
|
private let onTap: () -> Void
|
|
|
|
init(
|
|
size: CGFloat,
|
|
color: UIColor,
|
|
selected: Bool,
|
|
onTap: @escaping () -> Void
|
|
) {
|
|
self.size = size
|
|
self.color = color
|
|
self.onTap = onTap
|
|
|
|
super.init(frame: .zero)
|
|
|
|
setImage(selected: selected)
|
|
|
|
ows_adjustsImageWhenHighlighted = false
|
|
autoPinToSquareAspectRatio()
|
|
autoSetDimension(.width, toSize: size)
|
|
|
|
addTarget(self, action: #selector(didTap), for: .touchUpInside)
|
|
}
|
|
|
|
required init?(coder: NSCoder) { owsFail("Not implemented!") }
|
|
|
|
override var frame: CGRect {
|
|
didSet { layer.cornerRadius = width / 2 }
|
|
}
|
|
|
|
private func setImage(selected: Bool) {
|
|
let image: UIImage = {
|
|
if selected {
|
|
return Self.drawSelectedImage(
|
|
color: color.cgColor,
|
|
outerCircleColor: Theme.isDarkThemeEnabled ? .white : .black,
|
|
size: .square(size)
|
|
)
|
|
} else {
|
|
return Self.drawUnselectedImage(
|
|
color: color.cgColor,
|
|
size: .square(size)
|
|
)
|
|
}
|
|
}()
|
|
|
|
setImage(image, for: .normal)
|
|
}
|
|
|
|
@objc
|
|
private func didTap() {
|
|
onTap()
|
|
}
|
|
|
|
// MARK: Image drawing
|
|
|
|
/// A colored circle with a dimmed border.
|
|
private static func drawUnselectedImage(
|
|
color: CGColor,
|
|
size: CGSize
|
|
) -> UIImage {
|
|
return UIGraphicsImageRenderer(size: size).image { uiContext in
|
|
drawColoredCircleWithBorder(
|
|
cgContext: uiContext.cgContext,
|
|
color: color,
|
|
rect: CGRect(origin: .zero, size: size)
|
|
)
|
|
}
|
|
}
|
|
|
|
/// A colored circle with a dimmed border, inset within an outer circle.
|
|
private static func drawSelectedImage(
|
|
color: CGColor,
|
|
outerCircleColor: CGColor,
|
|
size: CGSize
|
|
) -> UIImage {
|
|
return UIGraphicsImageRenderer(size: size).image { uiContext in
|
|
let rect = CGRect(origin: .zero, size: size)
|
|
|
|
let cgContext = uiContext.cgContext
|
|
|
|
cgContext.setStrokeColor(outerCircleColor)
|
|
cgContext.strokeEllipse(fittingIn: rect, width: 3)
|
|
|
|
drawColoredCircleWithBorder(
|
|
cgContext: cgContext,
|
|
color: color,
|
|
rect: rect.inset(by: UIEdgeInsets(margin: 7))
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Draw a colored circle with a border into the given rect in the given
|
|
/// context.
|
|
private static func drawColoredCircleWithBorder(
|
|
cgContext: CGContext,
|
|
color: CGColor,
|
|
rect: CGRect
|
|
) {
|
|
cgContext.setFillColor(color)
|
|
cgContext.fillEllipse(in: rect)
|
|
|
|
cgContext.setStrokeColor(.black_alpha12)
|
|
cgContext.strokeEllipse(fittingIn: rect, width: 2)
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension CGContext {
|
|
func strokeEllipse(fittingIn rect: CGRect, width: CGFloat) {
|
|
setLineWidth(width)
|
|
strokeEllipse(in: rect.inset(by: width / 2))
|
|
}
|
|
}
|
|
|
|
private extension CGRect {
|
|
func inset(by amount: CGFloat) -> CGRect {
|
|
return insetBy(dx: amount, dy: amount)
|
|
}
|
|
}
|
|
|
|
private extension CGColor {
|
|
static let white: CGColor = CGColor(red: 1, green: 1, blue: 1, alpha: 1)
|
|
static let black: CGColor = CGColor(red: 0, green: 0, blue: 0, alpha: 1)
|
|
static let black_alpha12 = CGColor(red: 0, green: 0, blue: 0, alpha: 0.12)
|
|
}
|