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