|
|
@@ -0,0 +1,367 @@
|
|
|
+//
|
|
|
+// LNRoomOrderGuideView.swift
|
|
|
+// Gami
|
|
|
+//
|
|
|
+// Created by OneeChan on 2026/3/22.
|
|
|
+//
|
|
|
+
|
|
|
+import Foundation
|
|
|
+import UIKit
|
|
|
+import SnapKit
|
|
|
+
|
|
|
+
|
|
|
+class LNRoomOrderGuideView: UIView {
|
|
|
+ static weak var joinView: UIView?
|
|
|
+ static weak var mainSeatView: UIView?
|
|
|
+ static weak var playmateView: UIView?
|
|
|
+ static weak var orderCardView: UIView?
|
|
|
+ private static weak var currentGuide: LNRoomOrderGuideView?
|
|
|
+
|
|
|
+ private enum Step: Int, CaseIterable {
|
|
|
+ case join
|
|
|
+ case demand
|
|
|
+ case playmate
|
|
|
+ case order
|
|
|
+
|
|
|
+ var cardSize: CGSize {
|
|
|
+ switch self {
|
|
|
+ case .join, .playmate, .order:
|
|
|
+ .init(width: 314, height: self == .join ? 136 : 120)
|
|
|
+ case .demand:
|
|
|
+ .init(width: 320, height: 136)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ var focusInset: UIEdgeInsets {
|
|
|
+ switch self {
|
|
|
+ case .join:
|
|
|
+ .init(top: -4, left: -4, bottom: -4, right: -4)
|
|
|
+ case .demand:
|
|
|
+ .init(top: -8, left: -4, bottom: -8, right: -4)
|
|
|
+ case .playmate:
|
|
|
+ .init(top: 0, left: -4, bottom: 0, right: -4)
|
|
|
+ case .order:
|
|
|
+ .init(top: -2, left: -2, bottom: -2, right: -2)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ var focusCornerRadius: CGFloat {
|
|
|
+ switch self {
|
|
|
+ case .join:
|
|
|
+ 15
|
|
|
+ case .demand, .playmate:
|
|
|
+ 12
|
|
|
+ case .order:
|
|
|
+ 12
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ var progressText: String {
|
|
|
+ "(\(rawValue + 1)/\(Self.allCases.count))"
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private let dimLayer = CAShapeLayer()
|
|
|
+ private let lineLayer = CAShapeLayer()
|
|
|
+
|
|
|
+ private let dotView = UIView()
|
|
|
+ private let dotInnerView = UIView()
|
|
|
+
|
|
|
+ private let cardView = UIView()
|
|
|
+ private let cardGlowView = UIImageView(image: .primary_6)
|
|
|
+ private let titleLabel = UILabel()
|
|
|
+ private let descLabel = UILabel()
|
|
|
+ private let nextButton = UIButton(type: .custom)
|
|
|
+ private let nextTitleLabel = UILabel()
|
|
|
+ private let progressLabel = UILabel()
|
|
|
+
|
|
|
+ private weak var profileCard: LNRoomProfileCardPanel?
|
|
|
+
|
|
|
+ private var currentStep: Step = .join {
|
|
|
+ didSet {
|
|
|
+ updateTexts()
|
|
|
+ setNeedsLayout()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ static func show(_ container: UIView) {
|
|
|
+ guard currentGuide == nil else { return }
|
|
|
+
|
|
|
+ let guide = LNRoomOrderGuideView()
|
|
|
+ currentGuide = guide
|
|
|
+ container.addSubview(guide)
|
|
|
+ guide.snp.makeConstraints { make in
|
|
|
+ make.edges.equalToSuperview()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override init(frame: CGRect) {
|
|
|
+ super.init(frame: frame)
|
|
|
+
|
|
|
+ setupViews()
|
|
|
+ updateTexts()
|
|
|
+ }
|
|
|
+
|
|
|
+ required init?(coder: NSCoder) {
|
|
|
+ fatalError("init(coder:) has not been implemented")
|
|
|
+ }
|
|
|
+
|
|
|
+ override func layoutSubviews() {
|
|
|
+ super.layoutSubviews()
|
|
|
+
|
|
|
+ dimLayer.frame = bounds
|
|
|
+ lineLayer.frame = bounds
|
|
|
+ updateLayout()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+private extension LNRoomOrderGuideView {
|
|
|
+ var currentAnchorView: UIView? {
|
|
|
+ switch currentStep {
|
|
|
+ case .join:
|
|
|
+ Self.joinView
|
|
|
+ case .demand:
|
|
|
+ Self.mainSeatView
|
|
|
+ case .playmate:
|
|
|
+ Self.playmateView
|
|
|
+ case .order:
|
|
|
+ Self.orderCardView
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func setupViews() {
|
|
|
+ backgroundColor = .clear
|
|
|
+
|
|
|
+ dimLayer.fillColor = UIColor.black.withAlphaComponent(0.8).cgColor
|
|
|
+ dimLayer.fillRule = .evenOdd
|
|
|
+ layer.addSublayer(dimLayer)
|
|
|
+
|
|
|
+ lineLayer.strokeColor = UIColor.fill.cgColor
|
|
|
+ lineLayer.lineWidth = 1
|
|
|
+ lineLayer.fillColor = UIColor.clear.cgColor
|
|
|
+ layer.addSublayer(lineLayer)
|
|
|
+
|
|
|
+ dotView.backgroundColor = UIColor.fill.withAlphaComponent(0.2)
|
|
|
+ dotView.layer.cornerRadius = 7.5
|
|
|
+ addSubview(dotView)
|
|
|
+
|
|
|
+ dotInnerView.backgroundColor = .fill
|
|
|
+ dotInnerView.layer.cornerRadius = 3.5
|
|
|
+ dotView.addSubview(dotInnerView)
|
|
|
+ dotInnerView.snp.makeConstraints { make in
|
|
|
+ make.center.equalToSuperview()
|
|
|
+ make.width.height.equalTo(7)
|
|
|
+ }
|
|
|
+
|
|
|
+ cardView.backgroundColor = .fill
|
|
|
+ cardView.layer.cornerRadius = 20
|
|
|
+ cardView.clipsToBounds = true
|
|
|
+ addSubview(cardView)
|
|
|
+ cardView.frame = .init(x: 0, y: 0, width: 50, height: 50)
|
|
|
+
|
|
|
+ cardGlowView.alpha = 0.6
|
|
|
+ cardGlowView.contentMode = .scaleToFill
|
|
|
+ cardView.addSubview(cardGlowView)
|
|
|
+ cardGlowView.snp.makeConstraints { make in
|
|
|
+ make.edges.equalToSuperview()
|
|
|
+ }
|
|
|
+
|
|
|
+ titleLabel.font = .heading_h3
|
|
|
+ titleLabel.textColor = .text_5
|
|
|
+ titleLabel.numberOfLines = 0
|
|
|
+ cardView.addSubview(titleLabel)
|
|
|
+ titleLabel.snp.makeConstraints { make in
|
|
|
+ make.top.equalToSuperview().offset(20)
|
|
|
+ make.leading.equalToSuperview().offset(15)
|
|
|
+ make.trailing.equalToSuperview().offset(-15)
|
|
|
+ }
|
|
|
+
|
|
|
+ descLabel.font = .body_m
|
|
|
+ descLabel.textColor = .text_4
|
|
|
+ descLabel.numberOfLines = 0
|
|
|
+ cardView.addSubview(descLabel)
|
|
|
+ descLabel.snp.makeConstraints { make in
|
|
|
+ make.top.equalTo(titleLabel.snp.bottom).offset(8)
|
|
|
+ make.leading.equalToSuperview().offset(15)
|
|
|
+ make.trailing.equalToSuperview().offset(-15)
|
|
|
+ }
|
|
|
+
|
|
|
+ nextButton.setBackgroundImage(.primary_7, for: .normal)
|
|
|
+ nextButton.layer.cornerRadius = 16.5
|
|
|
+ nextButton.clipsToBounds = true
|
|
|
+ nextButton.addAction(UIAction(handler: { [weak self] _ in
|
|
|
+ self?.handleNext()
|
|
|
+ }), for: .touchUpInside)
|
|
|
+ cardView.addSubview(nextButton)
|
|
|
+ nextButton.snp.makeConstraints { make in
|
|
|
+ make.trailing.equalToSuperview().offset(-15)
|
|
|
+ make.bottom.equalToSuperview().offset(-13)
|
|
|
+ make.height.equalTo(33)
|
|
|
+ }
|
|
|
+
|
|
|
+ let buttonStack = UIStackView()
|
|
|
+ buttonStack.axis = .horizontal
|
|
|
+ buttonStack.spacing = 2
|
|
|
+ buttonStack.alignment = .center
|
|
|
+ buttonStack.isUserInteractionEnabled = false
|
|
|
+ nextButton.addSubview(buttonStack)
|
|
|
+ buttonStack.snp.makeConstraints { make in
|
|
|
+ make.leading.equalToSuperview().offset(12)
|
|
|
+ make.trailing.equalToSuperview().offset(-12)
|
|
|
+ make.centerY.equalToSuperview()
|
|
|
+ }
|
|
|
+
|
|
|
+ nextTitleLabel.font = .heading_h3
|
|
|
+ nextTitleLabel.textColor = .text_1
|
|
|
+ buttonStack.addArrangedSubview(nextTitleLabel)
|
|
|
+
|
|
|
+ progressLabel.font = .body_xs
|
|
|
+ progressLabel.textColor = .text_1
|
|
|
+ buttonStack.addArrangedSubview(progressLabel)
|
|
|
+ }
|
|
|
+
|
|
|
+ func updateTexts() {
|
|
|
+ nextTitleLabel.text = .init(key: "A00373")
|
|
|
+ progressLabel.text = currentStep.progressText
|
|
|
+
|
|
|
+ switch currentStep {
|
|
|
+ case .join:
|
|
|
+ titleLabel.text = .init(key: "A00374")
|
|
|
+ descLabel.attributedText = nil
|
|
|
+ descLabel.text = .init(key: "A00375")
|
|
|
+ case .demand:
|
|
|
+ titleLabel.text = .init(key: "A00376")
|
|
|
+ descLabel.attributedText = highlightedDemandText()
|
|
|
+ case .playmate:
|
|
|
+ titleLabel.text = .init(key: "A00381")
|
|
|
+ descLabel.attributedText = nil
|
|
|
+ descLabel.text = .init(key: "A00382")
|
|
|
+ case .order:
|
|
|
+ titleLabel.text = .init(key: "A00383")
|
|
|
+ descLabel.attributedText = nil
|
|
|
+ descLabel.text = .init(key: "A00384")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func highlightedDemandText() -> NSAttributedString {
|
|
|
+ let text = String(key: "A00378")
|
|
|
+ let attributed = NSMutableAttributedString(
|
|
|
+ string: text,
|
|
|
+ attributes: [
|
|
|
+ .font: UIFont.body_m,
|
|
|
+ .foregroundColor: UIColor.text_4
|
|
|
+ ]
|
|
|
+ )
|
|
|
+ [String(key: "A00379"), String(key: "A00380"), String(key: "A00385")].forEach { target in
|
|
|
+ let nsText = text as NSString
|
|
|
+ let range = nsText.range(of: target)
|
|
|
+ if range.location != NSNotFound {
|
|
|
+ attributed.addAttributes([
|
|
|
+ .foregroundColor: UIColor.text_6
|
|
|
+ ], range: range)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return attributed
|
|
|
+ }
|
|
|
+
|
|
|
+ func updateLayout() {
|
|
|
+ guard let focusRect = currentFocusRect() else {
|
|
|
+ dimLayer.path = UIBezierPath(rect: bounds).cgPath
|
|
|
+ lineLayer.path = nil
|
|
|
+ dotView.isHidden = true
|
|
|
+ cardView.isHidden = true
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ cardView.isHidden = false
|
|
|
+ dotView.isHidden = false
|
|
|
+
|
|
|
+ let cardFrame = currentCardFrame(for: focusRect)
|
|
|
+ cardView.frame = cardFrame
|
|
|
+
|
|
|
+ let maskPath = UIBezierPath(rect: bounds)
|
|
|
+ maskPath.append(UIBezierPath(roundedRect: focusRect, cornerRadius: currentStep.focusCornerRadius))
|
|
|
+ dimLayer.path = maskPath.cgPath
|
|
|
+
|
|
|
+ let isCardAbove = cardFrame.maxY <= focusRect.minY
|
|
|
+ let dotCenter = CGPoint(
|
|
|
+ x: focusRect.midX,
|
|
|
+ y: isCardAbove ? focusRect.minY - 7.5 : focusRect.maxY + 7.5
|
|
|
+ )
|
|
|
+ dotView.bounds = CGRect(x: 0, y: 0, width: 15, height: 15)
|
|
|
+ dotView.center = dotCenter
|
|
|
+
|
|
|
+ let path = UIBezierPath()
|
|
|
+ path.move(to: CGPoint(x: dotCenter.x, y: isCardAbove ? cardFrame.maxY : cardFrame.minY))
|
|
|
+ path.addLine(to: dotCenter)
|
|
|
+ lineLayer.path = path.cgPath
|
|
|
+ }
|
|
|
+
|
|
|
+ func currentFocusRect() -> CGRect? {
|
|
|
+ guard let anchor = currentAnchorView,
|
|
|
+ anchor.window != nil else {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ let rect = anchor.convert(anchor.bounds, to: self)
|
|
|
+ let inset = currentStep.focusInset
|
|
|
+ let focusedRect = rect.inset(by: inset)
|
|
|
+ let visibleRect = focusedRect.intersection(bounds)
|
|
|
+ guard !visibleRect.isNull, !visibleRect.isEmpty else { return nil }
|
|
|
+ return visibleRect
|
|
|
+ }
|
|
|
+
|
|
|
+ func currentCardFrame(for focusRect: CGRect) -> CGRect {
|
|
|
+ let size = currentStep.cardSize
|
|
|
+ switch currentStep {
|
|
|
+ case .join:
|
|
|
+ let x = bounds.width - 12 - size.width
|
|
|
+ let y = focusRect.minY - 56 - size.height
|
|
|
+ return .init(x: x, y: max(100, y), width: size.width, height: size.height)
|
|
|
+ case .demand:
|
|
|
+ let x = bounds.width - 12 - size.width
|
|
|
+ return .init(x: x, y: focusRect.maxY + 61, width: size.width, height: size.height)
|
|
|
+ case .playmate:
|
|
|
+ return .init(x: (bounds.width - size.width) / 2, y: focusRect.maxY + 59, width: size.width, height: size.height)
|
|
|
+ case .order:
|
|
|
+ let y = focusRect.minY - 66 - size.height
|
|
|
+ return .init(x: 10, y: max(100, y), width: size.width, height: size.height)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func handleNext() {
|
|
|
+ switch currentStep {
|
|
|
+ case .join:
|
|
|
+ currentStep = .demand
|
|
|
+ case .demand:
|
|
|
+ currentStep = .playmate
|
|
|
+ case .playmate:
|
|
|
+ showOrderStepIfPossible()
|
|
|
+ case .order:
|
|
|
+ dismiss()
|
|
|
+ profileCard?.dismiss(animated: false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func showOrderStepIfPossible() {
|
|
|
+ let panel = LNRoomProfileCardPanel()
|
|
|
+ panel.toBeExample()
|
|
|
+ panel.popup(superview, animated: false)
|
|
|
+ superview?.bringSubviewToFront(self)
|
|
|
+ profileCard = panel
|
|
|
+
|
|
|
+ currentStep = .order
|
|
|
+ }
|
|
|
+
|
|
|
+ func dismiss() {
|
|
|
+ if Self.currentGuide === self {
|
|
|
+ Self.currentGuide = nil
|
|
|
+ }
|
|
|
+
|
|
|
+ UIView.animate(withDuration: 0.2, animations: {
|
|
|
+ self.alpha = 0
|
|
|
+ }, completion: { [weak self] _ in
|
|
|
+ self?.removeFromSuperview()
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|