Files
suixinkan_uikit/suixinkan/UI/Task/Views/TaskAddViews.swift

960 lines
35 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// TaskAddViews.swift
// suixinkan
//
import Kingfisher
import SnapKit
import UIKit
///
final class TaskAddMediaSectionView: UIView {
var onCloudImport: (() -> Void)?
var onLocalImport: (() -> Void)?
var onFileTap: ((TaskFileItem) -> Void)?
var onDeleteFile: ((TaskFileItem) -> Void)?
private var files: [TaskFileItem] = []
private var collectionHeightConstraint: Constraint?
private let gridColumns = 3
private let gridSpacing: CGFloat = 8
private enum Section { case main }
private enum Item: Hashable { case add; case file(String) }
private lazy var collectionView: UICollectionView = {
let layout = UICollectionViewCompositionalLayout { [weak self] _, environment in
let columns = self?.gridColumns ?? 3
let spacing = self?.gridSpacing ?? 8
let availableWidth = environment.container.effectiveContentSize.width
let totalSpacing = spacing * CGFloat(columns - 1)
let itemWidth = max(0, (availableWidth - totalSpacing) / CGFloat(columns))
let itemHeight = itemWidth / 1.77
let itemSize = NSCollectionLayoutSize(
widthDimension: .absolute(itemWidth),
heightDimension: .absolute(itemHeight)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .absolute(itemHeight)
)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: groupSize,
repeatingSubitem: item,
count: columns
)
group.interItemSpacing = .fixed(spacing)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = spacing
return section
}
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
view.backgroundColor = .clear
view.isScrollEnabled = false
return view
}()
private lazy var dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) {
[weak self] collectionView, indexPath, item in
switch item {
case .add:
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: TaskAddAddMediaCell.reuseIdentifier,
for: indexPath
) as! TaskAddAddMediaCell
return cell
case .file(let id):
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: TaskAddMediaCell.reuseIdentifier,
for: indexPath
) as! TaskAddMediaCell
if let file = self?.files.first(where: { $0.id == id }) {
cell.apply(file: file)
cell.onDelete = { self?.onDeleteFile?(file) }
}
return cell
}
}
private let titleLabel = UILabel()
private let tipsLabel = UILabel()
private let cloudButton = UIButton(type: .system)
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
titleLabel.text = "添加图片"
titleLabel.font = .app(.bodyMedium)
titleLabel.textColor = AppColor.textPrimary
tipsLabel.text = "(点击图片或视频可添加备注)"
tipsLabel.font = .app(.caption)
tipsLabel.textColor = AppColor.textPrimary
tipsLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
cloudButton.setTitle("云盘导入", for: .normal)
cloudButton.setTitleColor(AppColor.primary, for: .normal)
cloudButton.titleLabel?.font = .app(.caption)
cloudButton.addTarget(self, action: #selector(cloudTapped), for: .touchUpInside)
cloudButton.setContentCompressionResistancePriority(.required, for: .horizontal)
collectionView.register(TaskAddMediaCell.self, forCellWithReuseIdentifier: TaskAddMediaCell.reuseIdentifier)
collectionView.register(TaskAddAddMediaCell.self, forCellWithReuseIdentifier: TaskAddAddMediaCell.reuseIdentifier)
collectionView.delegate = self
addSubview(titleLabel)
addSubview(tipsLabel)
addSubview(cloudButton)
addSubview(collectionView)
titleLabel.snp.makeConstraints { make in
make.top.leading.equalToSuperview()
}
tipsLabel.snp.makeConstraints { make in
make.leading.equalTo(titleLabel.snp.trailing).offset(AppSpacing.xxs)
make.centerY.equalTo(titleLabel)
make.trailing.lessThanOrEqualTo(cloudButton.snp.leading).offset(-AppSpacing.xs)
}
cloudButton.snp.makeConstraints { make in
make.trailing.equalToSuperview()
make.centerY.equalTo(titleLabel)
}
collectionView.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(AppSpacing.sm)
make.leading.trailing.bottom.equalToSuperview()
collectionHeightConstraint = make.height.equalTo(88).constraint
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(files: [TaskFileItem]) {
self.files = files
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
snapshot.appendItems(files.map { Item.file($0.id) } + [.add])
dataSource.apply(snapshot, animatingDifferences: false)
updateCollectionHeight(itemCount: files.count + 1)
}
override func layoutSubviews() {
super.layoutSubviews()
updateCollectionHeight(itemCount: files.count + 1)
}
private func updateCollectionHeight(itemCount: Int) {
let availableWidth = collectionView.bounds.width
guard availableWidth > 0 else { return }
let itemWidth = max(0, (availableWidth - gridSpacing * CGFloat(gridColumns - 1)) / CGFloat(gridColumns))
let itemHeight = itemWidth / 1.77
let rows = max(1, Int(ceil(Double(itemCount) / Double(gridColumns))))
let height = itemHeight * CGFloat(rows) + gridSpacing * CGFloat(rows - 1)
collectionHeightConstraint?.update(offset: max(88, height))
}
@objc private func cloudTapped() {
onCloudImport?()
}
}
extension TaskAddMediaSectionView: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
switch item {
case .add:
onLocalImport?()
case .file(let id):
guard let file = files.first(where: { $0.id == id }) else { return }
onFileTap?(file)
}
}
}
/// Android cell
final class TaskCircularProgressView: UIView {
private let trackLayer = CAShapeLayer()
private let progressLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
isHidden = true
layer.addSublayer(trackLayer)
layer.addSublayer(progressLayer)
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.strokeColor = UIColor.white.withAlphaComponent(0.35).cgColor
trackLayer.lineWidth = 3
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.strokeColor = UIColor.white.cgColor
progressLayer.lineCap = .round
progressLayer.lineWidth = 3
progressLayer.strokeEnd = 0
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
let radius = min(bounds.width, bounds.height) / 2 - progressLayer.lineWidth / 2
let path = UIBezierPath(
arcCenter: CGPoint(x: bounds.midX, y: bounds.midY),
radius: radius,
startAngle: -.pi / 2,
endAngle: .pi * 1.5,
clockwise: true
)
trackLayer.frame = bounds
progressLayer.frame = bounds
trackLayer.path = path.cgPath
progressLayer.path = path.cgPath
}
/// 0...1
func setProgress(_ progress: CGFloat) {
progressLayer.strokeEnd = min(1, max(0, progress))
}
}
/// cell
final class TaskAddMediaCell: UICollectionViewCell {
static let reuseIdentifier = "TaskAddMediaCell"
var onDelete: (() -> Void)?
private let imageView = UIImageView()
private let overlayView = UIView()
private let progressRingView = TaskCircularProgressView()
private let progressLabel = UILabel()
private let deleteButton = UIButton(type: .system)
private let playIconView = UIImageView()
private let sourceBadgeLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = AppColor.inputBackground
contentView.layer.cornerRadius = AppRadius.sm
contentView.clipsToBounds = true
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
overlayView.backgroundColor = UIColor.black.withAlphaComponent(0.2)
overlayView.isHidden = true
progressLabel.font = .app(.captionMedium)
progressLabel.textColor = .white
progressLabel.textAlignment = .center
deleteButton.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal)
deleteButton.tintColor = .white
deleteButton.addTarget(self, action: #selector(deleteTapped), for: .touchUpInside)
playIconView.image = UIImage(systemName: "play.circle.fill")
playIconView.tintColor = .white
playIconView.isHidden = true
sourceBadgeLabel.font = .app(.caption)
sourceBadgeLabel.textColor = .white
sourceBadgeLabel.textAlignment = .center
sourceBadgeLabel.backgroundColor = UIColor.black.withAlphaComponent(0.5)
sourceBadgeLabel.layer.cornerRadius = AppRadius.xs
sourceBadgeLabel.clipsToBounds = true
sourceBadgeLabel.isHidden = true
contentView.addSubview(imageView)
contentView.addSubview(overlayView)
contentView.addSubview(sourceBadgeLabel)
contentView.addSubview(playIconView)
contentView.addSubview(progressRingView)
contentView.addSubview(progressLabel)
contentView.addSubview(deleteButton)
imageView.snp.makeConstraints { make in make.edges.equalToSuperview() }
overlayView.snp.makeConstraints { make in make.edges.equalToSuperview() }
sourceBadgeLabel.snp.makeConstraints { make in
make.leading.bottom.equalToSuperview().inset(AppSpacing.xxs)
make.height.equalTo(18)
make.width.greaterThanOrEqualTo(34)
}
playIconView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.height.equalTo(24)
}
progressRingView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.centerY.equalToSuperview().offset(-10)
make.width.height.equalTo(28)
}
progressLabel.snp.makeConstraints { make in
make.top.equalTo(progressRingView.snp.bottom).offset(2)
make.centerX.equalToSuperview()
}
deleteButton.snp.makeConstraints { make in
make.top.trailing.equalToSuperview().inset(AppSpacing.xxs)
make.width.height.equalTo(20)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
onDelete = nil
imageView.kf.cancelDownloadTask()
imageView.image = nil
progressRingView.setProgress(0)
}
func apply(file: TaskFileItem) {
let urlString = file.previewURLString
if let url = URL(string: urlString), !urlString.isEmpty {
imageView.kf.setImage(with: url)
} else {
imageView.image = UIImage(systemName: "photo")
imageView.tintColor = AppColor.textTertiary
}
playIconView.isHidden = file.fileType != 1 || file.isUploading
deleteButton.isHidden = file.isUploading
sourceBadgeLabel.isHidden = file.isUploading
sourceBadgeLabel.text = file.sourceBadgeText
if file.isUploading {
overlayView.isHidden = false
progressRingView.isHidden = false
progressRingView.setProgress(CGFloat(file.uploadProgress) / 100)
progressLabel.text = "\(file.uploadProgress)%"
} else {
overlayView.isHidden = true
progressRingView.isHidden = true
progressLabel.text = nil
}
}
@objc private func deleteTapped() {
onDelete?()
}
}
/// cell
final class TaskAddAddMediaCell: UICollectionViewCell {
static let reuseIdentifier = "TaskAddAddMediaCell"
private let iconView = UIImageView()
private let titleLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = AppColor.inputBackground
contentView.layer.cornerRadius = AppRadius.sm
iconView.image = UIImage(systemName: "plus")
iconView.tintColor = AppColor.textTabInactive
titleLabel.text = "本地导入"
titleLabel.font = .app(.caption)
titleLabel.textColor = AppColor.textTabInactive
contentView.addSubview(iconView)
contentView.addSubview(titleLabel)
iconView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.centerY.equalToSuperview().offset(-8)
make.width.height.equalTo(24)
}
titleLabel.snp.makeConstraints { make in
make.top.equalTo(iconView.snp.bottom).offset(2)
make.centerX.equalToSuperview()
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
///
final class TaskPrioritySectionView: UIView {
var onUrgentHourChange: ((Int) -> Void)?
var onCustomHourTextChange: ((String) -> Void)?
private let titleLabel = UILabel()
private let noneButton = UIButton(type: .system)
private let twoHourButton = UIButton(type: .system)
private let customButton = UIButton(type: .system)
private let customField = UITextField()
private let customConfirmButton = UIButton(type: .system)
private let optionRow = UIStackView()
private let customRow = UIStackView()
private let stackView = UIStackView()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
titleLabel.text = "优先级"
titleLabel.font = .app(.bodyMedium)
titleLabel.textColor = AppColor.textPrimary
configureOptionButton(noneButton, title: "不加急")
configureOptionButton(twoHourButton, title: "加急(2小时)")
configureOptionButton(customButton, title: "自定义")
noneButton.addTarget(self, action: #selector(noneTapped), for: .touchUpInside)
twoHourButton.addTarget(self, action: #selector(twoHourTapped), for: .touchUpInside)
customButton.addTarget(self, action: #selector(customTapped), for: .touchUpInside)
customField.placeholder = "请输入小时数"
customField.font = .app(.body)
customField.textColor = AppColor.textPrimary
customField.keyboardType = .numberPad
customField.backgroundColor = .white
customField.layer.cornerRadius = AppRadius.sm
customField.layer.borderWidth = 1
customField.layer.borderColor = AppColor.border.cgColor
customField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: AppSpacing.sm, height: 1))
customField.leftViewMode = .always
customField.addTarget(self, action: #selector(customFieldChanged), for: .editingChanged)
customConfirmButton.setTitle("确定", for: .normal)
customConfirmButton.setTitleColor(.white, for: .normal)
customConfirmButton.titleLabel?.font = .app(.bodyMedium)
customConfirmButton.backgroundColor = AppColor.primary
customConfirmButton.layer.cornerRadius = AppRadius.xs
customConfirmButton.addTarget(self, action: #selector(customConfirmTapped), for: .touchUpInside)
optionRow.addArrangedSubview(noneButton)
optionRow.addArrangedSubview(twoHourButton)
optionRow.addArrangedSubview(customButton)
optionRow.axis = .horizontal
optionRow.spacing = AppSpacing.xs
optionRow.distribution = .fillEqually
customRow.addArrangedSubview(customField)
customRow.addArrangedSubview(customConfirmButton)
customRow.axis = .horizontal
customRow.spacing = AppSpacing.xs
customRow.alignment = .fill
customRow.isHidden = true
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(optionRow)
stackView.addArrangedSubview(customRow)
stackView.axis = .vertical
stackView.spacing = AppSpacing.xs
addSubview(stackView)
stackView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
optionRow.snp.makeConstraints { make in
make.height.equalTo(42)
}
customField.snp.makeConstraints { make in
make.height.equalTo(46)
}
customConfirmButton.snp.makeConstraints { make in
make.width.equalTo(72)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(urgentHour: Int) {
let isCustom = urgentHour > 0 && urgentHour != 2
styleOption(noneButton, selected: urgentHour == 0)
styleOption(twoHourButton, selected: urgentHour == 2)
styleOption(customButton, selected: isCustom)
customRow.isHidden = !isCustom
if isCustom {
customField.text = "\(urgentHour)"
} else {
customField.text = ""
}
}
private func configureOptionButton(_ button: UIButton, title: String) {
button.setTitle(title, for: .normal)
button.titleLabel?.font = .app(.caption)
button.layer.cornerRadius = AppRadius.xs
button.clipsToBounds = true
}
private func styleOption(_ button: UIButton, selected: Bool) {
button.backgroundColor = selected ? AppColor.primary : AppColor.inputBackground
button.setTitleColor(selected ? .white : AppColor.textSecondary, for: .normal)
}
@objc private func noneTapped() { onUrgentHourChange?(0) }
@objc private func twoHourTapped() { onUrgentHourChange?(2) }
@objc private func customTapped() {
onUrgentHourChange?(1)
onCustomHourTextChange?("1")
}
@objc private func customFieldChanged() {
onCustomHourTextChange?(customField.text ?? "")
}
@objc private func customConfirmTapped() {
onCustomHourTextChange?(customField.text ?? "")
}
}
///
final class TaskOrderSelectButton: UIControl {
private let titleLabel = UILabel()
private let arrowView = UIImageView(image: UIImage(systemName: "chevron.down"))
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
layer.cornerRadius = AppRadius.sm
layer.borderWidth = 1
layer.borderColor = AppColor.border.cgColor
titleLabel.font = .app(.bodyMedium)
titleLabel.textColor = AppColor.textTabInactive
titleLabel.text = "关联订单(可选)"
arrowView.tintColor = AppColor.textTabInactive
addSubview(titleLabel)
addSubview(arrowView)
titleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(AppSpacing.sm)
make.centerY.equalToSuperview()
}
arrowView.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(AppSpacing.sm)
make.centerY.equalToSuperview()
}
snp.makeConstraints { make in make.height.equalTo(44) }
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(orderNumber: String?) {
if let orderNumber, !orderNumber.isEmpty {
titleLabel.text = orderNumber
titleLabel.textColor = AppColor.textPrimary
} else {
titleLabel.text = "关联订单(可选)"
titleLabel.textColor = AppColor.textTabInactive
}
}
}
///
final class TaskFileRemarkDialogView: KeyboardAvoidingDialogView {
var onCancel: (() -> Void)?
var onConfirm: (() -> Void)?
private let containerView = UIView()
private let titleLabel = UILabel()
private let previewView = UIImageView()
private let remarkTitleLabel = UILabel()
private let textView = UITextView()
private let placeholderLabel = UILabel()
private let countLabel = UILabel()
private let cancelButton = UIButton(type: .system)
private let confirmButton = UIButton(type: .system)
private let buttonRow = UIStackView()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = AppColor.overlayScrim
keyboardAvoidingContentView = containerView
containerView.backgroundColor = .white
containerView.layer.cornerRadius = AppRadius.lg
titleLabel.text = "请填写剪辑备注"
titleLabel.font = .app(.title)
titleLabel.textColor = AppColor.textPrimary
previewView.contentMode = .scaleAspectFill
previewView.clipsToBounds = true
previewView.layer.cornerRadius = AppRadius.lg
previewView.backgroundColor = AppColor.inputBackground
remarkTitleLabel.text = "备注信息"
remarkTitleLabel.font = .app(.caption)
remarkTitleLabel.textColor = AppColor.textPrimary
textView.font = .app(.body)
textView.textColor = AppColor.textPrimary
textView.layer.borderWidth = 1
textView.layer.borderColor = AppColor.border.cgColor
textView.layer.cornerRadius = AppRadius.sm
textView.delegate = self
textView.textContainerInset = UIEdgeInsets(top: AppSpacing.sm, left: AppSpacing.xs, bottom: AppSpacing.sm, right: AppSpacing.xs)
placeholderLabel.text = "请输入备注最多50字"
placeholderLabel.font = .app(.body)
placeholderLabel.textColor = AppColor.textTertiary
countLabel.font = .app(.caption)
countLabel.textColor = AppColor.textTertiary
countLabel.text = "0/50"
cancelButton.setTitle("取消", for: .normal)
cancelButton.setTitleColor(AppColor.textSecondary, for: .normal)
cancelButton.backgroundColor = AppColor.inputBackground
cancelButton.layer.cornerRadius = AppRadius.xs
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
confirmButton.setTitle("确定", for: .normal)
confirmButton.setTitleColor(.white, for: .normal)
confirmButton.backgroundColor = AppColor.primary
confirmButton.layer.cornerRadius = AppRadius.xs
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
buttonRow.axis = .horizontal
buttonRow.spacing = AppSpacing.xs
buttonRow.alignment = .center
buttonRow.distribution = .fill
buttonRow.addArrangedSubview(cancelButton)
buttonRow.addArrangedSubview(confirmButton)
addSubview(containerView)
containerView.addSubview(titleLabel)
containerView.addSubview(previewView)
containerView.addSubview(remarkTitleLabel)
containerView.addSubview(countLabel)
containerView.addSubview(textView)
textView.addSubview(placeholderLabel)
containerView.addSubview(buttonRow)
containerView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
keyboardAvoidingCenterYConstraint = make.centerY.equalToSuperview().constraint
make.leading.trailing.equalToSuperview().inset(AppSpacing.lg)
}
titleLabel.snp.makeConstraints { make in
make.top.leading.equalToSuperview().offset(AppSpacing.md)
}
previewView.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(AppSpacing.sm)
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
make.height.equalTo(previewView.snp.width)
}
remarkTitleLabel.snp.makeConstraints { make in
make.top.equalTo(previewView.snp.bottom).offset(AppSpacing.md)
make.leading.equalToSuperview().offset(AppSpacing.md)
}
countLabel.snp.makeConstraints { make in
make.centerY.equalTo(remarkTitleLabel)
make.trailing.equalToSuperview().inset(AppSpacing.md)
}
textView.snp.makeConstraints { make in
make.top.equalTo(remarkTitleLabel.snp.bottom).offset(AppSpacing.xs)
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
make.height.equalTo(80)
}
placeholderLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(AppSpacing.sm)
make.leading.equalToSuperview().offset(AppSpacing.sm + 3)
make.trailing.lessThanOrEqualToSuperview().inset(AppSpacing.sm)
}
buttonRow.snp.makeConstraints { make in
make.top.equalTo(textView.snp.bottom).offset(AppSpacing.md)
make.trailing.bottom.equalToSuperview().inset(AppSpacing.md)
}
cancelButton.snp.makeConstraints { make in
make.height.equalTo(36)
make.width.equalTo(72)
}
confirmButton.snp.makeConstraints { make in
make.height.equalTo(36)
make.width.equalTo(72)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(file: TaskFileItem, text: String) {
textView.text = text
countLabel.text = "\(text.count)/50"
placeholderLabel.isHidden = !text.isEmpty
let urlString = file.previewURLString
if let url = URL(string: urlString), !urlString.isEmpty {
previewView.kf.setImage(with: url)
} else {
previewView.image = UIImage(systemName: "photo")
previewView.tintColor = AppColor.textTertiary
}
}
@objc private func cancelTapped() { onCancel?() }
@objc private func confirmTapped() { onConfirm?() }
///
var inputText: String { textView.text ?? "" }
}
extension TaskFileRemarkDialogView: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
if textView.text.count > 50 {
textView.text = String(textView.text.prefix(50))
}
placeholderLabel.isHidden = !textView.text.isEmpty
countLabel.text = "\(textView.text.count)/50"
}
}
///
final class TaskBorderTextField: UIView {
let textField = UITextField()
init(placeholder: String) {
super.init(frame: .zero)
backgroundColor = .white
layer.cornerRadius = AppRadius.sm
layer.borderWidth = 1
layer.borderColor = AppColor.border.cgColor
textField.placeholder = placeholder
textField.font = .app(.body)
textField.textColor = AppColor.textPrimary
addSubview(textField)
textField.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 10, left: 12, bottom: 10, right: 12))
}
snp.makeConstraints { make in make.height.equalTo(44) }
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
///
final class TaskBorderTextView: UIView {
let textView = UITextView()
init(placeholder: String) {
super.init(frame: .zero)
backgroundColor = .white
layer.cornerRadius = AppRadius.sm
layer.borderWidth = 1
layer.borderColor = AppColor.border.cgColor
textView.font = .app(.body)
textView.textColor = AppColor.textPrimary
textView.text = placeholder
textView.textColor = AppColor.textTertiary
addSubview(textView)
textView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(AppSpacing.xs)
}
snp.makeConstraints { make in make.height.equalTo(80) }
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
/// Android `AddFileBottomModalForTask`
final class TaskUploadTypeSheetView: UIView {
var onChooseImage: (() -> Void)?
var onChooseVideo: (() -> Void)?
var onCancel: (() -> Void)?
private let dimmingView = UIView()
private let panelView = UIView()
private let titleLabel = UILabel()
private let imageItem = TaskUploadTypeItemView(
title: "上传图片",
image: UIImage(named: "icon_cloud_storage_add_photo") ?? UIImage(systemName: "photo")
)
private let videoItem = TaskUploadTypeItemView(
title: "上传视频",
image: UIImage(named: "icon_cloud_storage_add_video") ?? UIImage(systemName: "video")
)
private let dividerView = UIView()
private let cancelButton = UIButton(type: .system)
private var panelBottomConstraint: Constraint?
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
dimmingView.backgroundColor = AppColor.overlayScrim
dimmingView.alpha = 0
let tap = UITapGestureRecognizer(target: self, action: #selector(cancelTapped))
dimmingView.addGestureRecognizer(tap)
panelView.backgroundColor = .white
panelView.layer.cornerRadius = AppRadius.sheetTop
panelView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
panelView.clipsToBounds = true
titleLabel.text = "选择上传类型"
titleLabel.font = .app(.bodyMedium)
titleLabel.textColor = AppColor.textPrimary
titleLabel.textAlignment = .center
imageItem.addTarget(self, action: #selector(imageTapped), for: .touchUpInside)
videoItem.addTarget(self, action: #selector(videoTapped), for: .touchUpInside)
dividerView.backgroundColor = AppColor.border
cancelButton.setTitle("取消", for: .normal)
cancelButton.titleLabel?.font = .app(.bodyMedium)
cancelButton.setTitleColor(UIColor(hex: 0x4B5563), for: .normal)
cancelButton.backgroundColor = AppColor.inputBackground
cancelButton.layer.cornerRadius = AppRadius.lg
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
addSubview(dimmingView)
addSubview(panelView)
panelView.addSubview(titleLabel)
panelView.addSubview(imageItem)
panelView.addSubview(videoItem)
panelView.addSubview(dividerView)
panelView.addSubview(cancelButton)
dimmingView.snp.makeConstraints { make in make.edges.equalToSuperview() }
panelView.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
panelBottomConstraint = make.bottom.equalToSuperview().offset(260).constraint
}
titleLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(AppSpacing.md)
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
}
imageItem.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(AppSpacing.md)
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
make.height.equalTo(58)
}
videoItem.snp.makeConstraints { make in
make.top.equalTo(imageItem.snp.bottom).offset(AppSpacing.sm)
make.leading.trailing.equalTo(imageItem)
make.height.equalTo(imageItem)
}
dividerView.snp.makeConstraints { make in
make.top.equalTo(videoItem.snp.bottom).offset(AppSpacing.md)
make.leading.trailing.equalToSuperview()
make.height.equalTo(0.5)
}
cancelButton.snp.makeConstraints { make in
make.top.equalTo(dividerView.snp.bottom).offset(AppSpacing.md)
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
make.height.equalTo(48)
make.bottom.equalTo(safeAreaLayoutGuide).inset(AppSpacing.md)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
///
func show() {
layoutIfNeeded()
panelBottomConstraint?.update(offset: 0)
UIView.animate(withDuration: 0.22, delay: 0, options: [.curveEaseOut]) {
self.dimmingView.alpha = 1
self.layoutIfNeeded()
}
}
///
func dismiss(completion: (() -> Void)? = nil) {
panelBottomConstraint?.update(offset: panelView.bounds.height)
UIView.animate(withDuration: 0.18, delay: 0, options: [.curveEaseIn]) {
self.dimmingView.alpha = 0
self.layoutIfNeeded()
} completion: { _ in
completion?()
}
}
@objc private func imageTapped() {
onChooseImage?()
}
@objc private func videoTapped() {
onChooseVideo?()
}
@objc private func cancelTapped() {
onCancel?()
}
}
///
final class TaskUploadTypeItemView: UIControl {
private let iconView = UIImageView()
private let titleLabel = UILabel()
init(title: String, image: UIImage?) {
super.init(frame: .zero)
backgroundColor = .white
layer.cornerRadius = AppRadius.sm
layer.borderWidth = 1
layer.borderColor = AppColor.inputBackground.cgColor
iconView.image = image
iconView.contentMode = .scaleAspectFit
iconView.tintColor = AppColor.primary
titleLabel.text = title
titleLabel.font = .app(.bodyMedium)
titleLabel.textColor = .black
addSubview(iconView)
addSubview(titleLabel)
iconView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(AppSpacing.md)
make.centerY.equalToSuperview()
make.width.height.equalTo(32)
}
titleLabel.snp.makeConstraints { make in
make.leading.equalTo(iconView.snp.trailing).offset(AppSpacing.sm)
make.centerY.equalToSuperview()
make.trailing.lessThanOrEqualToSuperview().inset(AppSpacing.md)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}