// // 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) } }