feat: add live module and optimize report list
This commit is contained in:
242
suixinkan/UI/Live/LiveUIComponents.swift
Normal file
242
suixinkan/UI/Live/LiveUIComponents.swift
Normal file
@ -0,0 +1,242 @@
|
||||
//
|
||||
// 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
|
||||
imageEdgeInsets = UIEdgeInsets(top: 0, left: -4, bottom: 0, right: 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)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user