同步相册云盘界面并完善相关交互
This commit is contained in:
@ -6,8 +6,14 @@
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 云盘底部操作弹层,用于同步 Android 云盘上传与文件操作菜单。
|
||||
/// 云盘底部操作弹层,严格同步 Android 上传与文件操作菜单。
|
||||
final class CloudDriveActionSheetViewController: UIViewController {
|
||||
/// 云盘弹层内容布局。
|
||||
enum Layout {
|
||||
case upload
|
||||
case controls
|
||||
}
|
||||
|
||||
/// 云盘底部弹层操作项。
|
||||
struct Item {
|
||||
/// 操作文案。
|
||||
@ -20,19 +26,20 @@ final class CloudDriveActionSheetViewController: UIViewController {
|
||||
let handler: () -> Void
|
||||
}
|
||||
|
||||
private let sheetTitle: String?
|
||||
private let sheetTitle: String
|
||||
private let layout: Layout
|
||||
private let items: [Item]
|
||||
private let dimView = UIView()
|
||||
private let panelView = UIView()
|
||||
private let stackView = UIStackView()
|
||||
private let contentStack = UIStackView()
|
||||
|
||||
/// 初始化云盘底部弹层。
|
||||
init(title: String? = nil, items: [Item]) {
|
||||
self.sheetTitle = title
|
||||
/// 初始化 Android 风格云盘底部弹层。
|
||||
init(title: String, layout: Layout, items: [Item]) {
|
||||
sheetTitle = title
|
||||
self.layout = layout
|
||||
self.items = items
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
modalPresentationStyle = .overFullScreen
|
||||
modalTransitionStyle = .crossDissolve
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
@ -46,92 +53,607 @@ final class CloudDriveActionSheetViewController: UIViewController {
|
||||
setupConstraints()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
dimView.alpha = 0
|
||||
panelView.transform = CGAffineTransform(translationX: 0, y: 420)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
UIView.animate(withDuration: 0.24, delay: 0, options: [.curveEaseOut]) {
|
||||
self.dimView.alpha = 1
|
||||
self.panelView.transform = .identity
|
||||
}
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
view.backgroundColor = .clear
|
||||
dimView.backgroundColor = UIColor.black.withAlphaComponent(0.35)
|
||||
dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(cancelTapped)))
|
||||
|
||||
panelView.backgroundColor = .white
|
||||
panelView.layer.cornerRadius = 18
|
||||
panelView.layer.cornerRadius = 16
|
||||
panelView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
panelView.clipsToBounds = true
|
||||
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = 0
|
||||
contentStack.axis = .vertical
|
||||
contentStack.spacing = 0
|
||||
|
||||
view.addSubview(dimView)
|
||||
view.addSubview(panelView)
|
||||
panelView.addSubview(stackView)
|
||||
panelView.addSubview(contentStack)
|
||||
|
||||
if let sheetTitle {
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = sheetTitle
|
||||
titleLabel.font = .app(.bodyMedium)
|
||||
titleLabel.textColor = AppColor.textPrimary
|
||||
titleLabel.textAlignment = .center
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.height.equalTo(54)
|
||||
}
|
||||
stackView.addArrangedSubview(titleLabel)
|
||||
let titleContainer = UIView()
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = sheetTitle
|
||||
titleLabel.font = .systemFont(ofSize: 18, weight: .semibold)
|
||||
titleLabel.textColor = .black
|
||||
titleLabel.textAlignment = .left
|
||||
titleContainer.addSubview(titleLabel)
|
||||
titleContainer.snp.makeConstraints { make in make.height.equalTo(52) }
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.leading.trailing.equalToSuperview().inset(16)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
contentStack.addArrangedSubview(titleContainer)
|
||||
|
||||
let titleDivider = UIView()
|
||||
titleDivider.backgroundColor = UIColor(hex: 0xEEEEEE)
|
||||
titleDivider.snp.makeConstraints { make in make.height.equalTo(0.5) }
|
||||
contentStack.addArrangedSubview(titleDivider)
|
||||
|
||||
switch layout {
|
||||
case .upload:
|
||||
contentStack.addArrangedSubview(makeUploadContent())
|
||||
case .controls:
|
||||
contentStack.addArrangedSubview(makeControlsContent())
|
||||
}
|
||||
|
||||
for item in items {
|
||||
stackView.addArrangedSubview(makeItemButton(item))
|
||||
}
|
||||
|
||||
let spacer = UIView()
|
||||
spacer.backgroundColor = AppColor.pageBackground
|
||||
spacer.snp.makeConstraints { make in
|
||||
make.height.equalTo(8)
|
||||
}
|
||||
stackView.addArrangedSubview(spacer)
|
||||
|
||||
let cancelButton = UIButton(type: .system)
|
||||
cancelButton.setTitle("取消", for: .normal)
|
||||
cancelButton.setTitleColor(AppColor.textPrimary, for: .normal)
|
||||
cancelButton.titleLabel?.font = .app(.body)
|
||||
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
|
||||
cancelButton.snp.makeConstraints { make in
|
||||
make.height.equalTo(54)
|
||||
}
|
||||
stackView.addArrangedSubview(cancelButton)
|
||||
let divider = UIView()
|
||||
divider.backgroundColor = UIColor(hex: 0xEEEEEE)
|
||||
divider.snp.makeConstraints { make in make.height.equalTo(0.5) }
|
||||
contentStack.addArrangedSubview(divider)
|
||||
contentStack.addArrangedSubview(makeCancelArea())
|
||||
let bottomSpacer = UIView()
|
||||
bottomSpacer.snp.makeConstraints { make in make.height.equalTo(12) }
|
||||
contentStack.addArrangedSubview(bottomSpacer)
|
||||
}
|
||||
|
||||
private func setupConstraints() {
|
||||
dimView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
dimView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
panelView.snp.makeConstraints { make in
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
make.top.greaterThanOrEqualTo(view.safeAreaLayoutGuide).offset(20)
|
||||
}
|
||||
stackView.snp.makeConstraints { make in
|
||||
contentStack.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview()
|
||||
make.bottom.equalTo(view.safeAreaLayoutGuide)
|
||||
}
|
||||
}
|
||||
|
||||
private func makeItemButton(_ item: Item) -> UIButton {
|
||||
let button = UIButton(type: .system)
|
||||
button.setTitle(item.title, for: .normal)
|
||||
button.setImage(item.image, for: .normal)
|
||||
button.tintColor = item.isDestructive ? AppColor.danger : AppColor.textPrimary
|
||||
button.setTitleColor(item.isDestructive ? AppColor.danger : AppColor.textPrimary, for: .normal)
|
||||
button.titleLabel?.font = .app(.body)
|
||||
button.contentHorizontalAlignment = .left
|
||||
button.contentEdgeInsets = UIEdgeInsets(top: 0, left: AppSpacing.xl, bottom: 0, right: AppSpacing.xl)
|
||||
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: AppSpacing.md)
|
||||
button.addAction(UIAction { [weak self] _ in
|
||||
self?.dismiss(animated: true) {
|
||||
item.handler()
|
||||
private func makeUploadContent() -> UIView {
|
||||
let container = UIView()
|
||||
let stack = UIStackView()
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 12
|
||||
container.addSubview(stack)
|
||||
stack.snp.makeConstraints { make in make.edges.equalToSuperview().inset(16) }
|
||||
|
||||
items.enumerated().forEach { index, item in
|
||||
let row = UIView()
|
||||
row.layer.cornerRadius = 8
|
||||
row.layer.borderWidth = 1
|
||||
row.layer.borderColor = UIColor(hex: 0xF4F4F4).cgColor
|
||||
|
||||
let iconView = UIImageView(image: item.image?.withRenderingMode(.alwaysOriginal))
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
let label = UILabel()
|
||||
label.text = item.title
|
||||
label.font = .systemFont(ofSize: 14, weight: .bold)
|
||||
label.textColor = .black
|
||||
let button = UIButton(type: .custom)
|
||||
button.tag = index
|
||||
button.accessibilityLabel = item.title
|
||||
button.addTarget(self, action: #selector(itemTapped(_:)), for: .touchUpInside)
|
||||
|
||||
row.addSubview(iconView)
|
||||
row.addSubview(label)
|
||||
row.addSubview(button)
|
||||
iconView.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(16)
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.height.equalTo(32)
|
||||
}
|
||||
}, for: .touchUpInside)
|
||||
button.snp.makeConstraints { make in
|
||||
make.height.equalTo(54)
|
||||
label.snp.makeConstraints { make in
|
||||
make.leading.equalTo(iconView.snp.trailing).offset(12)
|
||||
make.centerY.equalToSuperview()
|
||||
make.trailing.lessThanOrEqualToSuperview().inset(16)
|
||||
}
|
||||
button.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
row.snp.makeConstraints { make in make.height.equalTo(56) }
|
||||
stack.addArrangedSubview(row)
|
||||
}
|
||||
return container
|
||||
}
|
||||
|
||||
private func makeControlsContent() -> UIView {
|
||||
let row = UIStackView()
|
||||
row.axis = .horizontal
|
||||
row.distribution = .fillEqually
|
||||
|
||||
items.enumerated().forEach { index, item in
|
||||
let container = UIView()
|
||||
let iconView = UIImageView(image: item.image?.withRenderingMode(.alwaysOriginal))
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
let label = UILabel()
|
||||
label.text = item.title
|
||||
label.font = .systemFont(ofSize: 14)
|
||||
label.textColor = UIColor(hex: 0x4B5563)
|
||||
label.textAlignment = .center
|
||||
let button = UIButton(type: .custom)
|
||||
button.tag = index
|
||||
button.accessibilityLabel = item.title
|
||||
button.addTarget(self, action: #selector(itemTapped(_:)), for: .touchUpInside)
|
||||
|
||||
container.addSubview(iconView)
|
||||
container.addSubview(label)
|
||||
container.addSubview(button)
|
||||
iconView.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(28)
|
||||
make.centerX.equalToSuperview()
|
||||
make.width.height.equalTo(24)
|
||||
}
|
||||
label.snp.makeConstraints { make in
|
||||
make.top.equalTo(iconView.snp.bottom).offset(8)
|
||||
make.leading.trailing.equalToSuperview().inset(4)
|
||||
}
|
||||
button.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
container.snp.makeConstraints { make in make.height.equalTo(105) }
|
||||
row.addArrangedSubview(container)
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
private func makeCancelArea() -> UIView {
|
||||
let container = UIView()
|
||||
let button = UIButton(type: .system)
|
||||
button.setTitle("取消", for: .normal)
|
||||
button.setTitleColor(UIColor(hex: 0x4B5563), for: .normal)
|
||||
button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
button.backgroundColor = UIColor(hex: 0xF4F4F4)
|
||||
button.layer.cornerRadius = 12
|
||||
button.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
|
||||
container.addSubview(button)
|
||||
button.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(16)
|
||||
make.height.equalTo(48)
|
||||
}
|
||||
return container
|
||||
}
|
||||
|
||||
private func close(completion: (() -> Void)? = nil) {
|
||||
UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseIn]) {
|
||||
self.dimView.alpha = 0
|
||||
self.panelView.transform = CGAffineTransform(translationX: 0, y: self.panelView.bounds.height + 40)
|
||||
} completion: { _ in
|
||||
self.dismiss(animated: false, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func itemTapped(_ sender: UIButton) {
|
||||
guard items.indices.contains(sender.tag) else { return }
|
||||
let handler = items[sender.tag].handler
|
||||
close(completion: handler)
|
||||
}
|
||||
|
||||
@objc private func cancelTapped() {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
/// 云盘居中对话框基类,提供 Android 风格遮罩、圆角和转场。
|
||||
class CloudDriveOverlayDialogViewController: UIViewController {
|
||||
let dimView = UIView()
|
||||
let cardView = UIView()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
modalPresentationStyle = .overFullScreen
|
||||
view.backgroundColor = .clear
|
||||
dimView.backgroundColor = UIColor.black.withAlphaComponent(0.35)
|
||||
cardView.backgroundColor = .white
|
||||
cardView.layer.cornerRadius = 12
|
||||
cardView.clipsToBounds = true
|
||||
view.addSubview(dimView)
|
||||
view.addSubview(cardView)
|
||||
dimView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(cancelOverlay)))
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
dimView.alpha = 0
|
||||
cardView.alpha = 0
|
||||
cardView.transform = CGAffineTransform(scaleX: 0.94, y: 0.94)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
self.dimView.alpha = 1
|
||||
self.cardView.alpha = 1
|
||||
self.cardView.transform = .identity
|
||||
}
|
||||
}
|
||||
|
||||
func dismissOverlay(completion: (() -> Void)? = nil) {
|
||||
UIView.animate(withDuration: 0.16) {
|
||||
self.dimView.alpha = 0
|
||||
self.cardView.alpha = 0
|
||||
self.cardView.transform = CGAffineTransform(scaleX: 0.96, y: 0.96)
|
||||
} completion: { _ in
|
||||
self.dismiss(animated: false, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func cancelOverlay() {
|
||||
dismissOverlay()
|
||||
}
|
||||
}
|
||||
|
||||
/// Android `WeDialog` 对齐的云盘删除确认对话框。
|
||||
final class CloudDriveConfirmationDialogViewController: CloudDriveOverlayDialogViewController {
|
||||
private let dialogTitle: String
|
||||
private let message: String
|
||||
private let onConfirm: () -> Void
|
||||
|
||||
/// 创建删除确认对话框。
|
||||
init(title: String, message: String, onConfirm: @escaping () -> Void) {
|
||||
dialogTitle = title
|
||||
self.message = message
|
||||
self.onConfirm = onConfirm
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
modalPresentationStyle = .overFullScreen
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = dialogTitle
|
||||
titleLabel.font = .systemFont(ofSize: 17, weight: .bold)
|
||||
titleLabel.textColor = .black
|
||||
titleLabel.textAlignment = .center
|
||||
let messageLabel = UILabel()
|
||||
messageLabel.text = message
|
||||
messageLabel.font = .systemFont(ofSize: 17)
|
||||
messageLabel.textColor = .black
|
||||
messageLabel.textAlignment = .center
|
||||
messageLabel.numberOfLines = 0
|
||||
let divider = UIView()
|
||||
divider.backgroundColor = UIColor(hex: 0xD1D5DB)
|
||||
let verticalDivider = UIView()
|
||||
verticalDivider.backgroundColor = UIColor(hex: 0xD1D5DB)
|
||||
let cancelButton = makeDialogButton(title: "取消", color: .black, action: #selector(cancelTapped))
|
||||
let confirmButton = makeDialogButton(title: "确定", color: AppColor.primary, action: #selector(confirmTapped))
|
||||
|
||||
[titleLabel, messageLabel, divider, cancelButton, verticalDivider, confirmButton].forEach(cardView.addSubview)
|
||||
cardView.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
make.width.equalToSuperview().multipliedBy(0.8)
|
||||
}
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(32)
|
||||
make.leading.trailing.equalToSuperview().inset(24)
|
||||
}
|
||||
messageLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(16)
|
||||
make.leading.trailing.equalToSuperview().inset(24)
|
||||
}
|
||||
divider.snp.makeConstraints { make in
|
||||
make.top.equalTo(messageLabel.snp.bottom).offset(32)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(0.5)
|
||||
}
|
||||
cancelButton.snp.makeConstraints { make in
|
||||
make.top.equalTo(divider.snp.bottom)
|
||||
make.leading.bottom.equalToSuperview()
|
||||
make.height.equalTo(56)
|
||||
}
|
||||
confirmButton.snp.makeConstraints { make in
|
||||
make.top.bottom.width.equalTo(cancelButton)
|
||||
make.leading.equalTo(cancelButton.snp.trailing)
|
||||
make.trailing.equalToSuperview()
|
||||
}
|
||||
verticalDivider.snp.makeConstraints { make in
|
||||
make.top.equalTo(divider.snp.bottom)
|
||||
make.bottom.equalToSuperview()
|
||||
make.centerX.equalToSuperview()
|
||||
make.width.equalTo(0.5)
|
||||
}
|
||||
}
|
||||
|
||||
private func makeDialogButton(title: String, color: UIColor, action: Selector) -> UIButton {
|
||||
let button = UIButton(type: .system)
|
||||
button.setTitle(title, for: .normal)
|
||||
button.setTitleColor(color, for: .normal)
|
||||
button.titleLabel?.font = .systemFont(ofSize: 17, weight: .bold)
|
||||
button.addTarget(self, action: action, for: .touchUpInside)
|
||||
return button
|
||||
}
|
||||
|
||||
@objc private func cancelTapped() {
|
||||
dismiss(animated: true)
|
||||
dismissOverlay()
|
||||
}
|
||||
|
||||
@objc private func confirmTapped() {
|
||||
dismissOverlay(completion: onConfirm)
|
||||
}
|
||||
}
|
||||
|
||||
/// Android `AddFolderDialog` 对齐的新建文件夹输入对话框。
|
||||
final class CloudDriveTextInputDialogViewController: CloudDriveOverlayDialogViewController {
|
||||
private let dialogTitle: String
|
||||
private let placeholder: String
|
||||
private let onConfirm: (String) -> Void
|
||||
private let textField = UITextField()
|
||||
private let confirmButton = UIButton(type: .system)
|
||||
|
||||
/// 创建文本输入对话框。
|
||||
init(title: String, placeholder: String, onConfirm: @escaping (String) -> Void) {
|
||||
dialogTitle = title
|
||||
self.placeholder = placeholder
|
||||
self.onConfirm = onConfirm
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
modalPresentationStyle = .overFullScreen
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
cardView.layer.cornerRadius = 16
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = dialogTitle
|
||||
titleLabel.font = .systemFont(ofSize: 18, weight: .bold)
|
||||
titleLabel.textColor = .black
|
||||
let divider = UIView()
|
||||
divider.backgroundColor = UIColor(hex: 0xEEEEEE)
|
||||
|
||||
textField.placeholder = placeholder
|
||||
textField.font = .systemFont(ofSize: 14)
|
||||
textField.textColor = UIColor(hex: 0x333333)
|
||||
textField.backgroundColor = UIColor(hex: 0xF4F4F4)
|
||||
textField.layer.cornerRadius = 8
|
||||
textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 1))
|
||||
textField.leftViewMode = .always
|
||||
textField.clearButtonMode = .whileEditing
|
||||
textField.returnKeyType = .done
|
||||
textField.addTarget(self, action: #selector(textChanged), for: .editingChanged)
|
||||
textField.addTarget(self, action: #selector(confirmTapped), for: .editingDidEndOnExit)
|
||||
|
||||
let cancelButton = UIButton(type: .system)
|
||||
cancelButton.setTitle("取消", for: .normal)
|
||||
cancelButton.setTitleColor(AppColor.primary, for: .normal)
|
||||
cancelButton.titleLabel?.font = .systemFont(ofSize: 14)
|
||||
cancelButton.backgroundColor = UIColor(hex: 0xEFF6FF)
|
||||
cancelButton.layer.cornerRadius = 8
|
||||
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
|
||||
|
||||
confirmButton.setTitle("确定", for: .normal)
|
||||
confirmButton.setTitleColor(.white, for: .normal)
|
||||
confirmButton.titleLabel?.font = .systemFont(ofSize: 14)
|
||||
confirmButton.layer.cornerRadius = 8
|
||||
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
|
||||
confirmButton.isEnabled = false
|
||||
updateConfirmAppearance()
|
||||
|
||||
[titleLabel, divider, textField, cancelButton, confirmButton].forEach(cardView.addSubview)
|
||||
cardView.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
make.leading.trailing.equalToSuperview().inset(28)
|
||||
}
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview()
|
||||
make.leading.trailing.equalToSuperview().inset(16)
|
||||
make.height.equalTo(52)
|
||||
}
|
||||
divider.snp.makeConstraints { make in
|
||||
make.top.equalTo(titleLabel.snp.bottom)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(1)
|
||||
}
|
||||
textField.snp.makeConstraints { make in
|
||||
make.top.equalTo(divider.snp.bottom).offset(16)
|
||||
make.leading.trailing.equalToSuperview().inset(16)
|
||||
make.height.equalTo(48)
|
||||
}
|
||||
confirmButton.snp.makeConstraints { make in
|
||||
make.top.equalTo(textField.snp.bottom).offset(20)
|
||||
make.trailing.bottom.equalToSuperview().inset(16)
|
||||
make.width.equalTo(72)
|
||||
make.height.equalTo(38)
|
||||
}
|
||||
cancelButton.snp.makeConstraints { make in
|
||||
make.trailing.equalTo(confirmButton.snp.leading).offset(-8)
|
||||
make.centerY.width.height.equalTo(confirmButton)
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(keyboardFrameChanged(_:)),
|
||||
name: UIResponder.keyboardWillChangeFrameNotification,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
private func updateConfirmAppearance() {
|
||||
confirmButton.backgroundColor = confirmButton.isEnabled ? AppColor.primary : AppColor.buttonDisabled
|
||||
}
|
||||
|
||||
@objc private func textChanged() {
|
||||
confirmButton.isEnabled = !(textField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
updateConfirmAppearance()
|
||||
}
|
||||
|
||||
@objc private func cancelTapped() {
|
||||
view.endEditing(true)
|
||||
dismissOverlay()
|
||||
}
|
||||
|
||||
@objc private func confirmTapped() {
|
||||
let value = (textField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !value.isEmpty else { return }
|
||||
view.endEditing(true)
|
||||
dismissOverlay { [onConfirm] in onConfirm(value) }
|
||||
}
|
||||
|
||||
@objc private func keyboardFrameChanged(_ notification: Notification) {
|
||||
guard let frame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
|
||||
let frameInView = view.convert(frame, from: nil)
|
||||
let overlap = max(0, cardView.frame.maxY + 16 - frameInView.minY)
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
self.cardView.transform = CGAffineTransform(translationX: 0, y: -overlap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Android `MoveFileDialog` 对齐的云盘移动目标对话框。
|
||||
final class CloudDriveMoveDialogViewController: CloudDriveOverlayDialogViewController, UITableViewDataSource, UITableViewDelegate {
|
||||
private let folders: [CloudFile]
|
||||
private let onConfirm: (Int) -> Void
|
||||
private let tableView = UITableView(frame: .zero, style: .plain)
|
||||
private var selectedID: Int
|
||||
|
||||
/// 创建移动目标选择对话框。
|
||||
init(folders: [CloudFile], onConfirm: @escaping (Int) -> Void) {
|
||||
self.folders = folders
|
||||
self.onConfirm = onConfirm
|
||||
selectedID = folders.first?.id ?? 0
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
modalPresentationStyle = .overFullScreen
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
cardView.layer.cornerRadius = 28
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = "移动到"
|
||||
titleLabel.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||
titleLabel.textColor = UIColor(hex: 0x333333)
|
||||
let messageLabel = UILabel()
|
||||
messageLabel.text = "请选择目标文件夹"
|
||||
messageLabel.font = .systemFont(ofSize: 14)
|
||||
messageLabel.textColor = UIColor(hex: 0x666666)
|
||||
|
||||
tableView.backgroundColor = .white
|
||||
tableView.separatorStyle = .none
|
||||
tableView.rowHeight = 44
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
|
||||
let cancelButton = makeTextButton(title: "取消", color: UIColor(hex: 0x666666), action: #selector(cancelTapped))
|
||||
let confirmButton = makeTextButton(title: "确定", color: AppColor.primary, action: #selector(confirmTapped))
|
||||
|
||||
[titleLabel, messageLabel, tableView, cancelButton, confirmButton].forEach(cardView.addSubview)
|
||||
cardView.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
make.leading.trailing.equalToSuperview().inset(28)
|
||||
}
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(24)
|
||||
make.leading.trailing.equalToSuperview().inset(24)
|
||||
}
|
||||
messageLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(12)
|
||||
make.leading.trailing.equalTo(titleLabel)
|
||||
}
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.top.equalTo(messageLabel.snp.bottom).offset(12)
|
||||
make.leading.trailing.equalToSuperview().inset(16)
|
||||
make.height.equalTo(min(CGFloat(max(folders.count, 1)) * 44, UIScreen.main.bounds.height * 0.6))
|
||||
}
|
||||
confirmButton.snp.makeConstraints { make in
|
||||
make.top.equalTo(tableView.snp.bottom).offset(8)
|
||||
make.trailing.bottom.equalToSuperview().inset(12)
|
||||
make.width.equalTo(64)
|
||||
make.height.equalTo(44)
|
||||
}
|
||||
cancelButton.snp.makeConstraints { make in
|
||||
make.trailing.equalTo(confirmButton.snp.leading)
|
||||
make.centerY.width.height.equalTo(confirmButton)
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
folders.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let reuseIdentifier = "CloudDriveMoveFolderCell"
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier)
|
||||
?? UITableViewCell(style: .default, reuseIdentifier: reuseIdentifier)
|
||||
configure(cell, folder: folders[indexPath.row])
|
||||
return cell
|
||||
}
|
||||
|
||||
private func configure(_ cell: UITableViewCell, folder: CloudFile) {
|
||||
cell.textLabel?.text = folder.name
|
||||
cell.textLabel?.font = .systemFont(ofSize: 14)
|
||||
cell.textLabel?.textColor = UIColor(hex: 0x333333)
|
||||
cell.imageView?.image = UIImage(
|
||||
systemName: selectedID == folder.id ? "circle.inset.filled" : "circle",
|
||||
withConfiguration: UIImage.SymbolConfiguration(pointSize: 20)
|
||||
)
|
||||
cell.imageView?.tintColor = AppColor.primary
|
||||
cell.selectionStyle = .none
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let previousID = selectedID
|
||||
selectedID = folders[indexPath.row].id
|
||||
if let previousIndex = folders.firstIndex(where: { $0.id == previousID }),
|
||||
let previousCell = tableView.cellForRow(at: IndexPath(row: previousIndex, section: 0)) {
|
||||
configure(previousCell, folder: folders[previousIndex])
|
||||
}
|
||||
if let selectedCell = tableView.cellForRow(at: indexPath) {
|
||||
configure(selectedCell, folder: folders[indexPath.row])
|
||||
}
|
||||
}
|
||||
|
||||
private func makeTextButton(title: String, color: UIColor, action: Selector) -> UIButton {
|
||||
let button = UIButton(type: .system)
|
||||
button.setTitle(title, for: .normal)
|
||||
button.setTitleColor(color, for: .normal)
|
||||
button.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
button.addTarget(self, action: action, for: .touchUpInside)
|
||||
return button
|
||||
}
|
||||
|
||||
@objc private func cancelTapped() {
|
||||
dismissOverlay()
|
||||
}
|
||||
|
||||
@objc private func confirmTapped() {
|
||||
let selectedID = selectedID
|
||||
dismissOverlay { [onConfirm] in onConfirm(selectedID) }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user