// // LiveUIComponents.swift // suixinkan // import Kingfisher import PhotosUI import SnapKit import UIKit /// 直播 UI 格式化工具,集中处理 Android 页面使用的日期和时长文案。 enum LiveUIFormatter { /// 格式化直播时间范围。 static func timeRange(start: Int64, end: Int64) -> String { guard start > 0 || end > 0 else { return "" } let startText = timestampText(start) let endText = timestampText(end) if startText.isEmpty { return endText } if endText.isEmpty { return startText } return "\(startText) - \(endText)" } /// 格式化秒级时长。 static func duration(_ seconds: Int64) -> String { let value = max(seconds, 0) let hour = value / 3600 let minute = (value % 3600) / 60 let second = value % 60 if hour > 0 { return String(format: "%02lld:%02lld:%02lld", hour, minute, second) } return String(format: "%02lld:%02lld", minute, second) } /// 格式化日期。 static func date(_ date: Date?) -> String { guard let date else { return "" } return LiveAlbumListViewModel.dateString(date) } private static func timestampText(_ timestamp: Int64) -> String { guard timestamp > 0 else { return "" } let date = Date(timeIntervalSince1970: TimeInterval(timestamp)) let formatter = DateFormatter() formatter.calendar = Calendar(identifier: .gregorian) formatter.locale = Locale(identifier: "zh_CN") formatter.dateFormat = "yyyy-MM-dd HH:mm" return formatter.string(from: date) } } /// 直播模块按钮,支持 Android 页面中的主按钮、暂停和危险操作色。 final class LiveActionButton: UIButton { enum Style { case primary case warning case danger case disabled } var buttonStyle: Style = .primary { didSet { updateAppearance() } } override var isEnabled: Bool { didSet { updateAppearance() } } /// 初始化直播操作按钮。 init(title: String, image: UIImage? = nil) { super.init(frame: .zero) setTitle(title, for: .normal) setImage(image, for: .normal) titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) layer.cornerRadius = 8 clipsToBounds = true tintColor = .white setConfigurationImagePlacement(.leading, padding: 4) updateAppearance() } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func updateAppearance() { let effectiveStyle: Style = isEnabled ? buttonStyle : .disabled switch effectiveStyle { case .primary: backgroundColor = UIColor(hex: 0x0066FF) setTitleColor(.white, for: .normal) tintColor = .white case .warning: backgroundColor = AppColor.warning setTitleColor(.white, for: .normal) tintColor = .white case .danger: backgroundColor = AppColor.danger setTitleColor(.white, for: .normal) tintColor = .white case .disabled: backgroundColor = AppColor.inputBackground setTitleColor(UIColor(hex: 0xB6BECA), for: .normal) tintColor = UIColor(hex: 0xB6BECA) } } } /// 直播模块封面视图,统一 Kingfisher 加载和占位展示。 final class LiveCoverImageView: UIImageView { /// 初始化封面图片视图。 init(cornerRadius: CGFloat = 8) { super.init(frame: .zero) contentMode = .scaleAspectFill clipsToBounds = true backgroundColor = UIColor(hex: 0xF4F4F4) layer.cornerRadius = cornerRadius } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } /// 应用网络图片地址。 func setURL(_ text: String) { if let url = URL(string: text), !text.isEmpty { kf.setImage(with: url, placeholder: placeholderImage()) } else { image = placeholderImage() } } private func placeholderImage() -> UIImage? { UIImage(named: "img_square_loading_big") ?? UIImage(systemName: "photo") } } /// 虚线上传区域,复用直播封面和相册素材上传。 final class LiveDashedUploadView: UIControl { private let plusContainer = UIView() private let plusImageView = UIImageView(image: UIImage(systemName: "plus")) private let titleLabel = UILabel() private let subtitleLabel = UILabel() private var dashLayer: CAShapeLayer? /// 初始化虚线上传区域。 init(title: String = "点击上传图片", subtitle: String? = nil) { super.init(frame: .zero) setup(title: title, subtitle: subtitle) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() dashLayer?.removeFromSuperlayer() let layer = CAShapeLayer() layer.strokeColor = AppColor.border.cgColor layer.fillColor = UIColor.clear.cgColor layer.lineWidth = 2 layer.lineDashPattern = [6, 6] layer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 8).cgPath self.layer.insertSublayer(layer, at: 0) dashLayer = layer } private func setup(title: String, subtitle: String?) { backgroundColor = .white plusContainer.backgroundColor = AppColor.primary plusContainer.layer.cornerRadius = 16 plusImageView.tintColor = .white plusImageView.contentMode = .scaleAspectFit titleLabel.text = title titleLabel.font = .systemFont(ofSize: 14) titleLabel.textColor = subtitle == nil ? UIColor(hex: 0xB6BECA) : UIColor(hex: 0x4B5563) subtitleLabel.text = subtitle subtitleLabel.font = .systemFont(ofSize: 12) subtitleLabel.textColor = UIColor(hex: 0xB6BECA) subtitleLabel.textAlignment = .center addSubview(plusContainer) plusContainer.addSubview(plusImageView) addSubview(titleLabel) addSubview(subtitleLabel) plusContainer.snp.makeConstraints { make in make.centerX.equalToSuperview() make.centerY.equalToSuperview().offset(subtitle == nil ? -14 : -24) make.size.equalTo(32) } plusImageView.snp.makeConstraints { make in make.center.equalToSuperview() make.size.equalTo(18) } titleLabel.snp.makeConstraints { make in make.top.equalTo(plusContainer.snp.bottom).offset(12) make.centerX.equalToSuperview() } subtitleLabel.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(8) make.centerX.equalToSuperview() } } } /// 直播模块图片选择与上传数据加载工具。 enum LivePhotoPickerLoader { /// 从 PHPicker 结果加载图片数据。 static func loadImage(from result: PHPickerResult) async throws -> LivePickedMedia { try await withCheckedThrowingContinuation { continuation in let provider = result.itemProvider guard provider.canLoadObject(ofClass: UIImage.self) else { continuation.resume(throwing: OSSUploadError.unsupportedFileType) return } provider.loadObject(ofClass: UIImage.self) { object, error in if let error { continuation.resume(throwing: error) return } guard let image = object as? UIImage, let data = image.jpegData(compressionQuality: 0.9) else { continuation.resume(throwing: OSSUploadError.emptyFile) return } continuation.resume( returning: LivePickedMedia( data: data, fileName: "image_\(UUID().uuidString).jpg", size: Int64(data.count), width: Int(image.size.width), height: Int(image.size.height) ) ) } } } }