622 lines
26 KiB
Swift
622 lines
26 KiB
Swift
//
|
||
// WiredCameraTransferViewController.swift
|
||
// suixinkan
|
||
//
|
||
|
||
import Kingfisher
|
||
import SnapKit
|
||
import UIKit
|
||
|
||
/// 有线相机传输页,对齐 Android OTG 页面并接入真实 ImageCaptureCore 传输。
|
||
final class WiredCameraTransferViewController: BaseViewController {
|
||
private let viewModel: WiredCameraTransferViewModel
|
||
|
||
private let headerView = UIView()
|
||
private let backButton = UIButton(type: .system)
|
||
private let titleLabel = UILabel()
|
||
private let phoneLabel = UILabel()
|
||
private let statusCard = UIView()
|
||
private let storagePanel = UIView()
|
||
private let storageTitleLabel = UILabel()
|
||
private let storageValueLabel = UILabel()
|
||
private let statusLabel = UILabel()
|
||
private let refreshButton = UIButton(type: .system)
|
||
private let helpLabel = UILabel()
|
||
private let chipsStack = UIStackView()
|
||
private let retouchButton = UIButton(type: .system)
|
||
private let formatButton = UIButton(type: .system)
|
||
private let modeButton = UIButton(type: .system)
|
||
private let statsCard = UIStackView()
|
||
private let emptyLabel = UILabel()
|
||
private var collectionView: UICollectionView!
|
||
private var dataSource: UICollectionViewDiffableDataSource<Int, TravelAlbumOTGPhotoItem>!
|
||
private let bottomBar = UIView()
|
||
private let batchButton = UIButton(type: .system)
|
||
private let historyImportButton = UIButton(type: .system)
|
||
private let specifyButton = UIButton(type: .system)
|
||
|
||
init(viewModel: WiredCameraTransferViewModel) {
|
||
self.viewModel = viewModel
|
||
super.init(nibName: nil, bundle: nil)
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
override func setupNavigationBar() {
|
||
navigationController?.setNavigationBarHidden(true, animated: false)
|
||
}
|
||
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
applyViewModel()
|
||
}
|
||
|
||
override func viewWillAppear(_ animated: Bool) {
|
||
super.viewWillAppear(animated)
|
||
viewModel.start()
|
||
navigationController?.setNavigationBarHidden(true, animated: animated)
|
||
}
|
||
|
||
override func viewWillDisappear(_ animated: Bool) {
|
||
super.viewWillDisappear(animated)
|
||
viewModel.stop()
|
||
navigationController?.setNavigationBarHidden(false, animated: animated)
|
||
}
|
||
|
||
override func setupUI() {
|
||
view.backgroundColor = AppColor.pageBackground
|
||
headerView.backgroundColor = AppColor.primary
|
||
backButton.setImage(UIImage(systemName: "chevron.left"), for: .normal)
|
||
backButton.tintColor = .white
|
||
titleLabel.text = viewModel.albumTitle
|
||
titleLabel.font = .systemFont(ofSize: 20, weight: .bold)
|
||
titleLabel.textColor = .white
|
||
phoneLabel.text = viewModel.headerPhone
|
||
phoneLabel.font = .systemFont(ofSize: 13)
|
||
phoneLabel.textColor = UIColor.white.withAlphaComponent(0.92)
|
||
|
||
statusCard.backgroundColor = .white
|
||
statusCard.layer.cornerRadius = 12
|
||
statusCard.layer.shadowColor = UIColor.black.withAlphaComponent(0.10).cgColor
|
||
statusCard.layer.shadowOpacity = 1
|
||
statusCard.layer.shadowRadius = 8
|
||
statusCard.layer.shadowOffset = CGSize(width: 0, height: 3)
|
||
|
||
storagePanel.layer.cornerRadius = 12
|
||
storagePanel.layer.borderColor = AppColor.border.cgColor
|
||
storagePanel.layer.borderWidth = 3
|
||
storageTitleLabel.font = .systemFont(ofSize: 11, weight: .medium)
|
||
storageTitleLabel.textColor = AppColor.textPrimary
|
||
storageTitleLabel.textAlignment = .center
|
||
storageValueLabel.font = .systemFont(ofSize: 10)
|
||
storageValueLabel.textColor = AppColor.primary
|
||
storageValueLabel.textAlignment = .center
|
||
|
||
statusLabel.font = .systemFont(ofSize: 11, weight: .medium)
|
||
statusLabel.layer.cornerRadius = 4
|
||
statusLabel.clipsToBounds = true
|
||
statusLabel.textAlignment = .center
|
||
refreshButton.setTitleColor(.white, for: .normal)
|
||
refreshButton.titleLabel?.font = .systemFont(ofSize: 12)
|
||
refreshButton.backgroundColor = AppColor.primary
|
||
refreshButton.layer.cornerRadius = 8
|
||
helpLabel.font = .systemFont(ofSize: 10)
|
||
helpLabel.numberOfLines = 3
|
||
helpLabel.isUserInteractionEnabled = true
|
||
|
||
chipsStack.axis = .horizontal
|
||
chipsStack.spacing = 6
|
||
[retouchButton, formatButton, modeButton].forEach {
|
||
configureChipButton($0)
|
||
chipsStack.addArrangedSubview($0)
|
||
}
|
||
|
||
statsCard.axis = .horizontal
|
||
statsCard.distribution = .fillEqually
|
||
|
||
collectionView = UICollectionView(frame: .zero, collectionViewLayout: makeLayout())
|
||
collectionView.backgroundColor = AppColor.pageBackground
|
||
collectionView.delegate = self
|
||
collectionView.register(WiredTransferPhotoCell.self, forCellWithReuseIdentifier: WiredTransferPhotoCell.reuseIdentifier)
|
||
dataSource = UICollectionViewDiffableDataSource<Int, TravelAlbumOTGPhotoItem>(collectionView: collectionView) {
|
||
[weak self] collectionView, indexPath, item in
|
||
let cell = collectionView.dequeueReusableCell(
|
||
withReuseIdentifier: WiredTransferPhotoCell.reuseIdentifier,
|
||
for: indexPath
|
||
) as! WiredTransferPhotoCell
|
||
let selected = self?.viewModel.selectedPhotoIds.contains(item.id) == true
|
||
cell.apply(item: item, selectionMode: self?.viewModel.selectUploadMode == true, selected: selected)
|
||
cell.onRetry = { [weak self] in self?.viewModel.retryPhoto(photoId: item.id) }
|
||
cell.onDelete = { [weak self] in self?.confirmDeletePhoto(item.id) }
|
||
return cell
|
||
}
|
||
|
||
emptyLabel.text = "暂无照片"
|
||
emptyLabel.font = .systemFont(ofSize: 14)
|
||
emptyLabel.textColor = AppColor.textTertiary
|
||
emptyLabel.textAlignment = .center
|
||
|
||
bottomBar.backgroundColor = .white
|
||
configureBottomButton(batchButton, title: "批量上传", filled: true)
|
||
configureBottomButton(historyImportButton, title: "历史导入", filled: false)
|
||
configureBottomButton(specifyButton, title: "指定上传", filled: false)
|
||
|
||
view.addSubview(headerView)
|
||
headerView.addSubview(backButton)
|
||
headerView.addSubview(titleLabel)
|
||
headerView.addSubview(phoneLabel)
|
||
view.addSubview(statusCard)
|
||
statusCard.addSubview(storagePanel)
|
||
storagePanel.addSubview(storageTitleLabel)
|
||
storagePanel.addSubview(storageValueLabel)
|
||
statusCard.addSubview(statusLabel)
|
||
statusCard.addSubview(refreshButton)
|
||
statusCard.addSubview(helpLabel)
|
||
statusCard.addSubview(chipsStack)
|
||
statusCard.addSubview(statsCard)
|
||
view.addSubview(collectionView)
|
||
view.addSubview(emptyLabel)
|
||
view.addSubview(bottomBar)
|
||
bottomBar.addSubview(batchButton)
|
||
bottomBar.addSubview(historyImportButton)
|
||
bottomBar.addSubview(specifyButton)
|
||
}
|
||
|
||
override func setupConstraints() {
|
||
headerView.snp.makeConstraints { make in
|
||
make.top.leading.trailing.equalToSuperview()
|
||
make.height.equalTo(118)
|
||
}
|
||
backButton.snp.makeConstraints { make in
|
||
make.leading.equalToSuperview().offset(8)
|
||
make.bottom.equalToSuperview().offset(-28)
|
||
make.size.equalTo(44)
|
||
}
|
||
titleLabel.snp.makeConstraints { make in
|
||
make.leading.equalTo(backButton.snp.trailing).offset(4)
|
||
make.trailing.equalToSuperview().offset(-16)
|
||
make.top.equalTo(backButton).offset(4)
|
||
}
|
||
phoneLabel.snp.makeConstraints { make in
|
||
make.top.equalTo(titleLabel.snp.bottom).offset(2)
|
||
make.leading.trailing.equalTo(titleLabel)
|
||
}
|
||
statusCard.snp.makeConstraints { make in
|
||
make.top.equalTo(headerView.snp.bottom).offset(-10)
|
||
make.leading.trailing.equalToSuperview().inset(16)
|
||
}
|
||
storagePanel.snp.makeConstraints { make in
|
||
make.top.leading.equalToSuperview().offset(14)
|
||
make.width.greaterThanOrEqualTo(76)
|
||
make.height.equalTo(76)
|
||
}
|
||
storageTitleLabel.snp.makeConstraints { make in
|
||
make.top.equalToSuperview().offset(10)
|
||
make.leading.trailing.equalToSuperview().inset(8)
|
||
}
|
||
storageValueLabel.snp.makeConstraints { make in
|
||
make.top.equalTo(storageTitleLabel.snp.bottom).offset(6)
|
||
make.leading.trailing.equalTo(storageTitleLabel)
|
||
}
|
||
statusLabel.snp.makeConstraints { make in
|
||
make.top.equalToSuperview().offset(16)
|
||
make.leading.equalTo(storagePanel.snp.trailing).offset(10)
|
||
make.height.equalTo(22)
|
||
make.width.greaterThanOrEqualTo(84)
|
||
}
|
||
refreshButton.snp.makeConstraints { make in
|
||
make.centerY.equalTo(statusLabel)
|
||
make.trailing.equalToSuperview().offset(-14)
|
||
make.height.equalTo(34)
|
||
make.width.greaterThanOrEqualTo(72)
|
||
}
|
||
helpLabel.snp.makeConstraints { make in
|
||
make.top.equalTo(statusLabel.snp.bottom).offset(4)
|
||
make.leading.equalTo(statusLabel)
|
||
make.trailing.equalTo(refreshButton)
|
||
}
|
||
chipsStack.snp.makeConstraints { make in
|
||
make.top.equalTo(storagePanel.snp.bottom).offset(12)
|
||
make.leading.trailing.equalToSuperview().inset(14)
|
||
make.height.equalTo(34)
|
||
}
|
||
statsCard.snp.makeConstraints { make in
|
||
make.top.equalTo(chipsStack.snp.bottom).offset(14)
|
||
make.leading.trailing.bottom.equalToSuperview()
|
||
make.height.equalTo(68)
|
||
}
|
||
bottomBar.snp.makeConstraints { make in
|
||
make.leading.trailing.bottom.equalToSuperview()
|
||
}
|
||
batchButton.snp.makeConstraints { make in
|
||
make.top.equalToSuperview().offset(12)
|
||
make.leading.equalToSuperview().offset(16)
|
||
make.height.equalTo(44)
|
||
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-12)
|
||
}
|
||
historyImportButton.snp.makeConstraints { make in
|
||
make.top.height.width.equalTo(batchButton)
|
||
make.leading.equalTo(batchButton.snp.trailing).offset(8)
|
||
}
|
||
specifyButton.snp.makeConstraints { make in
|
||
make.top.height.width.equalTo(batchButton)
|
||
make.leading.equalTo(historyImportButton.snp.trailing).offset(8)
|
||
make.trailing.equalToSuperview().offset(-16)
|
||
}
|
||
collectionView.snp.makeConstraints { make in
|
||
make.top.equalTo(statusCard.snp.bottom).offset(14)
|
||
make.leading.trailing.equalToSuperview().inset(16)
|
||
make.bottom.equalTo(bottomBar.snp.top).offset(-10)
|
||
}
|
||
emptyLabel.snp.makeConstraints { make in
|
||
make.top.equalTo(statusCard.snp.bottom)
|
||
make.leading.trailing.equalToSuperview()
|
||
make.bottom.equalTo(bottomBar.snp.top)
|
||
}
|
||
}
|
||
|
||
override func bindActions() {
|
||
viewModel.onStateChange = { [weak self] in
|
||
Task { @MainActor in self?.applyViewModel() }
|
||
}
|
||
viewModel.onShowMessage = { [weak self] message in
|
||
Task { @MainActor in self?.showToast(message) }
|
||
}
|
||
viewModel.onLiveShotSaved = { [weak self] filename in
|
||
Task { @MainActor in self?.showToast("已保存 \(filename)") }
|
||
}
|
||
backButton.addTarget(self, action: #selector(backTapped), for: .touchUpInside)
|
||
refreshButton.addTarget(self, action: #selector(refreshTapped), for: .touchUpInside)
|
||
batchButton.addTarget(self, action: #selector(batchTapped), for: .touchUpInside)
|
||
historyImportButton.addTarget(self, action: #selector(historyImportTapped), for: .touchUpInside)
|
||
specifyButton.addTarget(self, action: #selector(specifyTapped), for: .touchUpInside)
|
||
formatButton.menu = UIMenu(children: TravelAlbumOTGPhotoFormatOption.allCases.map { option in
|
||
UIAction(title: option.rawValue) { [weak self] _ in self?.viewModel.selectPhotoFormat(option) }
|
||
})
|
||
formatButton.showsMenuAsPrimaryAction = true
|
||
modeButton.menu = UIMenu(children: ["边拍边传", "拍后传输"].map { option in
|
||
UIAction(title: option) { [weak self] _ in self?.viewModel.selectTransferMode(option) }
|
||
})
|
||
modeButton.showsMenuAsPrimaryAction = true
|
||
helpLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(helpTapped)))
|
||
}
|
||
|
||
@MainActor
|
||
private func applyViewModel() {
|
||
storageTitleLabel.text = viewModel.deviceModelName
|
||
storageValueLabel.text = viewModel.availableStorageText
|
||
statusLabel.text = viewModel.cameraStatusText
|
||
let isFailed = viewModel.cameraStatusText.contains("失败") || viewModel.cameraStatusText.contains("未连接")
|
||
statusLabel.textColor = isFailed ? AppColor.danger : AppColor.primary
|
||
statusLabel.backgroundColor = (isFailed ? AppColor.danger : AppColor.primary).withAlphaComponent(0.10)
|
||
refreshButton.setTitle(viewModel.actionButtonText, for: .normal)
|
||
|
||
retouchButton.setTitle(viewModel.retouchOption, for: .normal)
|
||
formatButton.setTitle(viewModel.photoFormatOption.rawValue, for: .normal)
|
||
modeButton.setTitle(viewModel.transferModeOption, for: .normal)
|
||
helpLabel.attributedText = helpText(viewModel.sonyMTPHint)
|
||
|
||
rebuildStats()
|
||
var snapshot = NSDiffableDataSourceSnapshot<Int, TravelAlbumOTGPhotoItem>()
|
||
snapshot.appendSections([0])
|
||
snapshot.appendItems(viewModel.filteredPhotos())
|
||
dataSource.apply(snapshot, animatingDifferences: true)
|
||
|
||
emptyLabel.isHidden = !viewModel.filteredPhotos().isEmpty
|
||
collectionView.isHidden = viewModel.filteredPhotos().isEmpty
|
||
if viewModel.selectUploadMode {
|
||
let count = viewModel.selectedPhotoIds.count
|
||
batchButton.setTitle(count > 0 ? "上传选中(\(count))" : "取消", for: .normal)
|
||
} else {
|
||
batchButton.setTitle("批量上传", for: .normal)
|
||
}
|
||
historyImportButton.setTitle("历史导入", for: .normal)
|
||
}
|
||
|
||
private func rebuildStats() {
|
||
statsCard.arrangedSubviews.forEach { view in
|
||
statsCard.removeArrangedSubview(view)
|
||
view.removeFromSuperview()
|
||
}
|
||
for tab in TravelAlbumOTGTransferTab.allCases {
|
||
let selected = viewModel.selectedTab == tab
|
||
let button = makeStatButton(
|
||
title: tab.title,
|
||
count: viewModel.tabCounts[tab.rawValue],
|
||
selected: selected,
|
||
danger: tab == .failed
|
||
)
|
||
button.tag = tab.rawValue
|
||
button.addTarget(self, action: #selector(tabTapped(_:)), for: .touchUpInside)
|
||
statsCard.addArrangedSubview(button)
|
||
}
|
||
}
|
||
|
||
private func configureChipButton(_ button: UIButton) {
|
||
button.titleLabel?.font = .systemFont(ofSize: 11)
|
||
button.setTitleColor(AppColor.textTertiary, for: .normal)
|
||
button.backgroundColor = AppColor.pageBackground
|
||
button.layer.cornerRadius = 8
|
||
button.layer.borderWidth = 1
|
||
button.layer.borderColor = AppColor.border.cgColor
|
||
button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10)
|
||
}
|
||
|
||
private func makeStatButton(title: String, count: Int, selected: Bool, danger: Bool) -> UIButton {
|
||
var config = UIButton.Configuration.plain()
|
||
config.title = "\(count)\n\(title)"
|
||
config.titleAlignment = .center
|
||
config.baseForegroundColor = selected ? AppColor.primary : (danger ? AppColor.danger : AppColor.textPrimary)
|
||
config.background.backgroundColor = .white
|
||
let button = UIButton(configuration: config)
|
||
button.titleLabel?.numberOfLines = 2
|
||
button.titleLabel?.font = .systemFont(ofSize: 14, weight: selected ? .semibold : .regular)
|
||
return button
|
||
}
|
||
|
||
private func configureBottomButton(_ button: UIButton, title: String, filled: Bool) {
|
||
button.setTitle(title, for: .normal)
|
||
button.titleLabel?.font = .systemFont(ofSize: 15)
|
||
button.layer.cornerRadius = 10
|
||
if filled {
|
||
button.backgroundColor = AppColor.primary
|
||
button.setTitleColor(.white, for: .normal)
|
||
} else {
|
||
button.backgroundColor = .white
|
||
button.setTitleColor(AppColor.primary, for: .normal)
|
||
button.layer.borderWidth = 1
|
||
button.layer.borderColor = AppColor.primary.cgColor
|
||
}
|
||
}
|
||
|
||
private func makeLayout() -> UICollectionViewCompositionalLayout {
|
||
UICollectionViewCompositionalLayout { _, environment in
|
||
let spacing: CGFloat = 8
|
||
let width = (environment.container.effectiveContentSize.width - spacing * 2) / 3
|
||
let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(width), heightDimension: .absolute(width + 58))
|
||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(width + 58))
|
||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, repeatingSubitem: item, count: 3)
|
||
group.interItemSpacing = .fixed(spacing)
|
||
let section = NSCollectionLayoutSection(group: group)
|
||
section.interGroupSpacing = 12
|
||
return section
|
||
}
|
||
}
|
||
|
||
private func helpText(_ sonyHint: String?) -> NSAttributedString {
|
||
let text = NSMutableAttributedString(
|
||
string: sonyHint ?? "未连接,查看",
|
||
attributes: [.foregroundColor: AppColor.textSecondary]
|
||
)
|
||
if sonyHint == nil {
|
||
text.append(NSAttributedString(string: "《帮助文档》", attributes: [.foregroundColor: AppColor.primary]))
|
||
}
|
||
return text
|
||
}
|
||
|
||
private func confirmDeletePhoto(_ photoId: String) {
|
||
let alert = UIAlertController(title: "删除照片", message: "仅删除本地 OTG 缓存,不会删除服务端已上传素材。", preferredStyle: .alert)
|
||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||
alert.addAction(UIAlertAction(title: "删除", style: .destructive) { [weak self] _ in
|
||
self?.viewModel.deletePhoto(photoId: photoId)
|
||
})
|
||
present(alert, animated: true)
|
||
}
|
||
|
||
@objc private func backTapped() {
|
||
navigationController?.popViewController(animated: true)
|
||
}
|
||
|
||
@objc private func refreshTapped() {
|
||
viewModel.refreshCameraFiles()
|
||
}
|
||
|
||
@objc private func batchTapped() {
|
||
viewModel.onBatchUploadButtonClick()
|
||
}
|
||
|
||
@objc private func historyImportTapped() {
|
||
guard let importViewModel = viewModel.makeCameraImportViewModel() else { return }
|
||
viewModel.pauseLiveTransferForHistoricalImport()
|
||
let importController = TravelAlbumCameraImportViewController(viewModel: importViewModel)
|
||
importController.onImportFinished = { [weak self] importedCount, failedCount in
|
||
guard let self else { return }
|
||
self.viewModel.reloadLocalPhotos()
|
||
self.viewModel.start()
|
||
if failedCount > 0 {
|
||
self.showToast("已导入 \(importedCount) 张,\(failedCount) 张失败")
|
||
} else {
|
||
self.showToast("已导入 \(importedCount) 张照片")
|
||
}
|
||
}
|
||
importController.onImportCancelled = { [weak self] in
|
||
self?.viewModel.start()
|
||
}
|
||
let navigationController = UINavigationController(rootViewController: importController)
|
||
navigationController.modalPresentationStyle = .pageSheet
|
||
present(navigationController, animated: true)
|
||
}
|
||
|
||
@objc private func specifyTapped() {
|
||
viewModel.onSpecifyUploadButtonClick()
|
||
}
|
||
|
||
@objc private func tabTapped(_ sender: UIButton) {
|
||
guard let tab = TravelAlbumOTGTransferTab(rawValue: sender.tag) else { return }
|
||
viewModel.selectTab(tab)
|
||
}
|
||
|
||
@objc private func helpTapped() {
|
||
guard let url = URL(string: "https://sharesky.feishu.cn/wiki/CaAhwl3Gnin4Kqk0BVocK3SGnab?from=from_copylink") else { return }
|
||
UIApplication.shared.open(url)
|
||
}
|
||
}
|
||
|
||
extension WiredCameraTransferViewController: UICollectionViewDelegate {
|
||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
|
||
if viewModel.selectUploadMode {
|
||
viewModel.toggleTransferPhotoSelection(photoId: item.id)
|
||
} else if item.status == .failed {
|
||
viewModel.retryPhoto(photoId: item.id)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 有线传输照片宫格 Cell。
|
||
private final class WiredTransferPhotoCell: UICollectionViewCell {
|
||
static let reuseIdentifier = "WiredTransferPhotoCell"
|
||
|
||
private let imageView = UIImageView()
|
||
private let statusLabel = UILabel()
|
||
private let titleLabel = UILabel()
|
||
private let sizeLabel = UILabel()
|
||
private let progressView = UIProgressView(progressViewStyle: .default)
|
||
private let retryButton = UIButton(type: .system)
|
||
private let deleteButton = UIButton(type: .system)
|
||
private let selectionOverlay = UIImageView()
|
||
|
||
var onRetry: (() -> Void)?
|
||
var onDelete: (() -> Void)?
|
||
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
setupUI()
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
override func prepareForReuse() {
|
||
super.prepareForReuse()
|
||
imageView.kf.cancelDownloadTask()
|
||
imageView.image = nil
|
||
onRetry = nil
|
||
onDelete = nil
|
||
}
|
||
|
||
func apply(item: TravelAlbumOTGPhotoItem, selectionMode: Bool, selected: Bool) {
|
||
if let url = item.thumbnailURL {
|
||
imageView.kf.setImage(with: url, placeholder: UIImage(systemName: "photo"))
|
||
} else {
|
||
imageView.image = UIImage(systemName: "photo")
|
||
imageView.tintColor = AppColor.textTertiary
|
||
}
|
||
titleLabel.text = item.fileName
|
||
sizeLabel.text = item.fileSizeText
|
||
statusLabel.text = statusText(item.status, progress: item.progress)
|
||
statusLabel.backgroundColor = statusColor(item.status).withAlphaComponent(0.90)
|
||
progressView.isHidden = item.status != .uploading && item.status != .transferring
|
||
progressView.progress = Float(item.progress) / 100.0
|
||
retryButton.isHidden = item.status != .failed
|
||
deleteButton.isHidden = selectionMode
|
||
selectionOverlay.isHidden = !selectionMode
|
||
selectionOverlay.image = UIImage(systemName: selected ? "checkmark.circle.fill" : "circle")
|
||
selectionOverlay.tintColor = selected ? AppColor.primary : .white
|
||
}
|
||
|
||
private func setupUI() {
|
||
contentView.backgroundColor = .white
|
||
contentView.layer.cornerRadius = 8
|
||
contentView.clipsToBounds = true
|
||
imageView.contentMode = .scaleAspectFill
|
||
imageView.clipsToBounds = true
|
||
statusLabel.font = .systemFont(ofSize: 10, weight: .medium)
|
||
statusLabel.textColor = .white
|
||
statusLabel.textAlignment = .center
|
||
statusLabel.layer.cornerRadius = 4
|
||
statusLabel.clipsToBounds = true
|
||
titleLabel.font = .systemFont(ofSize: 11, weight: .medium)
|
||
titleLabel.textColor = AppColor.textPrimary
|
||
titleLabel.lineBreakMode = .byTruncatingMiddle
|
||
sizeLabel.font = .systemFont(ofSize: 10)
|
||
sizeLabel.textColor = AppColor.textTertiary
|
||
retryButton.setImage(UIImage(systemName: "arrow.clockwise"), for: .normal)
|
||
retryButton.tintColor = AppColor.danger
|
||
deleteButton.setImage(UIImage(systemName: "trash"), for: .normal)
|
||
deleteButton.tintColor = AppColor.textTertiary
|
||
selectionOverlay.contentMode = .scaleAspectFit
|
||
|
||
contentView.addSubview(imageView)
|
||
imageView.addSubview(statusLabel)
|
||
imageView.addSubview(selectionOverlay)
|
||
contentView.addSubview(titleLabel)
|
||
contentView.addSubview(sizeLabel)
|
||
contentView.addSubview(progressView)
|
||
contentView.addSubview(retryButton)
|
||
contentView.addSubview(deleteButton)
|
||
|
||
imageView.snp.makeConstraints { make in
|
||
make.top.leading.trailing.equalToSuperview()
|
||
make.height.equalTo(contentView.snp.width)
|
||
}
|
||
statusLabel.snp.makeConstraints { make in
|
||
make.top.leading.equalToSuperview().offset(5)
|
||
make.height.equalTo(18)
|
||
make.width.greaterThanOrEqualTo(44)
|
||
}
|
||
selectionOverlay.snp.makeConstraints { make in
|
||
make.top.trailing.equalToSuperview().inset(6)
|
||
make.size.equalTo(22)
|
||
}
|
||
titleLabel.snp.makeConstraints { make in
|
||
make.top.equalTo(imageView.snp.bottom).offset(5)
|
||
make.leading.equalToSuperview().offset(6)
|
||
make.trailing.equalTo(deleteButton.snp.leading).offset(-4)
|
||
}
|
||
sizeLabel.snp.makeConstraints { make in
|
||
make.top.equalTo(titleLabel.snp.bottom).offset(2)
|
||
make.leading.trailing.equalTo(titleLabel)
|
||
}
|
||
progressView.snp.makeConstraints { make in
|
||
make.leading.trailing.bottom.equalToSuperview().inset(6)
|
||
}
|
||
retryButton.snp.makeConstraints { make in
|
||
make.centerY.equalTo(titleLabel)
|
||
make.trailing.equalToSuperview().offset(-2)
|
||
make.size.equalTo(26)
|
||
}
|
||
deleteButton.snp.makeConstraints { make in
|
||
make.centerY.equalTo(titleLabel)
|
||
make.trailing.equalToSuperview().offset(-2)
|
||
make.size.equalTo(26)
|
||
}
|
||
|
||
retryButton.addTarget(self, action: #selector(retryTapped), for: .touchUpInside)
|
||
deleteButton.addTarget(self, action: #selector(deleteTapped), for: .touchUpInside)
|
||
}
|
||
|
||
private func statusText(_ status: TravelAlbumOTGUploadStatus, progress: Int) -> String {
|
||
switch status {
|
||
case .pending: return "待上传"
|
||
case .transferring: return "传输\(progress)%"
|
||
case .uploading: return "上传\(progress)%"
|
||
case .uploaded: return "已上传"
|
||
case .failed: return "失败"
|
||
}
|
||
}
|
||
|
||
private func statusColor(_ status: TravelAlbumOTGUploadStatus) -> UIColor {
|
||
switch status {
|
||
case .pending: return AppColor.textTertiary
|
||
case .transferring, .uploading: return AppColor.primary
|
||
case .uploaded: return UIColor(hex: 0x16A34A)
|
||
case .failed: return AppColor.danger
|
||
}
|
||
}
|
||
|
||
@objc private func retryTapped() {
|
||
onRetry?()
|
||
}
|
||
|
||
@objc private func deleteTapped() {
|
||
onDelete?()
|
||
}
|
||
}
|