Files
suixinkan_uikit/suixinkan/UI/CloudDrive/CloudDriveActionSheetViewController.swift

660 lines
25 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// CloudDriveActionSheetViewController.swift
// suixinkan
//
import SnapKit
import UIKit
/// Android
final class CloudDriveActionSheetViewController: UIViewController {
///
enum Layout {
case upload
case controls
}
///
struct Item {
///
let title: String
///
let image: UIImage?
///
let isDestructive: Bool
///
let handler: () -> Void
}
private let sheetTitle: String
private let layout: Layout
private let items: [Item]
private let dimView = UIView()
private let panelView = UIView()
private let contentStack = UIStackView()
/// Android
init(title: String, layout: Layout, items: [Item]) {
sheetTitle = title
self.layout = layout
self.items = items
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()
setupUI()
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 = 16
panelView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
panelView.clipsToBounds = true
contentStack.axis = .vertical
contentStack.spacing = 0
view.addSubview(dimView)
view.addSubview(panelView)
panelView.addSubview(contentStack)
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())
}
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() }
panelView.snp.makeConstraints { make in
make.leading.trailing.bottom.equalToSuperview()
make.top.greaterThanOrEqualTo(view.safeAreaLayoutGuide).offset(20)
}
contentStack.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
make.bottom.equalTo(view.safeAreaLayoutGuide)
}
}
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)
}
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() {
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) }
}
}