diff --git a/packages/frontend/apps/ios/App/App/AffineViewController.swift b/packages/frontend/apps/ios/App/App/AffineViewController.swift index 98c5e192f5..ac0de01b76 100644 --- a/packages/frontend/apps/ios/App/App/AffineViewController.swift +++ b/packages/frontend/apps/ios/App/App/AffineViewController.swift @@ -33,7 +33,7 @@ class AFFiNEViewController: CAPBridgeViewController { } } -extension AFFiNEViewController: IntelligentsButtonDelegate { +extension AFFiNEViewController: IntelligentsButtonDelegate, IntelligentsFocusApertureViewDelegate { func onIntelligentsButtonTapped(_ button: IntelligentsButton) { guard let webView else { assertionFailure() // ? wdym ? @@ -56,37 +56,55 @@ extension AFFiNEViewController: IntelligentsButtonDelegate { print("[?] \(self) script error: \(error.localizedDescription)") } - if case let .success(content) = result, - let res = content as? String - { - print("[*] \(self) received document with \(res.count) characters") - DispatchQueue.main.async { - self.openIntelligentsSheet(withContext: res) - } - } else { - DispatchQueue.main.async { - self.openSimpleChat() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + if case let .success(content) = result, + let res = content as? String + { + print("[*] \(self) received document with \(res.count) characters") + DispatchQueue.main.async { + self.openIntelligentsSheet(withContext: res) + } + } else { + DispatchQueue.main.async { + self.openSimpleChat() + } } } } } func openIntelligentsSheet(withContext context: String) { - guard let view = webView else { + guard let view = webView?.subviews.first else { assertionFailure() return } + assert(view is UIScrollView) _ = context let focus = IntelligentsFocusApertureView() focus.prepareAnimationWith( capturingTargetContentView: view, coveringRootViewController: self ) + focus.delegate = self focus.executeAnimationKickIn() + dismissIntelligentsButton() } func openSimpleChat() { let targetController = IntelligentsChatController() presentIntoCurrentContext(withTargetController: targetController) } + + func focusApertureRequestAction(actionType: IntelligentsFocusApertureViewActionType) { + switch actionType { + case .translateTo: + fatalError("not implemented") + case .summary: + fatalError("not implemented") + case .chatWithAI: + fatalError("not implemented") + case .dismiss: + presentIntelligentsButton() + } + } } diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIFont.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIFont.swift new file mode 100644 index 0000000000..b94160a5c2 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIFont.swift @@ -0,0 +1,33 @@ +// +// Ext+UIFont.swift +// Intelligents +// +// Created by 秋星桥 on 2024/11/21. +// + +import UIKit + +extension UIFont { + static func preferredFont(for style: TextStyle, weight: Weight, italic: Bool = false) -> UIFont { + // Get the style's default pointSize + let traits = UITraitCollection(preferredContentSizeCategory: .large) + let desc = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style, compatibleWith: traits) + + // Get the font at the default size and preferred weight + var font = UIFont.systemFont(ofSize: desc.pointSize, weight: weight) + if italic == true { + font = font.with([.traitItalic]) + } + + // Setup the font to be auto-scalable + let metrics = UIFontMetrics(forTextStyle: style) + return metrics.scaledFont(for: font) + } + + private func with(_ traits: UIFontDescriptor.SymbolicTraits...) -> UIFont { + guard let descriptor = fontDescriptor.withSymbolicTraits(UIFontDescriptor.SymbolicTraits(traits).union(fontDescriptor.symbolicTraits)) else { + return self + } + return UIFont(descriptor: descriptor, size: 0) + } +} diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIView.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIView.swift index 63b8131f4e..1da7e3b992 100644 --- a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIView.swift +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Extension/Ext+UIView.swift @@ -19,6 +19,15 @@ extension UIView { return nil } + func removeEveryAutoResizingMasks() { + var views: [UIView] = [self] + while let view = views.first { + views.removeFirst() + view.translatesAutoresizingMaskIntoConstraints = false + view.subviews.forEach { views.append($0) } + } + } + #if DEBUG func debugFrame() { layer.borderWidth = 1 diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsFocusApertureView/IntelligentsFocusApertureView+ActionButton.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsFocusApertureView/IntelligentsFocusApertureView+ActionButton.swift new file mode 100644 index 0000000000..03a39267ec --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsFocusApertureView/IntelligentsFocusApertureView+ActionButton.swift @@ -0,0 +1,76 @@ +// +// IntelligentsFocusApertureView+ActionButton.swift +// Intelligents +// +// Created by 秋星桥 on 2024/11/21. +// + +import UIKit + +extension IntelligentsFocusApertureView.ControlButtonsPanel { + class DarkActionButton: UIView { + var iconSystemName: String { + set { iconView.image = UIImage(systemName: newValue) } + get { fatalError() } + } + + var title: String { + set { titleLabel.text = newValue } + get { titleLabel.text ?? "" } + } + + let titleLabel = UILabel() + let iconView = UIImageView() + var action: (() -> Void)? = nil + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .white.withAlphaComponent(0.25) + layer.cornerRadius = 12 + + let layoutGuide = UILayoutGuide() + addLayoutGuide(layoutGuide) + + titleLabel.textAlignment = .center + titleLabel.font = .systemFont(ofSize: UIFont.labelFontSize, weight: .semibold) + titleLabel.textColor = .white + addSubview(titleLabel) + + iconView.contentMode = .scaleAspectFit + iconView.tintColor = .white + addSubview(iconView) + + [ + layoutGuide.centerXAnchor.constraint(equalTo: centerXAnchor), + layoutGuide.centerYAnchor.constraint(equalTo: centerYAnchor), + + iconView.topAnchor.constraint(greaterThanOrEqualTo: layoutGuide.topAnchor), + iconView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor), + iconView.bottomAnchor.constraint(lessThanOrEqualTo: layoutGuide.bottomAnchor), + iconView.centerYAnchor.constraint(equalTo: layoutGuide.centerYAnchor), + + titleLabel.topAnchor.constraint(greaterThanOrEqualTo: layoutGuide.topAnchor), + titleLabel.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor), + titleLabel.bottomAnchor.constraint(lessThanOrEqualTo: layoutGuide.bottomAnchor), + titleLabel.centerYAnchor.constraint(equalTo: layoutGuide.centerYAnchor), + + titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 8), + ].forEach { $0.isActive = true } + + isUserInteractionEnabled = true + addGestureRecognizer(UITapGestureRecognizer( + target: self, + action: #selector(onTapped) + )) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError() + } + + @objc func onTapped() { + action?() + } + } +} diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsFocusApertureView/IntelligentsFocusApertureView+Delegate.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsFocusApertureView/IntelligentsFocusApertureView+Delegate.swift new file mode 100644 index 0000000000..bbd7d14e49 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsFocusApertureView/IntelligentsFocusApertureView+Delegate.swift @@ -0,0 +1,19 @@ +// +// IntelligentsFocusApertureView+Delegate.swift +// Intelligents +// +// Created by 秋星桥 on 2024/11/21. +// + +import Foundation + +public enum IntelligentsFocusApertureViewActionType: String { + case translateTo + case summary + case chatWithAI + case dismiss +} + +public protocol IntelligentsFocusApertureViewDelegate: AnyObject { + func focusApertureRequestAction(actionType: IntelligentsFocusApertureViewActionType) +} diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsFocusApertureView/IntelligentsFocusApertureView+Panel.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsFocusApertureView/IntelligentsFocusApertureView+Panel.swift index 2ad612ee1c..53f745414c 100644 --- a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsFocusApertureView/IntelligentsFocusApertureView+Panel.swift +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsFocusApertureView/IntelligentsFocusApertureView+Panel.swift @@ -9,11 +9,102 @@ import UIKit extension IntelligentsFocusApertureView { class ControlButtonsPanel: UIView { + let headerLabel = UILabel() + let headerIcon = UIImageView() + + let translateButton = DarkActionButton() + let summaryButton = DarkActionButton() + let chatWithAIButton = DarkActionButton() + init() { super.init(frame: .zero) - backgroundColor = .red + defer { removeEveryAutoResizingMasks() } - heightAnchor.constraint(equalToConstant: 256).isActive = true + let contentSpacing: CGFloat = 16 + let buttonGroupHeight: CGFloat = 55 + + let headerGroup = UIView() + addSubview(headerGroup) + [ + headerGroup.topAnchor.constraint(equalTo: topAnchor), + headerGroup.leadingAnchor.constraint(equalTo: leadingAnchor), + headerGroup.trailingAnchor.constraint(equalTo: trailingAnchor), + ].forEach { $0.isActive = true } + + headerLabel.text = NSLocalizedString("AFFiNE AI", comment: "") // TODO: FREE TRAIL??? + // title 3 with bold + headerLabel.font = .preferredFont(for: .title3, weight: .bold) + headerLabel.textColor = .white + headerLabel.textAlignment = .left + headerIcon.image = .init(named: "spark", in: .module, with: nil) + headerIcon.contentMode = .scaleAspectFit + headerIcon.tintColor = Constant.affineTintColor + headerGroup.addSubview(headerLabel) + headerGroup.addSubview(headerIcon) + [ + headerLabel.topAnchor.constraint(equalTo: headerGroup.topAnchor), + headerLabel.leadingAnchor.constraint(equalTo: headerGroup.leadingAnchor), + headerLabel.bottomAnchor.constraint(equalTo: headerGroup.bottomAnchor), + + headerIcon.topAnchor.constraint(equalTo: headerGroup.topAnchor), + headerIcon.trailingAnchor.constraint(equalTo: headerGroup.trailingAnchor), + headerIcon.bottomAnchor.constraint(equalTo: headerGroup.bottomAnchor), + + headerIcon.widthAnchor.constraint(equalToConstant: 32), + headerIcon.trailingAnchor.constraint(equalTo: headerGroup.trailingAnchor), + headerIcon.leadingAnchor.constraint(equalTo: headerLabel.trailingAnchor, constant: contentSpacing), + ].forEach { $0.isActive = true } + + let firstButtonSectionGroup = UIView() + addSubview(firstButtonSectionGroup) + [ + firstButtonSectionGroup.topAnchor.constraint(equalTo: headerGroup.bottomAnchor, constant: contentSpacing), + firstButtonSectionGroup.leadingAnchor.constraint(equalTo: leadingAnchor), + firstButtonSectionGroup.trailingAnchor.constraint(equalTo: trailingAnchor), + firstButtonSectionGroup.heightAnchor.constraint(equalToConstant: buttonGroupHeight), + ].forEach { $0.isActive = true } + + translateButton.title = NSLocalizedString("Translate", comment: "") + translateButton.iconSystemName = "textformat" + summaryButton.title = NSLocalizedString("Summary", comment: "") + summaryButton.iconSystemName = "doc.text" + firstButtonSectionGroup.addSubview(translateButton) + firstButtonSectionGroup.addSubview(summaryButton) + [ + translateButton.topAnchor.constraint(equalTo: firstButtonSectionGroup.topAnchor), + translateButton.leadingAnchor.constraint(equalTo: firstButtonSectionGroup.leadingAnchor), + translateButton.bottomAnchor.constraint(equalTo: firstButtonSectionGroup.bottomAnchor), + + summaryButton.topAnchor.constraint(equalTo: firstButtonSectionGroup.topAnchor), + summaryButton.trailingAnchor.constraint(equalTo: firstButtonSectionGroup.trailingAnchor), + summaryButton.bottomAnchor.constraint(equalTo: firstButtonSectionGroup.bottomAnchor), + + translateButton.widthAnchor.constraint(equalTo: summaryButton.widthAnchor), + translateButton.trailingAnchor.constraint(equalTo: summaryButton.leadingAnchor, constant: -contentSpacing), + ].forEach { $0.isActive = true } + + let secondButtonSectionGroup = UIView() + addSubview(secondButtonSectionGroup) + [ + secondButtonSectionGroup.topAnchor.constraint(equalTo: firstButtonSectionGroup.bottomAnchor, constant: contentSpacing), + secondButtonSectionGroup.leadingAnchor.constraint(equalTo: leadingAnchor), + secondButtonSectionGroup.trailingAnchor.constraint(equalTo: trailingAnchor), + secondButtonSectionGroup.heightAnchor.constraint(equalToConstant: buttonGroupHeight), + ].forEach { $0.isActive = true } + + secondButtonSectionGroup.addSubview(chatWithAIButton) + chatWithAIButton.title = NSLocalizedString("Chat with AI", comment: "") + chatWithAIButton.iconSystemName = "paperplane" + [ + chatWithAIButton.topAnchor.constraint(equalTo: secondButtonSectionGroup.topAnchor), + chatWithAIButton.leadingAnchor.constraint(equalTo: secondButtonSectionGroup.leadingAnchor), + chatWithAIButton.bottomAnchor.constraint(equalTo: secondButtonSectionGroup.bottomAnchor), + chatWithAIButton.trailingAnchor.constraint(equalTo: secondButtonSectionGroup.trailingAnchor), + ].forEach { $0.isActive = true } + + [ + secondButtonSectionGroup.bottomAnchor.constraint(equalTo: bottomAnchor), + ].forEach { $0.isActive = true } } @available(*, unavailable) diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsFocusApertureView/IntelligentsFocusApertureView.swift b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsFocusApertureView/IntelligentsFocusApertureView.swift index 176150e99c..e79fb3faeb 100644 --- a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsFocusApertureView/IntelligentsFocusApertureView.swift +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/IntelligentsFocusApertureView/IntelligentsFocusApertureView.swift @@ -25,34 +25,44 @@ public class IntelligentsFocusApertureView: UIView { var contentBeginConstraints: [NSLayoutConstraint] = [] var contentFinalConstraints: [NSLayoutConstraint] = [] + public weak var delegate: (any IntelligentsFocusApertureViewDelegate)? + public init() { super.init(frame: .zero) - let tap = UITapGestureRecognizer( - target: self, - action: #selector(dismissFocus) - ) - backgroundView.backgroundColor = .black backgroundView.isUserInteractionEnabled = true - backgroundView.addGestureRecognizer(tap) + backgroundView.addGestureRecognizer(UITapGestureRecognizer( + target: self, + action: #selector(dismissFocus) + )) + snapshotView.setContentHuggingPriority(.defaultLow, for: .vertical) + snapshotView.setContentCompressionResistancePriority(.defaultLow, for: .vertical) + snapshotView.layer.contentsGravity = .top snapshotView.layer.masksToBounds = true snapshotView.contentMode = .scaleAspectFill snapshotView.isUserInteractionEnabled = true - snapshotView.addGestureRecognizer(tap) + snapshotView.addGestureRecognizer(UITapGestureRecognizer( + target: self, + action: #selector(dismissFocus) + )) addSubview(backgroundView) addSubview(controlButtonsPanel) addSubview(snapshotView) bringSubviewToFront(snapshotView) - var views: [UIView] = [self] - while let view = views.first { - views.removeFirst() - view.translatesAutoresizingMaskIntoConstraints = false - view.subviews.forEach { views.append($0) } + controlButtonsPanel.translateButton.action = { [weak self] in + self?.delegate?.focusApertureRequestAction(actionType: .translateTo) } + controlButtonsPanel.summaryButton.action = { [weak self] in + self?.delegate?.focusApertureRequestAction(actionType: .summary) + } + controlButtonsPanel.chatWithAIButton.action = { [weak self] in + self?.delegate?.focusApertureRequestAction(actionType: .chatWithAI) + } + removeEveryAutoResizingMasks() } @available(*, unavailable) @@ -112,6 +122,7 @@ public class IntelligentsFocusApertureView: UIView { isUserInteractionEnabled = false executeAnimationDismiss { self.removeFromSuperview() + self.delegate?.focusApertureRequestAction(actionType: .dismiss) } } } diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/en.lproj/Localizable.strings b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/en.lproj/Localizable.strings index 65b1b827a5..68c69c1f2c 100644 --- a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/en.lproj/Localizable.strings +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/en.lproj/Localizable.strings @@ -9,5 +9,14 @@ /* No comment provided by engineer. */ "Chat with AI" = "Chat with AI"; +/* No comment provided by engineer. */ +"AFFiNE AI" = "AFFiNE AI"; + +/* No comment provided by engineer. */ +"Translate" = "Translate"; + +/* No comment provided by engineer. */ +"Summary" = "Summary"; + /* No comment provided by engineer. */ "Summarize this article for me..." = "Summarize this article for me..."; diff --git a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/zh-Hans.lproj/Localizable.strings b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/zh-Hans.lproj/Localizable.strings index 744e2d5db3..ec1b0b72c4 100644 --- a/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/zh-Hans.lproj/Localizable.strings +++ b/packages/frontend/apps/ios/App/App/Packages/Intelligents/Sources/Intelligents/Resources/zh-Hans.lproj/Localizable.strings @@ -9,5 +9,14 @@ /* No comment provided by engineer. */ "Chat with AI" = "与 AI 聊天"; +/* No comment provided by engineer. */ +"AFFiNE AI" = "AFFiNE 人工智能"; + +/* No comment provided by engineer. */ +"Translate" = "翻译"; + +/* No comment provided by engineer. */ +"Summary" = "总结"; + /* No comment provided by engineer. */ "Summarize this article for me..." = "请为我总结这份文档...";