701 lines
28 KiB
Swift
701 lines
28 KiB
Swift
//
|
||
// LiveAlbumViewControllers.swift
|
||
// suixinkan
|
||
//
|
||
|
||
import Kingfisher
|
||
import PhotosUI
|
||
import SnapKit
|
||
import UIKit
|
||
|
||
/// 直播相册列表页,对齐 Android `AliveAlbumScreen`。
|
||
final class LiveAlbumListViewController: BaseViewController, UITableViewDelegate {
|
||
private enum Section {
|
||
case main
|
||
}
|
||
|
||
private let viewModel = LiveAlbumListViewModel()
|
||
private let liveAPI: any LiveServing
|
||
private let filterCard = UIView()
|
||
private let startButton = UIButton(type: .system)
|
||
private let endButton = UIButton(type: .system)
|
||
private let tableView = UITableView(frame: .zero, style: .plain)
|
||
private let refreshControl = UIRefreshControl()
|
||
private let emptyLabel = UILabel()
|
||
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
|
||
private let bottomBar = UIView()
|
||
private let uploadButton = AppButton(title: "上传素材")
|
||
private var dataSource: UITableViewDiffableDataSource<Section, LiveAlbumFolder>!
|
||
|
||
/// 初始化直播相册列表页。
|
||
@MainActor
|
||
init(liveAPI: (any LiveServing)? = nil) {
|
||
self.liveAPI = liveAPI ?? NetworkServices.shared.liveAPI
|
||
super.init(nibName: nil, bundle: nil)
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
override func setupNavigationBar() {
|
||
title = "直播相册"
|
||
}
|
||
|
||
override func setupUI() {
|
||
view.backgroundColor = AppColor.pageBackground
|
||
filterCard.backgroundColor = .white
|
||
filterCard.layer.cornerRadius = 12
|
||
configureDateButton(startButton, title: "开始时间")
|
||
configureDateButton(endButton, title: "结束时间")
|
||
tableView.backgroundColor = .clear
|
||
tableView.separatorStyle = .none
|
||
tableView.delegate = self
|
||
tableView.refreshControl = refreshControl
|
||
tableView.register(LiveAlbumFolderCell.self, forCellReuseIdentifier: LiveAlbumFolderCell.reuseIdentifier)
|
||
tableView.contentInset = UIEdgeInsets(top: 12, left: 0, bottom: 12, right: 0)
|
||
dataSource = UITableViewDiffableDataSource<Section, LiveAlbumFolder>(tableView: tableView) { [weak self] tableView, indexPath, folder in
|
||
guard let cell = tableView.dequeueReusableCell(withIdentifier: LiveAlbumFolderCell.reuseIdentifier, for: indexPath) as? LiveAlbumFolderCell else {
|
||
return UITableViewCell()
|
||
}
|
||
cell.apply(folder)
|
||
cell.onDelete = { [weak self] in self?.confirmDelete(folder) }
|
||
cell.onImageSelected = { [weak self] index in
|
||
self?.navigationController?.pushViewController(
|
||
LiveAlbumPreviewViewController(folderId: folder.id, startIndex: index),
|
||
animated: true
|
||
)
|
||
}
|
||
return cell
|
||
}
|
||
emptyLabel.text = "暂无素材"
|
||
emptyLabel.font = .systemFont(ofSize: 14)
|
||
emptyLabel.textColor = UIColor(hex: 0x4B5563)
|
||
emptyLabel.isHidden = true
|
||
loadingIndicator.hidesWhenStopped = true
|
||
loadingIndicator.color = AppColor.primary
|
||
bottomBar.backgroundColor = .white
|
||
|
||
view.addSubview(filterCard)
|
||
filterCard.addSubview(startButton)
|
||
filterCard.addSubview(endButton)
|
||
view.addSubview(tableView)
|
||
view.addSubview(emptyLabel)
|
||
view.addSubview(loadingIndicator)
|
||
view.addSubview(bottomBar)
|
||
bottomBar.addSubview(uploadButton)
|
||
}
|
||
|
||
override func setupConstraints() {
|
||
filterCard.snp.makeConstraints { make in
|
||
make.top.equalTo(view.safeAreaLayoutGuide)
|
||
make.leading.trailing.equalToSuperview()
|
||
make.height.equalTo(59)
|
||
}
|
||
startButton.snp.makeConstraints { make in
|
||
make.leading.equalToSuperview().offset(12)
|
||
make.centerY.equalToSuperview()
|
||
make.height.equalTo(35)
|
||
}
|
||
endButton.snp.makeConstraints { make in
|
||
make.leading.equalTo(startButton.snp.trailing).offset(8)
|
||
make.trailing.equalToSuperview().inset(12)
|
||
make.centerY.width.height.equalTo(startButton)
|
||
}
|
||
bottomBar.snp.makeConstraints { make in
|
||
make.leading.trailing.bottom.equalToSuperview()
|
||
}
|
||
uploadButton.snp.makeConstraints { make in
|
||
make.top.equalToSuperview().offset(16)
|
||
make.leading.trailing.equalToSuperview().inset(16)
|
||
make.height.equalTo(48)
|
||
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(16)
|
||
}
|
||
tableView.snp.makeConstraints { make in
|
||
make.top.equalTo(filterCard.snp.bottom)
|
||
make.leading.trailing.equalToSuperview()
|
||
make.bottom.equalTo(bottomBar.snp.top)
|
||
}
|
||
emptyLabel.snp.makeConstraints { make in
|
||
make.center.equalTo(tableView)
|
||
}
|
||
loadingIndicator.snp.makeConstraints { make in
|
||
make.center.equalTo(tableView)
|
||
}
|
||
}
|
||
|
||
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) }
|
||
}
|
||
refreshControl.addTarget(self, action: #selector(refreshTriggered), for: .valueChanged)
|
||
startButton.addTarget(self, action: #selector(selectStartDate), for: .touchUpInside)
|
||
endButton.addTarget(self, action: #selector(selectEndDate), for: .touchUpInside)
|
||
uploadButton.addTarget(self, action: #selector(openUpload), for: .touchUpInside)
|
||
}
|
||
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
Task { await viewModel.loadInitial(api: liveAPI) }
|
||
}
|
||
|
||
override func viewWillAppear(_ animated: Bool) {
|
||
super.viewWillAppear(animated)
|
||
Task { await viewModel.refresh(api: liveAPI) }
|
||
}
|
||
|
||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||
guard let last = tableView.indexPathsForVisibleRows?.map(\.row).max() else { return }
|
||
Task { await viewModel.loadMoreIfNeeded(lastVisibleIndex: last, api: liveAPI) }
|
||
}
|
||
|
||
@objc private func refreshTriggered() {
|
||
Task { await viewModel.refresh(api: liveAPI) }
|
||
}
|
||
|
||
@objc private func selectStartDate() {
|
||
presentDatePicker(initialDate: viewModel.startDate ?? Date()) { [weak self] date in
|
||
guard let self else { return }
|
||
Task { await self.viewModel.setStartDate(date, api: self.liveAPI) }
|
||
}
|
||
}
|
||
|
||
@objc private func selectEndDate() {
|
||
presentDatePicker(initialDate: viewModel.endDate ?? Date()) { [weak self] date in
|
||
guard let self else { return }
|
||
Task { await self.viewModel.setEndDate(date, api: self.liveAPI) }
|
||
}
|
||
}
|
||
|
||
@objc private func openUpload() {
|
||
navigationController?.pushViewController(LiveAlbumAddViewController(), animated: true)
|
||
}
|
||
|
||
private func applyViewModel() {
|
||
let startText = LiveUIFormatter.date(viewModel.startDate)
|
||
let endText = LiveUIFormatter.date(viewModel.endDate)
|
||
setDateButtonTitle(startButton, text: startText.isEmpty ? "开始时间" : startText)
|
||
setDateButtonTitle(endButton, text: endText.isEmpty ? "结束时间" : endText)
|
||
if viewModel.isRefreshing {
|
||
if !refreshControl.isRefreshing { refreshControl.beginRefreshing() }
|
||
} else {
|
||
refreshControl.endRefreshing()
|
||
}
|
||
viewModel.isLoading && viewModel.folders.isEmpty ? loadingIndicator.startAnimating() : loadingIndicator.stopAnimating()
|
||
emptyLabel.isHidden = viewModel.isLoading || !viewModel.folders.isEmpty
|
||
var snapshot = NSDiffableDataSourceSnapshot<Section, LiveAlbumFolder>()
|
||
snapshot.appendSections([.main])
|
||
snapshot.appendItems(viewModel.folders)
|
||
dataSource.apply(snapshot, animatingDifferences: true)
|
||
}
|
||
|
||
private func confirmDelete(_ folder: LiveAlbumFolder) {
|
||
showAlert(title: "确认删除", message: "是否确认删除该相册?删除后将无法恢复。") { [weak self] in
|
||
guard let self else { return }
|
||
Task { await self.viewModel.deleteFolder(folder, api: self.liveAPI) }
|
||
}
|
||
}
|
||
|
||
private func configureDateButton(_ button: UIButton, title: String) {
|
||
button.backgroundColor = UIColor(hex: 0xF9FAFB)
|
||
button.layer.cornerRadius = 4
|
||
button.setTitleColor(AppColor.textPrimary, for: .normal)
|
||
button.titleLabel?.font = .systemFont(ofSize: 13)
|
||
button.setImage(UIImage(systemName: "calendar"), for: .normal)
|
||
button.tintColor = AppColor.textPrimary
|
||
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: -4, bottom: 0, right: 4)
|
||
setDateButtonTitle(button, text: title)
|
||
}
|
||
|
||
private func setDateButtonTitle(_ button: UIButton, text: String) {
|
||
button.setTitle(text, for: .normal)
|
||
}
|
||
|
||
private func presentDatePicker(initialDate: Date, onConfirm: @escaping (Date) -> Void) {
|
||
let alert = UIAlertController(title: nil, message: "\n\n\n\n\n\n\n\n", preferredStyle: .actionSheet)
|
||
let picker = UIDatePicker()
|
||
picker.datePickerMode = .date
|
||
picker.preferredDatePickerStyle = .wheels
|
||
picker.locale = Locale(identifier: "zh_CN")
|
||
picker.date = initialDate
|
||
alert.view.addSubview(picker)
|
||
picker.snp.makeConstraints { make in
|
||
make.top.equalToSuperview().offset(8)
|
||
make.leading.trailing.equalToSuperview().inset(8)
|
||
make.height.equalTo(216)
|
||
}
|
||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||
alert.addAction(UIAlertAction(title: "确定", style: .default) { _ in onConfirm(picker.date) })
|
||
present(alert, animated: true)
|
||
}
|
||
}
|
||
|
||
/// 直播相册文件夹 Cell,对齐 Android `AliveAlbumFolderView`。
|
||
private final class LiveAlbumFolderCell: UITableViewCell {
|
||
static let reuseIdentifier = "LiveAlbumFolderCell"
|
||
|
||
var onDelete: (() -> Void)?
|
||
var onImageSelected: ((Int) -> Void)?
|
||
|
||
private let container = UIView()
|
||
private let titleLabel = UILabel()
|
||
private let deleteButton = UIButton(type: .system)
|
||
private let gridStack = UIStackView()
|
||
private var imageViews: [LiveCoverImageView] = []
|
||
|
||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||
setup()
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
override func prepareForReuse() {
|
||
super.prepareForReuse()
|
||
onDelete = nil
|
||
onImageSelected = nil
|
||
imageViews.forEach { $0.removeFromSuperview() }
|
||
imageViews.removeAll()
|
||
gridStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||
}
|
||
|
||
/// 应用直播相册文件夹。
|
||
func apply(_ folder: LiveAlbumFolder) {
|
||
titleLabel.text = folder.displayName
|
||
buildGrid(items: folder.items)
|
||
}
|
||
|
||
private func setup() {
|
||
selectionStyle = .none
|
||
backgroundColor = .clear
|
||
container.backgroundColor = .white
|
||
titleLabel.font = .systemFont(ofSize: 16, weight: .medium)
|
||
titleLabel.textColor = AppColor.textPrimary
|
||
deleteButton.setImage(UIImage(systemName: "trash"), for: .normal)
|
||
deleteButton.tintColor = AppColor.danger
|
||
gridStack.axis = .vertical
|
||
gridStack.spacing = 12
|
||
|
||
contentView.addSubview(container)
|
||
container.addSubview(titleLabel)
|
||
container.addSubview(deleteButton)
|
||
container.addSubview(gridStack)
|
||
container.snp.makeConstraints { make in
|
||
make.top.bottom.equalToSuperview()
|
||
make.leading.trailing.equalToSuperview()
|
||
}
|
||
titleLabel.snp.makeConstraints { make in
|
||
make.top.leading.equalToSuperview().offset(16)
|
||
make.trailing.lessThanOrEqualTo(deleteButton.snp.leading).offset(-8)
|
||
}
|
||
deleteButton.snp.makeConstraints { make in
|
||
make.top.trailing.equalToSuperview().inset(8)
|
||
make.size.equalTo(44)
|
||
}
|
||
gridStack.snp.makeConstraints { make in
|
||
make.top.equalTo(titleLabel.snp.bottom).offset(12)
|
||
make.leading.trailing.equalToSuperview().inset(12)
|
||
make.bottom.equalToSuperview().inset(12)
|
||
}
|
||
deleteButton.addTarget(self, action: #selector(deleteTapped), for: .touchUpInside)
|
||
}
|
||
|
||
private func buildGrid(items: [LiveAlbumFile]) {
|
||
let rows = stride(from: 0, to: items.count, by: 2).map { start in
|
||
Array(items[start ..< min(start + 2, items.count)])
|
||
}
|
||
for (rowIndex, rowItems) in rows.enumerated() {
|
||
let rowStack = UIStackView()
|
||
rowStack.axis = .horizontal
|
||
rowStack.spacing = 12
|
||
rowStack.distribution = .fillEqually
|
||
gridStack.addArrangedSubview(rowStack)
|
||
for (columnIndex, item) in rowItems.enumerated() {
|
||
let imageView = LiveCoverImageView(cornerRadius: 12)
|
||
imageView.setURL(item.url)
|
||
imageView.isUserInteractionEnabled = true
|
||
imageView.tag = rowIndex * 2 + columnIndex
|
||
imageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(imageTapped(_:))))
|
||
rowStack.addArrangedSubview(imageView)
|
||
imageViews.append(imageView)
|
||
imageView.snp.makeConstraints { make in
|
||
make.height.equalTo(imageView.snp.width).multipliedBy(9.0 / 16.0)
|
||
}
|
||
}
|
||
if rowItems.count == 1 {
|
||
rowStack.addArrangedSubview(UIView())
|
||
}
|
||
}
|
||
}
|
||
|
||
@objc private func deleteTapped() {
|
||
onDelete?()
|
||
}
|
||
|
||
@objc private func imageTapped(_ recognizer: UITapGestureRecognizer) {
|
||
guard let view = recognizer.view else { return }
|
||
onImageSelected?(view.tag)
|
||
}
|
||
}
|
||
|
||
/// 上传直播相册素材页,对齐 Android `AliveAlbumAddScreen`。
|
||
final class LiveAlbumAddViewController: BaseViewController, PHPickerViewControllerDelegate {
|
||
private let viewModel = LiveAlbumAddViewModel()
|
||
private let liveAPI: any LiveServing
|
||
private let uploader: any LiveOSSUploading
|
||
private let scrollView = UIScrollView()
|
||
private let cardView = UIView()
|
||
private let stackView = UIStackView()
|
||
private let titleField = UITextField()
|
||
private let uploadView = LiveDashedUploadView(title: "点击上传图片", subtitle: "支持 jpg、png、mp4 格式")
|
||
private let filesStack = UIStackView()
|
||
private let bottomBar = UIView()
|
||
private let confirmButton = AppButton(title: "确认")
|
||
|
||
/// 初始化上传直播相册素材页。
|
||
@MainActor
|
||
init(
|
||
liveAPI: (any LiveServing)? = nil,
|
||
uploader: (any LiveOSSUploading)? = nil
|
||
) {
|
||
self.liveAPI = liveAPI ?? NetworkServices.shared.liveAPI
|
||
self.uploader = uploader ?? NetworkServices.shared.ossUploadService
|
||
super.init(nibName: nil, bundle: nil)
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
override func setupNavigationBar() {
|
||
title = "上传素材"
|
||
}
|
||
|
||
override func setupUI() {
|
||
view.backgroundColor = AppColor.pageBackground
|
||
cardView.backgroundColor = .white
|
||
cardView.layer.cornerRadius = 12
|
||
stackView.axis = .vertical
|
||
stackView.spacing = 8
|
||
filesStack.axis = .vertical
|
||
filesStack.spacing = 12
|
||
titleField.placeholder = "请输入作品名称"
|
||
titleField.font = .systemFont(ofSize: 14)
|
||
titleField.layer.borderWidth = 1
|
||
titleField.layer.borderColor = AppColor.border.cgColor
|
||
titleField.layer.cornerRadius = 8
|
||
titleField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 1))
|
||
titleField.leftViewMode = .always
|
||
bottomBar.backgroundColor = .white
|
||
|
||
view.addSubview(scrollView)
|
||
scrollView.addSubview(cardView)
|
||
cardView.addSubview(stackView)
|
||
addRequiredTitle("直播标题", to: stackView)
|
||
stackView.addArrangedSubview(titleField)
|
||
addRequiredTitle("封面图片", suffix: "(单次最多可上传 20 张图片)", to: stackView)
|
||
stackView.addArrangedSubview(uploadView)
|
||
stackView.addArrangedSubview(filesStack)
|
||
view.addSubview(bottomBar)
|
||
bottomBar.addSubview(confirmButton)
|
||
}
|
||
|
||
override func setupConstraints() {
|
||
bottomBar.snp.makeConstraints { make in
|
||
make.leading.trailing.bottom.equalToSuperview()
|
||
}
|
||
confirmButton.snp.makeConstraints { make in
|
||
make.top.equalToSuperview().offset(16)
|
||
make.leading.trailing.equalToSuperview().inset(16)
|
||
make.height.equalTo(48)
|
||
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(16)
|
||
}
|
||
scrollView.snp.makeConstraints { make in
|
||
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
|
||
make.bottom.equalTo(bottomBar.snp.top)
|
||
}
|
||
cardView.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview().inset(16)
|
||
make.width.equalTo(scrollView).offset(-32)
|
||
}
|
||
stackView.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview().inset(16)
|
||
}
|
||
titleField.snp.makeConstraints { make in
|
||
make.height.equalTo(44)
|
||
}
|
||
uploadView.snp.makeConstraints { make in
|
||
make.height.equalTo(120)
|
||
}
|
||
}
|
||
|
||
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.onCreateSuccess = { [weak self] in
|
||
Task { @MainActor in self?.navigationController?.popViewController(animated: true) }
|
||
}
|
||
titleField.addTarget(self, action: #selector(titleChanged), for: .editingChanged)
|
||
uploadView.addTarget(self, action: #selector(pickImage), for: .touchUpInside)
|
||
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
|
||
}
|
||
|
||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||
picker.dismiss(animated: true)
|
||
guard let result = results.first else { return }
|
||
Task {
|
||
do {
|
||
let media = try await LivePhotoPickerLoader.loadImage(from: result)
|
||
await viewModel.addLocalMedia(media, uploader: uploader)
|
||
} catch {
|
||
showToast(error.localizedDescription)
|
||
}
|
||
}
|
||
}
|
||
|
||
@objc private func titleChanged() {
|
||
viewModel.updateTitle(titleField.text ?? "")
|
||
if titleField.text != viewModel.title {
|
||
titleField.text = viewModel.title
|
||
}
|
||
}
|
||
|
||
@objc private func pickImage() {
|
||
var configuration = PHPickerConfiguration(photoLibrary: .shared())
|
||
configuration.filter = .images
|
||
configuration.selectionLimit = 1
|
||
let picker = PHPickerViewController(configuration: configuration)
|
||
picker.delegate = self
|
||
present(picker, animated: true)
|
||
}
|
||
|
||
@objc private func confirmTapped() {
|
||
Task { await viewModel.create(api: liveAPI) }
|
||
}
|
||
|
||
private func applyViewModel() {
|
||
viewModel.isLoading ? showLoading() : hideLoading()
|
||
filesStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||
for (index, file) in viewModel.files.enumerated() {
|
||
let view = LiveAlbumUploadedFileView(file: file)
|
||
view.onDelete = { [weak self] in self?.confirmDeleteLocalFile(at: index) }
|
||
filesStack.addArrangedSubview(view)
|
||
view.snp.makeConstraints { make in
|
||
make.height.equalTo(view.snp.width).multipliedBy(9.0 / 16.0)
|
||
}
|
||
}
|
||
}
|
||
|
||
private func confirmDeleteLocalFile(at index: Int) {
|
||
showAlert(title: "确认删除", message: "是否确认删除该相册?删除后将无法恢复。") { [weak self] in
|
||
self?.viewModel.deleteLocalFile(at: index)
|
||
}
|
||
}
|
||
|
||
private func addRequiredTitle(_ title: String, suffix: String? = nil, to stack: UIStackView) {
|
||
let label = UILabel()
|
||
let text = NSMutableAttributedString(
|
||
string: "\(title) *",
|
||
attributes: [.font: UIFont.systemFont(ofSize: 14, weight: .medium), .foregroundColor: AppColor.textPrimary]
|
||
)
|
||
text.addAttribute(.foregroundColor, value: UIColor.red, range: NSRange(location: title.count + 1, length: 1))
|
||
if let suffix {
|
||
text.append(NSAttributedString(string: suffix, attributes: [.font: UIFont.systemFont(ofSize: 14), .foregroundColor: AppColor.textPrimary]))
|
||
}
|
||
label.attributedText = text
|
||
stack.addArrangedSubview(label)
|
||
}
|
||
}
|
||
|
||
/// 已上传直播素材预览行。
|
||
private final class LiveAlbumUploadedFileView: UIView {
|
||
var onDelete: (() -> Void)?
|
||
|
||
private let imageView = LiveCoverImageView()
|
||
private let deleteButton = UIButton(type: .system)
|
||
|
||
/// 初始化已上传素材视图。
|
||
init(file: LiveAlbumFile) {
|
||
super.init(frame: .zero)
|
||
imageView.setURL(file.url)
|
||
deleteButton.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal)
|
||
deleteButton.tintColor = .gray
|
||
addSubview(imageView)
|
||
addSubview(deleteButton)
|
||
imageView.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview()
|
||
}
|
||
deleteButton.snp.makeConstraints { make in
|
||
make.top.trailing.equalToSuperview().inset(4)
|
||
make.size.equalTo(28)
|
||
}
|
||
deleteButton.addTarget(self, action: #selector(deleteTapped), for: .touchUpInside)
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
@objc private func deleteTapped() {
|
||
onDelete?()
|
||
}
|
||
}
|
||
|
||
/// 直播相册预览页,对齐 Android `AliveAlbumPreviewScreen`。
|
||
final class LiveAlbumPreviewViewController: BaseViewController, UIScrollViewDelegate {
|
||
private let viewModel: LiveAlbumPreviewViewModel
|
||
private let liveAPI: any LiveServing
|
||
private let pagingScrollView = UIScrollView()
|
||
private let pageControl = UIPageControl()
|
||
private let emptyLabel = UILabel()
|
||
private let bottomBar = UIView()
|
||
private let deleteButton = LiveActionButton(title: "删除素材")
|
||
|
||
/// 初始化直播相册预览页。
|
||
@MainActor
|
||
init(folderId: Int, startIndex: Int, liveAPI: (any LiveServing)? = nil) {
|
||
viewModel = LiveAlbumPreviewViewModel(folderId: folderId, startIndex: startIndex)
|
||
self.liveAPI = liveAPI ?? NetworkServices.shared.liveAPI
|
||
super.init(nibName: nil, bundle: nil)
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
override func setupNavigationBar() {
|
||
title = "预览"
|
||
navigationController?.navigationBar.barTintColor = .black
|
||
navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white]
|
||
navigationController?.navigationBar.tintColor = .white
|
||
}
|
||
|
||
override func setupUI() {
|
||
view.backgroundColor = .black
|
||
pagingScrollView.isPagingEnabled = true
|
||
pagingScrollView.showsHorizontalScrollIndicator = false
|
||
pagingScrollView.delegate = self
|
||
pageControl.currentPageIndicatorTintColor = .white
|
||
pageControl.pageIndicatorTintColor = UIColor.white.withAlphaComponent(0.4)
|
||
emptyLabel.text = "没有可预览的图片"
|
||
emptyLabel.textColor = .white
|
||
emptyLabel.font = .systemFont(ofSize: 16)
|
||
emptyLabel.textAlignment = .center
|
||
bottomBar.backgroundColor = .black
|
||
deleteButton.buttonStyle = .danger
|
||
view.addSubview(pagingScrollView)
|
||
view.addSubview(pageControl)
|
||
view.addSubview(emptyLabel)
|
||
view.addSubview(bottomBar)
|
||
bottomBar.addSubview(deleteButton)
|
||
}
|
||
|
||
override func setupConstraints() {
|
||
bottomBar.snp.makeConstraints { make in
|
||
make.leading.trailing.bottom.equalToSuperview()
|
||
}
|
||
deleteButton.snp.makeConstraints { make in
|
||
make.top.equalToSuperview().offset(16)
|
||
make.leading.trailing.equalToSuperview().inset(16)
|
||
make.height.equalTo(48)
|
||
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(16)
|
||
}
|
||
pagingScrollView.snp.makeConstraints { make in
|
||
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
|
||
make.bottom.equalTo(bottomBar.snp.top)
|
||
}
|
||
pageControl.snp.makeConstraints { make in
|
||
make.centerX.equalToSuperview()
|
||
make.bottom.equalTo(pagingScrollView).inset(8)
|
||
}
|
||
emptyLabel.snp.makeConstraints { make in
|
||
make.center.equalTo(pagingScrollView)
|
||
}
|
||
}
|
||
|
||
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) }
|
||
}
|
||
deleteButton.addTarget(self, action: #selector(confirmDelete), for: .touchUpInside)
|
||
}
|
||
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
Task { await viewModel.load(api: liveAPI) }
|
||
}
|
||
|
||
override func viewWillDisappear(_ animated: Bool) {
|
||
super.viewWillDisappear(animated)
|
||
navigationController?.navigationBar.barTintColor = nil
|
||
navigationController?.navigationBar.titleTextAttributes = nil
|
||
navigationController?.navigationBar.tintColor = nil
|
||
}
|
||
|
||
override func viewDidLayoutSubviews() {
|
||
super.viewDidLayoutSubviews()
|
||
layoutPages()
|
||
}
|
||
|
||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||
let index = Int(round(scrollView.contentOffset.x / max(scrollView.bounds.width, 1)))
|
||
viewModel.updateCurrentIndex(index)
|
||
}
|
||
|
||
private func applyViewModel() {
|
||
viewModel.isLoading && viewModel.files.isEmpty ? showLoading() : hideLoading()
|
||
pageControl.numberOfPages = viewModel.files.count
|
||
pageControl.currentPage = viewModel.currentIndex
|
||
emptyLabel.isHidden = !viewModel.files.isEmpty
|
||
deleteButton.isEnabled = !viewModel.files.isEmpty
|
||
buildPages()
|
||
}
|
||
|
||
private func buildPages() {
|
||
pagingScrollView.subviews.forEach { $0.removeFromSuperview() }
|
||
for (index, file) in viewModel.files.enumerated() {
|
||
let imageView = LiveCoverImageView(cornerRadius: 0)
|
||
imageView.contentMode = .scaleAspectFit
|
||
imageView.backgroundColor = .black
|
||
imageView.setURL(file.url)
|
||
imageView.tag = index
|
||
pagingScrollView.addSubview(imageView)
|
||
}
|
||
layoutPages()
|
||
}
|
||
|
||
private func layoutPages() {
|
||
let size = pagingScrollView.bounds.size
|
||
guard size.width > 0 else { return }
|
||
for (index, view) in pagingScrollView.subviews.enumerated() {
|
||
view.frame = CGRect(x: CGFloat(index) * size.width, y: 0, width: size.width, height: size.height)
|
||
}
|
||
pagingScrollView.contentSize = CGSize(width: size.width * CGFloat(viewModel.files.count), height: size.height)
|
||
pagingScrollView.setContentOffset(CGPoint(x: CGFloat(viewModel.currentIndex) * size.width, y: 0), animated: false)
|
||
}
|
||
|
||
@objc private func confirmDelete() {
|
||
showAlert(title: "确认删除", message: "是否确认删除该素材?删除后将无法恢复。") { [weak self] in
|
||
guard let self else { return }
|
||
Task { await self.viewModel.deleteCurrentFile(api: self.liveAPI) }
|
||
}
|
||
}
|
||
}
|