模块化 AppStore 并完善素材管理与个人空间设置。
将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
258
suixinkan/UI/MaterialManagement/MaterialManagementDialogs.swift
Normal file
258
suixinkan/UI/MaterialManagement/MaterialManagementDialogs.swift
Normal file
@ -0,0 +1,258 @@
|
||||
//
|
||||
// MaterialManagementDialogs.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import AVKit
|
||||
import Kingfisher
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 素材模块 Android 风格红色删除确认弹窗。
|
||||
final class MaterialDeleteDialogViewController: UIViewController {
|
||||
private let dialogTitle: String
|
||||
private let itemName: String
|
||||
private let itemSuffix: String
|
||||
private let onConfirm: () -> Void
|
||||
private let onCancel: () -> Void
|
||||
private let dimView = UIView()
|
||||
private let cardView = UIView()
|
||||
|
||||
/// 创建素材删除确认弹窗。
|
||||
init(
|
||||
title: String = "删除素材",
|
||||
itemName: String,
|
||||
itemSuffix: String = "素材吗?",
|
||||
onCancel: @escaping () -> Void = {},
|
||||
onConfirm: @escaping () -> Void
|
||||
) {
|
||||
dialogTitle = title
|
||||
self.itemName = itemName
|
||||
self.itemSuffix = itemSuffix
|
||||
self.onConfirm = onConfirm
|
||||
self.onCancel = onCancel
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
modalPresentationStyle = .overFullScreen
|
||||
modalTransitionStyle = .crossDissolve
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .clear
|
||||
dimView.backgroundColor = UIColor.black.withAlphaComponent(0.32)
|
||||
cardView.backgroundColor = .white
|
||||
cardView.layer.cornerRadius = 16
|
||||
|
||||
let stack = UIStackView()
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 16
|
||||
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = dialogTitle
|
||||
titleLabel.font = .systemFont(ofSize: 18, weight: .bold)
|
||||
titleLabel.textColor = UIColor(hex: 0xFF0000)
|
||||
|
||||
let messageLabel = UILabel()
|
||||
messageLabel.numberOfLines = 0
|
||||
let prefix = "您确定要删除"
|
||||
let suffix = itemSuffix
|
||||
let message = NSMutableAttributedString(
|
||||
string: prefix + itemName + suffix,
|
||||
attributes: [
|
||||
.font: UIFont.systemFont(ofSize: 16),
|
||||
.foregroundColor: UIColor(hex: 0x333333),
|
||||
]
|
||||
)
|
||||
if !itemName.isEmpty {
|
||||
let prefixLength = (prefix as NSString).length
|
||||
let itemLength = (itemName as NSString).length
|
||||
message.addAttribute(
|
||||
.font,
|
||||
value: UIFont.systemFont(ofSize: 16, weight: .bold),
|
||||
range: NSRange(location: prefixLength, length: itemLength)
|
||||
)
|
||||
}
|
||||
messageLabel.attributedText = message
|
||||
|
||||
let buttons = UIStackView()
|
||||
buttons.axis = .horizontal
|
||||
buttons.spacing = 12
|
||||
buttons.distribution = .fillEqually
|
||||
let cancelButton = makeButton(title: "取消", titleColor: UIColor(hex: 0xFF0000), background: UIColor(hex: 0xFFE5E5))
|
||||
let confirmButton = makeButton(title: "确认", titleColor: .white, background: UIColor(hex: 0xFF0000))
|
||||
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
|
||||
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
|
||||
[cancelButton, confirmButton].forEach { button in
|
||||
button.snp.makeConstraints { make in make.height.equalTo(44) }
|
||||
buttons.addArrangedSubview(button)
|
||||
}
|
||||
|
||||
view.addSubview(dimView)
|
||||
view.addSubview(cardView)
|
||||
cardView.addSubview(stack)
|
||||
[titleLabel, messageLabel, buttons].forEach(stack.addArrangedSubview)
|
||||
stack.setCustomSpacing(24, after: messageLabel)
|
||||
dimView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
cardView.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
make.leading.trailing.equalToSuperview().inset(32)
|
||||
}
|
||||
stack.snp.makeConstraints { make in make.edges.equalToSuperview().inset(24) }
|
||||
}
|
||||
|
||||
private func makeButton(title: String, titleColor: UIColor, background: UIColor) -> UIButton {
|
||||
let button = UIButton(type: .system)
|
||||
button.setTitle(title, for: .normal)
|
||||
button.setTitleColor(titleColor, for: .normal)
|
||||
button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
button.backgroundColor = background
|
||||
button.layer.cornerRadius = 8
|
||||
return button
|
||||
}
|
||||
|
||||
@objc private func cancelTapped() {
|
||||
dismiss(animated: true) { [onCancel] in onCancel() }
|
||||
}
|
||||
|
||||
@objc private func confirmTapped() {
|
||||
dismiss(animated: true) { [onConfirm] in onConfirm() }
|
||||
}
|
||||
}
|
||||
|
||||
/// 素材预览数据,统一描述图片和视频。
|
||||
struct MaterialPreviewItem: Equatable {
|
||||
let url: String
|
||||
let mediaType: MaterialMediaType
|
||||
}
|
||||
|
||||
/// Android `MediaPreviewDialog` 对齐的全屏素材预览页。
|
||||
final class MaterialMediaPreviewViewController: UIViewController, UIScrollViewDelegate {
|
||||
private let items: [MaterialPreviewItem]
|
||||
private let startIndex: Int
|
||||
private let scrollView = UIScrollView()
|
||||
private let closeButton = UIButton(type: .system)
|
||||
private var pages: [UIView] = []
|
||||
private var players: [Int: AVPlayerViewController] = [:]
|
||||
private var currentIndex = 0
|
||||
|
||||
/// 创建全屏素材预览页。
|
||||
init(items: [MaterialPreviewItem], startIndex: Int) {
|
||||
self.items = items
|
||||
self.startIndex = max(0, min(startIndex, max(0, items.count - 1)))
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
modalPresentationStyle = .overFullScreen
|
||||
modalTransitionStyle = .crossDissolve
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .black
|
||||
scrollView.backgroundColor = .black
|
||||
scrollView.isPagingEnabled = true
|
||||
scrollView.showsHorizontalScrollIndicator = false
|
||||
scrollView.bounces = false
|
||||
scrollView.delegate = self
|
||||
closeButton.setImage(UIImage(named: "material_ic_clear"), for: .normal)
|
||||
closeButton.tintColor = .white
|
||||
closeButton.backgroundColor = UIColor.black.withAlphaComponent(0.5)
|
||||
closeButton.layer.cornerRadius = 22
|
||||
closeButton.accessibilityLabel = "关闭"
|
||||
closeButton.addTarget(self, action: #selector(closeTapped), for: .touchUpInside)
|
||||
|
||||
view.addSubview(scrollView)
|
||||
view.addSubview(closeButton)
|
||||
scrollView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
closeButton.snp.makeConstraints { make in
|
||||
make.top.equalTo(view.safeAreaLayoutGuide).offset(5)
|
||||
make.trailing.equalToSuperview().inset(16)
|
||||
make.size.equalTo(44)
|
||||
}
|
||||
buildPages()
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
let size = scrollView.bounds.size
|
||||
guard size.width > 0, size.height > 0 else { return }
|
||||
for (index, page) in pages.enumerated() {
|
||||
page.frame = CGRect(x: CGFloat(index) * size.width, y: 0, width: size.width, height: size.height)
|
||||
}
|
||||
scrollView.contentSize = CGSize(width: CGFloat(items.count) * size.width, height: size.height)
|
||||
scrollView.contentOffset = CGPoint(x: CGFloat(currentIndex) * size.width, y: 0)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
currentIndex = startIndex
|
||||
view.setNeedsLayout()
|
||||
view.layoutIfNeeded()
|
||||
playCurrentVideo()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
players.values.forEach { $0.player?.pause() }
|
||||
}
|
||||
|
||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
updateCurrentIndex()
|
||||
}
|
||||
|
||||
private func buildPages() {
|
||||
for (index, item) in items.enumerated() {
|
||||
let page = UIView()
|
||||
page.backgroundColor = .black
|
||||
scrollView.addSubview(page)
|
||||
pages.append(page)
|
||||
if item.mediaType == .video, let url = URL(string: item.url) {
|
||||
let controller = AVPlayerViewController()
|
||||
controller.player = AVPlayer(url: url)
|
||||
controller.showsPlaybackControls = true
|
||||
addChild(controller)
|
||||
page.addSubview(controller.view)
|
||||
controller.view.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
controller.didMove(toParent: self)
|
||||
players[index] = controller
|
||||
} else {
|
||||
let imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.backgroundColor = .black
|
||||
page.addSubview(imageView)
|
||||
imageView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
if let url = URL(string: item.url) {
|
||||
imageView.kf.setImage(with: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateCurrentIndex() {
|
||||
guard scrollView.bounds.width > 0 else { return }
|
||||
currentIndex = max(0, min(Int(round(scrollView.contentOffset.x / scrollView.bounds.width)), items.count - 1))
|
||||
playCurrentVideo()
|
||||
}
|
||||
|
||||
private func playCurrentVideo() {
|
||||
players.forEach { index, controller in
|
||||
if index == currentIndex {
|
||||
controller.player?.play()
|
||||
} else {
|
||||
controller.player?.pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func closeTapped() {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user