mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-27 02:42:25 +08:00
feat: completed input box ui + ux (#12927)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a document picker UI for embedding and selecting AFFiNE documents, with improved search and recent document functionality. - Added support for searching and fetching recently updated documents within a workspace. - Enhanced error handling for login and metadata validation during document operations. - Added ability to attach documents directly from the input box. - **Improvements** - Refined UI animations and layout behaviors for attachment and document picker components. - Updated attachment and header views for clearer pluralization and display logic. - Improved selection indicators and search experience in the document picker. - Enhanced delegate handling for user actions across input and attachment components. - **Bug Fixes** - Fixed progress indicators and error alerts for failed document operations. - **Chores** - Updated project configuration for improved build and signing processes. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -3,16 +3,17 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 56;
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
2E0DD47B57B994A104B25EED /* Pods_AFFiNE.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF48636D7DB5BEE00770FD9A /* Pods_AFFiNE.framework */; };
|
||||
5075136A2D1924C600AD60C0 /* RootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507513692D1924C600AD60C0 /* RootViewController.swift */; };
|
||||
5075136E2D1925BC00AD60C0 /* IntelligentsPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5075136D2D1925BC00AD60C0 /* IntelligentsPlugin.swift */; };
|
||||
50802D612D112F8700694021 /* Intelligents in Frameworks */ = {isa = PBXBuildFile; productRef = 50802D602D112F8700694021 /* Intelligents */; };
|
||||
50802D612D112F8700694021 /* Intelligents in Frameworks */ = {isa = PBXBuildFile; productRef = 50802D602D112F8700694021 /* Intelligents */; settings = {ATTRIBUTES = (Required, ); }; };
|
||||
50A285D72D112A5E000D5A6D /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 50A285D62D112A5E000D5A6D /* Localizable.xcstrings */; };
|
||||
50A285D82D112A5E000D5A6D /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 50A285D52D112A5E000D5A6D /* InfoPlist.xcstrings */; };
|
||||
50E218302E0BE1A700EA4C6E /* Intelligents in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 50802D602D112F8700694021 /* Intelligents */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
50FF428A2D2E757E0050AA83 /* ApplicationBridgedWindowScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50FF42892D2E757E0050AA83 /* ApplicationBridgedWindowScript.swift */; };
|
||||
50FF428C2D2E77CC0050AA83 /* AffineViewController+AIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50FF428B2D2E77CC0050AA83 /* AffineViewController+AIButton.swift */; };
|
||||
9D52FC432D26CDBF00105D0A /* JSValueContainerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */; };
|
||||
@@ -36,6 +37,20 @@
|
||||
E93B276C2CED92B1001409B8 /* NavigationGesturePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = E93B276B2CED92B1001409B8 /* NavigationGesturePlugin.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
50E218312E0BE1A700EA4C6E /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
50E218302E0BE1A700EA4C6E /* Intelligents in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
3256F4410D881A03FE77D092 /* Pods-AFFiNE.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AFFiNE.debug.xcconfig"; path = "Pods/Target Support Files/Pods-AFFiNE/Pods-AFFiNE.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
5039CC962D1D42C700874F32 /* AffineGraphQL */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = AffineGraphQL; sourceTree = "<group>"; };
|
||||
@@ -77,8 +92,6 @@
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
C45499AB2D140B5000E21978 /* NBStore */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = NBStore;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -236,6 +249,7 @@
|
||||
504EC3011FED79650016851F /* Frameworks */,
|
||||
504EC3021FED79650016851F /* Resources */,
|
||||
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
|
||||
50E218312E0BE1A700EA4C6E /* Embed Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -326,9 +340,13 @@
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AFFiNE/Pods-AFFiNE-frameworks.sh\"\n";
|
||||
@@ -514,11 +532,10 @@
|
||||
baseConfigurationReference = 3256F4410D881A03FE77D092 /* Pods-AFFiNE.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: TOEVERYTHING PTE. LTD. (73YMMDVT2M)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
DEVELOPMENT_TEAM = 73YMMDVT2M;
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 73YMMDVT2M;
|
||||
DEVELOPMENT_TEAM = 964G86XT2P;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
@@ -534,8 +551,7 @@
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.affine.pro;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "AppStore app.affine.pro";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "AppStore app.affine.pro";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
@@ -552,11 +568,10 @@
|
||||
baseConfigurationReference = E5E5070D1CA1200D4964D91F /* Pods-AFFiNE.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: TOEVERYTHING PTE. LTD. (73YMMDVT2M)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
DEVELOPMENT_TEAM = 73YMMDVT2M;
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 73YMMDVT2M;
|
||||
DEVELOPMENT_TEAM = 964G86XT2P;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
@@ -572,8 +587,7 @@
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.affine.pro;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "AppStore app.affine.pro";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "AppStore app.affine.pro";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
|
||||
@@ -13,10 +13,21 @@ extension AFFiNEViewController: IntelligentsButtonDelegate {
|
||||
IntelligentContext.shared.webView = webView!
|
||||
button.beginProgress()
|
||||
|
||||
IntelligentContext.shared.preparePresent() { _ in
|
||||
IntelligentContext.shared.preparePresent() { result in
|
||||
button.stopProgress()
|
||||
let controller = IntelligentsController()
|
||||
self.present(controller, animated: true)
|
||||
switch result {
|
||||
case .success(let success):
|
||||
let controller = IntelligentsController()
|
||||
self.present(controller, animated: true)
|
||||
case .failure(let failure):
|
||||
let alert = UIAlertController(
|
||||
title: "Error",
|
||||
message: failure.localizedDescription,
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||
self.present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// DateTime.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 6/25/25.
|
||||
//
|
||||
|
||||
import AffineGraphQL
|
||||
import Apollo
|
||||
import ApolloAPI
|
||||
import Foundation
|
||||
|
||||
/// A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.
|
||||
extension DateTime {
|
||||
extension DateTime {
|
||||
private static let formatter: DateFormatter = {
|
||||
let fmt = DateFormatter()
|
||||
fmt.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
|
||||
fmt.timeZone = TimeZone(identifier: "UTC")
|
||||
return fmt
|
||||
}()
|
||||
|
||||
var decoded: Date? {
|
||||
return Self.formatter.date(from: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,17 @@ public class IntelligentContext {
|
||||
return tempDir.appendingPathComponent("IntelligentContext")
|
||||
}()
|
||||
|
||||
public enum IntelligentError: Error, LocalizedError {
|
||||
case loginRequired(String)
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case let .loginRequired(reason):
|
||||
"Login required: \(reason)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private init() {}
|
||||
|
||||
public func preparePresent(_ completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
@@ -63,12 +74,28 @@ public class IntelligentContext {
|
||||
webViewGroup.wait()
|
||||
webViewMetadata = webViewMetadataResult
|
||||
|
||||
if let baseUrlString = webViewMetadataResult[.currentServerBaseUrl] as? String,
|
||||
let url = URL(string: baseUrlString)
|
||||
{
|
||||
QLService.shared.setEndpoint(base: url)
|
||||
// Check required webView metadata
|
||||
guard let baseUrlString = webViewMetadataResult[.currentServerBaseUrl] as? String,
|
||||
!baseUrlString.isEmpty,
|
||||
let url = URL(string: baseUrlString)
|
||||
else {
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(IntelligentError.loginRequired("Missing server base URL")))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard let workspaceId = webViewMetadataResult[.currentWorkspaceId] as? String,
|
||||
!workspaceId.isEmpty
|
||||
else {
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(IntelligentError.loginRequired("Missing workspace ID")))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
QLService.shared.setEndpoint(base: url)
|
||||
|
||||
let gqlGroup = DispatchGroup()
|
||||
var gqlMetadataResult: [QLMetadataKey: Any] = [:]
|
||||
gqlGroup.enter()
|
||||
@@ -79,6 +106,16 @@ public class IntelligentContext {
|
||||
gqlGroup.wait()
|
||||
qlMetadata = gqlMetadataResult
|
||||
|
||||
// Check required QL metadata
|
||||
guard let userIdentifier = gqlMetadataResult[.userIdentifierKey] as? String,
|
||||
!userIdentifier.isEmpty
|
||||
else {
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(IntelligentError.loginRequired("Missing user identifier")))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
dumpMetadataContents()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
|
||||
@@ -89,4 +89,38 @@ public final class QLService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func searchDocuments(
|
||||
workspaceId: String,
|
||||
keyword: String,
|
||||
limit: Int = 20,
|
||||
completion: @escaping ([IndexerSearchDocsQuery.Data.Workspace.SearchDoc]) -> Void
|
||||
) {
|
||||
let input = SearchDocsInput(keyword: keyword, limit: .some(limit))
|
||||
client.fetch(query: IndexerSearchDocsQuery(id: workspaceId, input: input)) { result in
|
||||
switch result {
|
||||
case let .success(graphQLResult):
|
||||
completion(graphQLResult.data?.workspace.searchDocs ?? [])
|
||||
case .failure:
|
||||
completion([])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func fetchRecentlyUpdatedDocs(
|
||||
workspaceId: String,
|
||||
first: Int = 20,
|
||||
completion: @escaping ([GetRecentlyUpdatedDocsQuery.Data.Workspace.RecentlyUpdatedDocs.Edge.Node]) -> Void
|
||||
) {
|
||||
let pagination = PaginationInput(first: .some(first))
|
||||
client.fetch(query: GetRecentlyUpdatedDocsQuery(workspaceId: workspaceId, pagination: pagination)) { result in
|
||||
switch result {
|
||||
case let .success(graphQLResult):
|
||||
let docs = graphQLResult.data?.workspace.recentlyUpdatedDocs.edges.map(\.node) ?? []
|
||||
completion(docs)
|
||||
case .failure:
|
||||
completion([])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,6 @@ import SnapKit
|
||||
import Then
|
||||
import UIKit
|
||||
|
||||
protocol AttachmentManagementControllerDelegate: AnyObject {
|
||||
func deleteFileAttachment(controller: AttachmentManagementController, _ attachment: FileAttachment)
|
||||
func deleteDocumentAttachment(controller: AttachmentManagementController, _ attachment: DocumentAttachment)
|
||||
}
|
||||
|
||||
class AttachmentManagementController: UINavigationController {
|
||||
private let _viewController: _AttachmentManagementController
|
||||
init(delegate: AttachmentManagementControllerDelegate) {
|
||||
@@ -171,17 +166,20 @@ private class AttachmentCell: UITableViewCell {
|
||||
let iconView = UIImageView().then {
|
||||
$0.contentMode = .scaleAspectFit
|
||||
$0.tintColor = .affineIconPrimary
|
||||
$0.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
}
|
||||
|
||||
let titleLabel = UILabel().then {
|
||||
$0.textColor = .label
|
||||
$0.textAlignment = .left
|
||||
$0.font = .preferredFont(forTextStyle: .body)
|
||||
$0.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
}
|
||||
|
||||
let deleteButton = UIButton(type: .system).then {
|
||||
$0.setImage(UIImage(systemName: "xmark"), for: .normal)
|
||||
$0.tintColor = .affineIconPrimary
|
||||
$0.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
}
|
||||
|
||||
var onDelete: (() -> Void)?
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// AttachmentManagementControllerDelegate.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 6/25/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol AttachmentManagementControllerDelegate: AnyObject {
|
||||
func deleteFileAttachment(controller: AttachmentManagementController, _ attachment: FileAttachment)
|
||||
func deleteDocumentAttachment(controller: AttachmentManagementController, _ attachment: DocumentAttachment)
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import PhotosUI
|
||||
import SnapKit
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
@@ -37,12 +38,40 @@ extension MainViewController: InputBoxDelegate {
|
||||
present(documentPicker, animated: true)
|
||||
}
|
||||
|
||||
func inputBoxDidSelectEmbedDocs(_ inputBox: InputBox) {
|
||||
print(#function, inputBox)
|
||||
func inputBoxDidSelectEmbedDocs(_: InputBox) {
|
||||
showDocumentPicker()
|
||||
}
|
||||
|
||||
func inputBoxDidSelectAttachment(_ inputBox: InputBox) {
|
||||
print(#function, inputBox)
|
||||
@objc func showDocumentPicker() {
|
||||
view.endEditing(true)
|
||||
terminateEditGesture.isEnabled = false
|
||||
documentPickerView.snp.remakeConstraints { make in
|
||||
make.bottom.equalTo(view.keyboardLayoutGuide.snp.top)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(300)
|
||||
}
|
||||
documentPickerHideDetector.isHidden = false
|
||||
documentPickerView.setSelectedDocuments(inputBox.viewModel.documentAttachments)
|
||||
|
||||
performWithAnimation(duration: 0.75) {
|
||||
self.view.layoutIfNeeded()
|
||||
} completion: { _ in
|
||||
self.documentPickerView.updateDocumentsFromRecentDocs()
|
||||
self.documentPickerView.searchTextField.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func hideDocumentPicker() {
|
||||
terminateEditGesture.isEnabled = true
|
||||
documentPickerView.snp.remakeConstraints { make in
|
||||
make.top.equalTo(view.snp.bottom).offset(200)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(300)
|
||||
}
|
||||
documentPickerHideDetector.isHidden = true
|
||||
performWithAnimation(duration: 0.75) {
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
func inputBoxDidSend(_ inputBox: InputBox) {
|
||||
@@ -127,3 +156,30 @@ extension MainViewController: UIDocumentPickerDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DocumentPickerViewDelegate
|
||||
|
||||
extension MainViewController: DocumentPickerViewDelegate {
|
||||
func documentPickerView(_: DocumentPickerView, didSelectDocument document: DocumentItem) {
|
||||
// Get current workspace ID
|
||||
guard let workspaceId = IntelligentContext.shared.webViewMetadata[.currentWorkspaceId] as? String,
|
||||
!workspaceId.isEmpty
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
// Create DocumentAttachment from DocumentItem
|
||||
let documentAttachment = DocumentAttachment(
|
||||
title: document.title,
|
||||
workspaceID: workspaceId,
|
||||
documentID: document.id,
|
||||
updatedAt: document.updatedAt
|
||||
)
|
||||
|
||||
// Add to InputBox
|
||||
inputBox.addDocumentAttachment(documentAttachment)
|
||||
|
||||
// Hide document picker
|
||||
hideDocumentPicker()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,23 @@ class MainViewController: UIViewController {
|
||||
$0.delegate = self
|
||||
}
|
||||
|
||||
lazy var documentPickerHideDetector = UIView().then {
|
||||
$0.isUserInteractionEnabled = true
|
||||
$0.isHidden = true
|
||||
$0.addGestureRecognizer(
|
||||
UITapGestureRecognizer(target: self, action: #selector(hideDocumentPicker))
|
||||
)
|
||||
}
|
||||
|
||||
lazy var documentPickerView = DocumentPickerView().then {
|
||||
$0.delegate = self
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private let intelligentContext = IntelligentContext.shared
|
||||
var terminateEditGesture: UITapGestureRecognizer!
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
@@ -32,6 +45,8 @@ class MainViewController: UIViewController {
|
||||
|
||||
view.addSubview(headerView)
|
||||
view.addSubview(inputBox)
|
||||
view.addSubview(documentPickerHideDetector)
|
||||
view.addSubview(documentPickerView)
|
||||
|
||||
headerView.snp.makeConstraints { make in
|
||||
make.top.equalTo(view.safeAreaLayoutGuide)
|
||||
@@ -42,11 +57,26 @@ class MainViewController: UIViewController {
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.bottom.equalTo(view.keyboardLayoutGuide.snp.top)
|
||||
}
|
||||
|
||||
documentPickerHideDetector.snp.makeConstraints { make in
|
||||
make.left.top.right.equalToSuperview()
|
||||
make.bottom.equalTo(documentPickerView.snp.top)
|
||||
}
|
||||
documentPickerView.snp.makeConstraints { make in
|
||||
make.top.equalTo(view.snp.bottom)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(500)
|
||||
}
|
||||
|
||||
view.isUserInteractionEnabled = true
|
||||
terminateEditGesture = UITapGestureRecognizer(target: self, action: #selector(terminateEditing))
|
||||
view.addGestureRecognizer(terminateEditGesture)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
navigationController!.setNavigationBarHidden(true, animated: animated)
|
||||
documentPickerView.updateDocumentsFromRecentDocs()
|
||||
DispatchQueue.main.async {
|
||||
self.inputBox.textView.becomeFirstResponder()
|
||||
}
|
||||
@@ -56,4 +86,8 @@ class MainViewController: UIViewController {
|
||||
super.viewWillDisappear(animated)
|
||||
navigationController!.setNavigationBarHidden(false, animated: animated)
|
||||
}
|
||||
|
||||
@objc func terminateEditing() {
|
||||
view.endEditing(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
import UIKit
|
||||
|
||||
func performWithAnimation(
|
||||
duration: TimeInterval = 0.5,
|
||||
animations: @escaping () -> Void,
|
||||
completion: @escaping (Bool) -> Void = { _ in }
|
||||
) {
|
||||
UIView.animate(
|
||||
withDuration: 0.5,
|
||||
withDuration: duration,
|
||||
delay: 0,
|
||||
usingSpringWithDamping: 0.8,
|
||||
initialSpringVelocity: 0.8,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// DocumentItem.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 6/25/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct DocumentItem: Hashable {
|
||||
let id: String
|
||||
let title: String
|
||||
let updatedAt: Date?
|
||||
|
||||
init(id: String, title: String, updatedAt: Date? = nil) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.updatedAt = updatedAt
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
|
||||
static func == (lhs: DocumentItem, rhs: DocumentItem) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
@@ -5,31 +5,33 @@
|
||||
// Created by 秋星桥 on 6/24/25.
|
||||
//
|
||||
|
||||
import AffineGraphQL
|
||||
import SnapKit
|
||||
import Then
|
||||
import UIKit
|
||||
|
||||
protocol DocumentPickerViewDelegate: AnyObject {
|
||||
func documentPickerView(_ view: DocumentPickerView, didSelectDocument document: DocumentItem)
|
||||
func documentPickerView(_ view: DocumentPickerView, didSearchWithText text: String)
|
||||
}
|
||||
|
||||
struct DocumentItem {
|
||||
let title: String
|
||||
let icon: UIImage?
|
||||
}
|
||||
|
||||
class DocumentPickerView: UIView {
|
||||
// MARK: - Properties
|
||||
|
||||
weak var delegate: DocumentPickerViewDelegate?
|
||||
|
||||
private var documents: [DocumentItem] = []
|
||||
private var selectedDocumentIds: Set<String> = []
|
||||
private let updateQueue = DispatchQueue(label: "com.affine.documentpicker.update", qos: .userInitiated)
|
||||
private var lastSearchKeyword: String = ""
|
||||
|
||||
// MARK: - DiffableDataSource
|
||||
|
||||
private enum Section {
|
||||
case main
|
||||
}
|
||||
|
||||
private var dataSource: UITableViewDiffableDataSource<Section, DocumentItem>!
|
||||
|
||||
// MARK: - UI Components
|
||||
|
||||
private lazy var containerView = UIView().then {
|
||||
$0.backgroundColor = .white
|
||||
lazy var containerView = UIView().then {
|
||||
$0.backgroundColor = .systemBackground
|
||||
$0.layer.cornerRadius = 10
|
||||
$0.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
$0.layer.shadowColor = UIColor.black.cgColor
|
||||
@@ -38,33 +40,37 @@ class DocumentPickerView: UIView {
|
||||
$0.layer.shadowOpacity = 0.07
|
||||
}
|
||||
|
||||
private lazy var searchContainerView = UIView().then {
|
||||
$0.backgroundColor = .white
|
||||
lazy var searchContainerView = UIView().then {
|
||||
$0.backgroundColor = .systemBackground
|
||||
$0.layer.cornerRadius = 10
|
||||
$0.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
$0.layer.borderWidth = 0.5
|
||||
$0.layer.borderColor = UIColor(hex: 0xE6E6E6)?.cgColor
|
||||
}
|
||||
|
||||
private lazy var searchIconImageView = UIImageView().then {
|
||||
lazy var searchIconImageView = UIImageView().then {
|
||||
$0.image = UIImage(systemName: "magnifyingglass")
|
||||
$0.tintColor = UIColor(hex: 0x141414)
|
||||
$0.tintColor = .affineIconPrimary
|
||||
$0.contentMode = .scaleAspectFit
|
||||
}
|
||||
|
||||
private lazy var searchTextField = UITextField().then {
|
||||
lazy var searchTextField = UITextField().then {
|
||||
$0.placeholder = "Search documents..."
|
||||
$0.font = .systemFont(ofSize: 17, weight: .regular)
|
||||
$0.textColor = UIColor(hex: 0x141414)
|
||||
$0.textColor = .affineTextPrimary
|
||||
$0.backgroundColor = .clear
|
||||
$0.addTarget(self, action: #selector(searchTextChanged), for: .editingChanged)
|
||||
}
|
||||
|
||||
lazy var activityIndicator = UIActivityIndicatorView(style: .medium).then {
|
||||
$0.hidesWhenStopped = true
|
||||
$0.color = .affineIconPrimary
|
||||
}
|
||||
|
||||
private lazy var tableView = UITableView().then {
|
||||
$0.backgroundColor = .white
|
||||
$0.separatorStyle = .none
|
||||
$0.delegate = self
|
||||
$0.dataSource = self
|
||||
$0.register(DocumentTableViewCell.self, forCellReuseIdentifier: "DocumentCell")
|
||||
}
|
||||
|
||||
@@ -72,7 +78,20 @@ class DocumentPickerView: UIView {
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
setupUI()
|
||||
isUserInteractionEnabled = true
|
||||
clipsToBounds = false // for shadow
|
||||
backgroundColor = .systemBackground
|
||||
|
||||
addSubview(containerView)
|
||||
containerView.addSubview(searchContainerView)
|
||||
containerView.addSubview(tableView)
|
||||
|
||||
searchContainerView.addSubview(searchIconImageView)
|
||||
searchContainerView.addSubview(searchTextField)
|
||||
searchContainerView.addSubview(activityIndicator)
|
||||
|
||||
setupConstraints()
|
||||
setupDataSource()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
@@ -82,24 +101,9 @@ class DocumentPickerView: UIView {
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
private func setupUI() {
|
||||
backgroundColor = .systemBackground
|
||||
|
||||
addSubview(containerView)
|
||||
containerView.addSubview(searchContainerView)
|
||||
containerView.addSubview(tableView)
|
||||
|
||||
searchContainerView.addSubview(searchIconImageView)
|
||||
searchContainerView.addSubview(searchTextField)
|
||||
|
||||
setupConstraints()
|
||||
}
|
||||
|
||||
private func setupConstraints() {
|
||||
containerView.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
make.width.equalTo(393)
|
||||
make.height.lessThanOrEqualTo(500)
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
|
||||
searchContainerView.snp.makeConstraints { make in
|
||||
@@ -116,8 +120,14 @@ class DocumentPickerView: UIView {
|
||||
|
||||
searchTextField.snp.makeConstraints { make in
|
||||
make.leading.equalTo(searchIconImageView.snp.trailing).offset(DocumentTableViewCell.spacing)
|
||||
make.trailing.equalTo(activityIndicator.snp.leading).offset(-DocumentTableViewCell.cellInset)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
|
||||
activityIndicator.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-DocumentTableViewCell.cellInset)
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.height.equalTo(DocumentTableViewCell.iconSize)
|
||||
}
|
||||
|
||||
tableView.snp.makeConstraints { make in
|
||||
@@ -126,15 +136,59 @@ class DocumentPickerView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
private func setupDataSource() {
|
||||
dataSource = UITableViewDiffableDataSource<Section, DocumentItem>(tableView: tableView) { [weak self] tableView, indexPath, document in
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "DocumentCell", for: indexPath) as! DocumentTableViewCell
|
||||
let isSelected = self?.selectedDocumentIds.contains(document.id) ?? false
|
||||
cell.configure(with: document, isSelected: isSelected)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
func updateDocuments(_ documents: [DocumentItem]) {
|
||||
self.documents = documents
|
||||
tableView.reloadData()
|
||||
updateQueue.async { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
let tableHeight = min(CGFloat(documents.count) * 37.11 + 44, 500)
|
||||
containerView.snp.updateConstraints { make in
|
||||
make.height.lessThanOrEqualTo(tableHeight)
|
||||
DispatchQueue.main.async {
|
||||
self.documents = documents
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, DocumentItem>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(documents)
|
||||
self.dataSource.apply(snapshot, animatingDifferences: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateDocumentsFromRecentDocs() {
|
||||
guard searchTextField.text?.isEmpty ?? true else {
|
||||
return
|
||||
}
|
||||
guard let workspaceId = IntelligentContext.shared.webViewMetadata[.currentWorkspaceId] as? String,
|
||||
!workspaceId.isEmpty
|
||||
else {
|
||||
activityIndicator.stopAnimating()
|
||||
return
|
||||
}
|
||||
activityIndicator.startAnimating()
|
||||
QLService.shared.fetchRecentlyUpdatedDocs(workspaceId: workspaceId, first: 20) { [weak self] docs in
|
||||
guard let self else { return }
|
||||
DispatchQueue.main.async {
|
||||
self.activityIndicator.stopAnimating()
|
||||
self.updateDocuments(docs.compactMap { DocumentItem(
|
||||
id: $0.id,
|
||||
title: $0.title ?? "Unknown Document",
|
||||
updatedAt: $0.updatedAt?.decoded
|
||||
) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setSelectedDocuments(_ documentAttachments: [DocumentAttachment]) {
|
||||
selectedDocumentIds = Set(documentAttachments.map(\.documentID))
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,21 +196,56 @@ class DocumentPickerView: UIView {
|
||||
|
||||
@objc private func searchTextChanged() {
|
||||
guard let text = searchTextField.text else { return }
|
||||
delegate?.documentPickerView(self, didSearchWithText: text)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
lastSearchKeyword = trimmedText
|
||||
|
||||
extension DocumentPickerView: UITableViewDataSource {
|
||||
func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
|
||||
documents.count
|
||||
NSObject.cancelPreviousPerformRequests(
|
||||
withTarget: self,
|
||||
selector: #selector(performCloudIndexerDocumentSearch(_:)),
|
||||
object: nil
|
||||
)
|
||||
|
||||
if trimmedText.isEmpty {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.activityIndicator.stopAnimating()
|
||||
}
|
||||
updateDocumentsFromRecentDocs()
|
||||
} else {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.activityIndicator.startAnimating()
|
||||
}
|
||||
perform(#selector(performCloudIndexerDocumentSearch(_:)), with: trimmedText, afterDelay: 0.25)
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "DocumentCell", for: indexPath) as! DocumentTableViewCell
|
||||
cell.configure(with: documents[indexPath.row])
|
||||
return cell
|
||||
@objc func performCloudIndexerDocumentSearch(_ keyword: String) {
|
||||
let trimmedKeyword = keyword.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
guard !trimmedKeyword.isEmpty else { return }
|
||||
guard trimmedKeyword == lastSearchKeyword else { return }
|
||||
guard let workspaceId = IntelligentContext.shared.webViewMetadata[.currentWorkspaceId] as? String,
|
||||
!workspaceId.isEmpty
|
||||
else {
|
||||
activityIndicator.stopAnimating()
|
||||
return
|
||||
}
|
||||
|
||||
QLService.shared.searchDocuments(
|
||||
workspaceId: workspaceId,
|
||||
keyword: trimmedKeyword,
|
||||
limit: 20
|
||||
) { [weak self] searchResults in
|
||||
guard let self, lastSearchKeyword == trimmedKeyword else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.activityIndicator.stopAnimating()
|
||||
self.updateDocuments(searchResults.map {
|
||||
.init(id: $0.docId, title: $0.title, updatedAt: $0.updatedAt.decoded)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +258,7 @@ extension DocumentPickerView: UITableViewDelegate {
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
let document = documents[indexPath.row]
|
||||
guard let document = dataSource.itemIdentifier(for: indexPath) else { return }
|
||||
delegate?.documentPickerView(self, didSelectDocument: document)
|
||||
}
|
||||
}
|
||||
@@ -185,14 +274,14 @@ extension DocumentPickerView: UITableViewDelegate {
|
||||
let view = DocumentPickerView()
|
||||
|
||||
let mockDocuments = [
|
||||
DocumentItem(title: "Project Proposal.docx", icon: UIImage(systemName: "doc.text")),
|
||||
DocumentItem(title: "Budget Analysis.xlsx", icon: UIImage(systemName: "tablecells")),
|
||||
DocumentItem(title: "Meeting Notes.pdf", icon: UIImage(systemName: "doc.richtext")),
|
||||
DocumentItem(title: "Design Guidelines.sketch", icon: UIImage(systemName: "paintbrush")),
|
||||
DocumentItem(title: "Code Review.md", icon: UIImage(systemName: "doc.plaintext")),
|
||||
DocumentItem(title: "User Research.pptx", icon: UIImage(systemName: "doc.on.doc")),
|
||||
DocumentItem(title: "Technical Specification.docx", icon: UIImage(systemName: "doc.text")),
|
||||
DocumentItem(title: "Database Schema.sql", icon: UIImage(systemName: "cylinder.split.1x2")),
|
||||
DocumentItem(id: "1", title: "Project Proposal.docx"),
|
||||
DocumentItem(id: "2", title: "Budget Analysis.xlsx"),
|
||||
DocumentItem(id: "3", title: "Meeting Notes.pdf"),
|
||||
DocumentItem(id: "4", title: "Design Guidelines.sketch"),
|
||||
DocumentItem(id: "5", title: "Code Review.md"),
|
||||
DocumentItem(id: "6", title: "User Research.pptx"),
|
||||
DocumentItem(id: "7", title: "Technical Specification.docx"),
|
||||
DocumentItem(id: "8", title: "Database Schema.sql"),
|
||||
]
|
||||
|
||||
view.updateDocuments(mockDocuments)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// DocumentPickerViewDelegate.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 6/25/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol DocumentPickerViewDelegate: AnyObject {
|
||||
func documentPickerView(_ view: DocumentPickerView, didSelectDocument document: DocumentItem)
|
||||
}
|
||||
@@ -25,6 +25,13 @@ class DocumentTableViewCell: UITableViewCell {
|
||||
$0.textAlignment = .left
|
||||
}
|
||||
|
||||
private lazy var checkmarkImageView = UIImageView().then {
|
||||
$0.image = UIImage(systemName: "checkmark.circle.fill")
|
||||
$0.tintColor = .systemBlue
|
||||
$0.contentMode = .scaleAspectFit
|
||||
$0.isHidden = true
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
setupUI()
|
||||
@@ -39,8 +46,11 @@ class DocumentTableViewCell: UITableViewCell {
|
||||
backgroundColor = .white
|
||||
selectionStyle = .none
|
||||
|
||||
contentView.clipsToBounds = true
|
||||
|
||||
contentView.addSubview(iconImageView)
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(checkmarkImageView)
|
||||
|
||||
iconImageView.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(Self.cellInset)
|
||||
@@ -50,13 +60,20 @@ class DocumentTableViewCell: UITableViewCell {
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(iconImageView.snp.trailing).offset(Self.spacing)
|
||||
make.trailing.equalTo(checkmarkImageView.snp.leading).offset(-Self.spacing)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
|
||||
checkmarkImageView.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-Self.cellInset)
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.height.equalTo(Self.iconSize)
|
||||
}
|
||||
}
|
||||
|
||||
func configure(with document: DocumentItem) {
|
||||
iconImageView.image = document.icon ?? UIImage(systemName: "doc.text")
|
||||
func configure(with document: DocumentItem, isSelected: Bool = false) {
|
||||
iconImageView.image = UIImage(systemName: "doc.text")
|
||||
titleLabel.text = document.title
|
||||
checkmarkImageView.isHidden = !isSelected
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,6 @@ import SnapKit
|
||||
import Then
|
||||
import UIKit
|
||||
|
||||
protocol FileAttachmentHeaderViewDelegate: AnyObject {
|
||||
func headerViewDidPickMore(_ headerView: FileAttachmentHeaderView)
|
||||
func headerViewDidTapManagement(_ headerView: FileAttachmentHeaderView)
|
||||
}
|
||||
|
||||
final class FileAttachmentHeaderView: UIView {
|
||||
// MARK: - Properties
|
||||
|
||||
@@ -131,7 +126,17 @@ final class FileAttachmentHeaderView: UIView {
|
||||
// MARK: - Public Methods
|
||||
|
||||
func updateContent(attachmentCount: Int, docsCount: Int) {
|
||||
primaryLabel.text = "\(attachmentCount) attachment, \(docsCount) AFFiNE docs"
|
||||
var components: [String] = []
|
||||
|
||||
if attachmentCount > 0 {
|
||||
components.append("\(attachmentCount) attachment\(attachmentCount > 1 ? "s" : "")")
|
||||
}
|
||||
|
||||
if docsCount > 0 {
|
||||
components.append("\(docsCount) AFFiNE doc\(docsCount > 1 ? "s" : "")")
|
||||
}
|
||||
|
||||
primaryLabel.text = components.joined(separator: ", ")
|
||||
}
|
||||
|
||||
func setIconImage(_ image: UIImage?) {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FileAttachmentHeaderViewDelegate.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 6/25/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol FileAttachmentHeaderViewDelegate: AnyObject {
|
||||
func headerViewDidPickMore(_ headerView: FileAttachmentHeaderView)
|
||||
func headerViewDidTapManagement(_ headerView: FileAttachmentHeaderView)
|
||||
}
|
||||
@@ -9,10 +9,6 @@ import SnapKit
|
||||
import Then
|
||||
import UIKit
|
||||
|
||||
protocol ImageAttachmentBarDelegate: AnyObject {
|
||||
func inputBoxImageBar(_ imageBar: ImageAttachmentBar, didRemoveImageWithId id: UUID)
|
||||
}
|
||||
|
||||
class ImageAttachmentBar: UICollectionView {
|
||||
weak var imageBarDelegate: ImageAttachmentBarDelegate?
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// ImageAttachmentBarDelegate.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 6/25/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol ImageAttachmentBarDelegate: AnyObject {
|
||||
func inputBoxImageBar(_ imageBar: ImageAttachmentBar, didRemoveImageWithId id: UUID)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// InputBox+Delegates.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 6/18/25.
|
||||
//
|
||||
|
||||
import SwifterSwift
|
||||
import UIKit
|
||||
|
||||
extension InputBox: ImageAttachmentBarDelegate {
|
||||
func inputBoxImageBar(_: ImageAttachmentBar, didRemoveImageWithId id: ImageAttachment.ID) {
|
||||
performWithAnimation { [self] in
|
||||
viewModel.removeImageAttachment(withId: id)
|
||||
layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension InputBox: FileAttachmentHeaderViewDelegate {
|
||||
func headerViewDidPickMore(_: FileAttachmentHeaderView) {
|
||||
delegate?.inputBoxDidSelectAttachFiles(self)
|
||||
}
|
||||
|
||||
func headerViewDidTapManagement(_: FileAttachmentHeaderView) {
|
||||
let controller = AttachmentManagementController(delegate: self)
|
||||
controller.set(fileAttachments: viewModel.fileAttachments)
|
||||
controller.set(documentAttachments: viewModel.documentAttachments)
|
||||
parentViewController?.present(controller, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension InputBox: AttachmentManagementControllerDelegate {
|
||||
func deleteFileAttachment(controller: AttachmentManagementController, _ attachment: FileAttachment) {
|
||||
viewModel.removeFileAttachment(withId: attachment.id)
|
||||
controller.set(fileAttachments: viewModel.fileAttachments)
|
||||
layoutIfNeeded()
|
||||
}
|
||||
|
||||
func deleteDocumentAttachment(controller: AttachmentManagementController, _ attachment: DocumentAttachment) {
|
||||
viewModel.removeDocumentAttachment(withId: attachment.id)
|
||||
controller.set(documentAttachments: viewModel.documentAttachments)
|
||||
layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
extension InputBox: InputBoxFunctionBarDelegate {
|
||||
func functionBarDidTapTakePhoto(_: InputBoxFunctionBar) {
|
||||
delegate?.inputBoxDidSelectTakePhoto(self)
|
||||
}
|
||||
|
||||
func functionBarDidTapPhotoLibrary(_: InputBoxFunctionBar) {
|
||||
delegate?.inputBoxDidSelectPhotoLibrary(self)
|
||||
}
|
||||
|
||||
func functionBarDidTapAttachFiles(_: InputBoxFunctionBar) {
|
||||
delegate?.inputBoxDidSelectAttachFiles(self)
|
||||
}
|
||||
|
||||
func functionBarDidTapEmbedDocs(_: InputBoxFunctionBar) {
|
||||
delegate?.inputBoxDidSelectEmbedDocs(self)
|
||||
}
|
||||
|
||||
func functionBarDidTapTool(_: InputBoxFunctionBar) {
|
||||
viewModel.toggleTool()
|
||||
}
|
||||
|
||||
func functionBarDidTapNetwork(_: InputBoxFunctionBar) {
|
||||
viewModel.toggleNetwork()
|
||||
}
|
||||
|
||||
func functionBarDidTapDeepThinking(_: InputBoxFunctionBar) {
|
||||
viewModel.toggleDeepThinking()
|
||||
}
|
||||
|
||||
func functionBarDidTapSend(_: InputBoxFunctionBar) {
|
||||
delegate?.inputBoxDidSend(self)
|
||||
}
|
||||
}
|
||||
|
||||
extension InputBox: UITextViewDelegate {
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
viewModel.updateText(textView.text ?? "")
|
||||
delegate?.inputBoxTextDidChange(textView.text ?? "")
|
||||
updatePlaceholderVisibility()
|
||||
updateTextViewHeight()
|
||||
}
|
||||
}
|
||||
@@ -291,6 +291,13 @@ class InputBox: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
public func addDocumentAttachment(_ documentAttachment: DocumentAttachment) {
|
||||
performWithAnimation { [self] in
|
||||
viewModel.addDocumentAttachment(documentAttachment)
|
||||
layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
public var inputBoxData: InputBoxData {
|
||||
viewModel.prepareSendData()
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
//
|
||||
// InputBoxDelegate.swift
|
||||
// InputBoxDelegate 2.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 6/18/25.
|
||||
// Created by 秋星桥 on 6/25/25.
|
||||
//
|
||||
|
||||
import SwifterSwift
|
||||
import UIKit
|
||||
import Foundation
|
||||
|
||||
protocol InputBoxDelegate: AnyObject {
|
||||
func inputBoxDidSelectTakePhoto(_ inputBox: InputBox)
|
||||
@@ -16,82 +15,3 @@ protocol InputBoxDelegate: AnyObject {
|
||||
func inputBoxDidSend(_ inputBox: InputBox)
|
||||
func inputBoxTextDidChange(_ text: String)
|
||||
}
|
||||
|
||||
extension InputBox: ImageAttachmentBarDelegate {
|
||||
func inputBoxImageBar(_: ImageAttachmentBar, didRemoveImageWithId id: ImageAttachment.ID) {
|
||||
performWithAnimation { [self] in
|
||||
viewModel.removeImageAttachment(withId: id)
|
||||
layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension InputBox: FileAttachmentHeaderViewDelegate {
|
||||
func headerViewDidPickMore(_: FileAttachmentHeaderView) {
|
||||
delegate?.inputBoxDidSelectAttachFiles(self)
|
||||
}
|
||||
|
||||
func headerViewDidTapManagement(_: FileAttachmentHeaderView) {
|
||||
let controller = AttachmentManagementController(delegate: self)
|
||||
controller.set(fileAttachments: viewModel.fileAttachments)
|
||||
controller.set(documentAttachments: viewModel.documentAttachments)
|
||||
parentViewController?.present(controller, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension InputBox: AttachmentManagementControllerDelegate {
|
||||
func deleteFileAttachment(controller: AttachmentManagementController, _ attachment: FileAttachment) {
|
||||
viewModel.removeFileAttachment(withId: attachment.id)
|
||||
controller.set(fileAttachments: viewModel.fileAttachments)
|
||||
layoutIfNeeded()
|
||||
}
|
||||
|
||||
func deleteDocumentAttachment(controller: AttachmentManagementController, _ attachment: DocumentAttachment) {
|
||||
viewModel.removeDocumentAttachment(withId: attachment.id)
|
||||
controller.set(documentAttachments: viewModel.documentAttachments)
|
||||
layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
extension InputBox: InputBoxFunctionBarDelegate {
|
||||
func functionBarDidTapTakePhoto(_: InputBoxFunctionBar) {
|
||||
delegate?.inputBoxDidSelectTakePhoto(self)
|
||||
}
|
||||
|
||||
func functionBarDidTapPhotoLibrary(_: InputBoxFunctionBar) {
|
||||
delegate?.inputBoxDidSelectPhotoLibrary(self)
|
||||
}
|
||||
|
||||
func functionBarDidTapAttachFiles(_: InputBoxFunctionBar) {
|
||||
delegate?.inputBoxDidSelectAttachFiles(self)
|
||||
}
|
||||
|
||||
func functionBarDidTapEmbedDocs(_: InputBoxFunctionBar) {
|
||||
delegate?.inputBoxDidSelectEmbedDocs(self)
|
||||
}
|
||||
|
||||
func functionBarDidTapTool(_: InputBoxFunctionBar) {
|
||||
viewModel.toggleTool()
|
||||
}
|
||||
|
||||
func functionBarDidTapNetwork(_: InputBoxFunctionBar) {
|
||||
viewModel.toggleNetwork()
|
||||
}
|
||||
|
||||
func functionBarDidTapDeepThinking(_: InputBoxFunctionBar) {
|
||||
viewModel.toggleDeepThinking()
|
||||
}
|
||||
|
||||
func functionBarDidTapSend(_: InputBoxFunctionBar) {
|
||||
delegate?.inputBoxDidSend(self)
|
||||
}
|
||||
}
|
||||
|
||||
extension InputBox: UITextViewDelegate {
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
viewModel.updateText(textView.text ?? "")
|
||||
delegate?.inputBoxTextDidChange(textView.text ?? "")
|
||||
updatePlaceholderVisibility()
|
||||
updateTextViewHeight()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,6 @@ import SnapKit
|
||||
import Then
|
||||
import UIKit
|
||||
|
||||
protocol InputBoxFunctionBarDelegate: AnyObject {
|
||||
func functionBarDidTapTakePhoto(_ functionBar: InputBoxFunctionBar)
|
||||
func functionBarDidTapPhotoLibrary(_ functionBar: InputBoxFunctionBar)
|
||||
func functionBarDidTapAttachFiles(_ functionBar: InputBoxFunctionBar)
|
||||
func functionBarDidTapEmbedDocs(_ functionBar: InputBoxFunctionBar)
|
||||
func functionBarDidTapTool(_ functionBar: InputBoxFunctionBar)
|
||||
func functionBarDidTapNetwork(_ functionBar: InputBoxFunctionBar)
|
||||
func functionBarDidTapDeepThinking(_ functionBar: InputBoxFunctionBar)
|
||||
func functionBarDidTapSend(_ functionBar: InputBoxFunctionBar)
|
||||
}
|
||||
|
||||
private let unselectedColor: UIColor = .affineIconPrimary
|
||||
private let selectedColor: UIColor = .affineIconActivated
|
||||
|
||||
@@ -164,7 +153,7 @@ class InputBoxFunctionBar: UIView {
|
||||
}
|
||||
|
||||
let embedDocsAction = UIAction(
|
||||
title: "Embed AFFINE Docs",
|
||||
title: "Add AFFiNE Docs",
|
||||
image: UIImage.affinePage
|
||||
) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// InputBoxFunctionBarDelegate.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 6/25/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol InputBoxFunctionBarDelegate: AnyObject {
|
||||
func functionBarDidTapTakePhoto(_ functionBar: InputBoxFunctionBar)
|
||||
func functionBarDidTapPhotoLibrary(_ functionBar: InputBoxFunctionBar)
|
||||
func functionBarDidTapAttachFiles(_ functionBar: InputBoxFunctionBar)
|
||||
func functionBarDidTapEmbedDocs(_ functionBar: InputBoxFunctionBar)
|
||||
func functionBarDidTapTool(_ functionBar: InputBoxFunctionBar)
|
||||
func functionBarDidTapNetwork(_ functionBar: InputBoxFunctionBar)
|
||||
func functionBarDidTapDeepThinking(_ functionBar: InputBoxFunctionBar)
|
||||
func functionBarDidTapSend(_ functionBar: InputBoxFunctionBar)
|
||||
}
|
||||
@@ -12,4 +12,13 @@ public struct DocumentAttachment: Identifiable, Equatable, Hashable, Codable {
|
||||
public var title: String = ""
|
||||
public var workspaceID: String = ""
|
||||
public var documentID: String = ""
|
||||
public var updatedAt: Date?
|
||||
|
||||
public init(id: UUID = .init(), title: String = "", workspaceID: String = "", documentID: String = "", updatedAt: Date? = nil) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.workspaceID = workspaceID
|
||||
self.documentID = documentID
|
||||
self.updatedAt = updatedAt
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,6 @@ import SnapKit
|
||||
import Then
|
||||
import UIKit
|
||||
|
||||
protocol MainHeaderViewDelegate: AnyObject {
|
||||
func mainHeaderViewDidTapClose()
|
||||
func mainHeaderViewDidTapDropdown()
|
||||
func mainHeaderViewDidTapMenu()
|
||||
}
|
||||
|
||||
class MainHeaderView: UIView {
|
||||
weak var delegate: MainHeaderViewDelegate?
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// MainHeaderViewDelegate.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 6/25/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol MainHeaderViewDelegate: AnyObject {
|
||||
func mainHeaderViewDidTapClose()
|
||||
func mainHeaderViewDidTapDropdown()
|
||||
func mainHeaderViewDidTapMenu()
|
||||
}
|
||||
Reference in New Issue
Block a user