WIP: 打个框架先

This commit is contained in:
砍砍
2024-11-18 22:06:39 +08:00
parent 401106203c
commit 60275046b4
43 changed files with 1312 additions and 30 deletions
@@ -7,10 +7,12 @@
objects = {
/* Begin PBXBuildFile section */
505B0A342CEB3FB10092FC35 /* Intelligents in Frameworks */ = {isa = PBXBuildFile; productRef = 505B0A332CEB3FB10092FC35 /* Intelligents */; };
505B0A362CEB48B10092FC35 /* RootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505B0A352CEB48B10092FC35 /* RootViewController.swift */; };
9D6A85332CCF6DA700DAB35F /* HashcashPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */; };
9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE172CCB9876006677DB /* CookieManager.swift */; };
9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE182CCB9876006677DB /* CookiePlugin.swift */; };
9D90BE272CCB9876006677DB /* AFFiNEViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE1B2CCB9876006677DB /* AFFiNEViewController.swift */; };
9D90BE272CCB9876006677DB /* AffineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE1B2CCB9876006677DB /* AffineViewController.swift */; };
9D90BE282CCB9876006677DB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE1C2CCB9876006677DB /* AppDelegate.swift */; };
9D90BE292CCB9876006677DB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE1D2CCB9876006677DB /* Assets.xcassets */; };
9D90BE2A2CCB9876006677DB /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE1E2CCB9876006677DB /* capacitor.config.json */; };
@@ -22,10 +24,12 @@
/* Begin PBXFileReference section */
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
505B0A312CEB3FAB0092FC35 /* Intelligents */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Intelligents; sourceTree = "<group>"; };
505B0A352CEB48B10092FC35 /* RootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewController.swift; sourceTree = "<group>"; };
9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashcashPlugin.swift; sourceTree = "<group>"; };
9D90BE172CCB9876006677DB /* CookieManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManager.swift; sourceTree = "<group>"; };
9D90BE182CCB9876006677DB /* CookiePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookiePlugin.swift; sourceTree = "<group>"; };
9D90BE1B2CCB9876006677DB /* AFFiNEViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AFFiNEViewController.swift; sourceTree = "<group>"; };
9D90BE1B2CCB9876006677DB /* AffineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AffineViewController.swift; sourceTree = "<group>"; };
9D90BE1C2CCB9876006677DB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
9D90BE1D2CCB9876006677DB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
9D90BE1E2CCB9876006677DB /* capacitor.config.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
@@ -43,6 +47,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
505B0A342CEB3FB10092FC35 /* Intelligents in Frameworks */,
C4C413792CBE705D00337889 /* Pods_App.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -65,9 +70,7 @@
504EC3051FED79650016851F /* Products */,
7F8756D8B27F46E3366F6CEA /* Pods */,
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
9D6A85312CCF6D6B00DAB35F /* Recovered References */,
);
indentWidth = 2;
sourceTree = "<group>";
tabWidth = 2;
};
@@ -79,6 +82,14 @@
name = Products;
sourceTree = "<group>";
};
505B0A322CEB3FAB0092FC35 /* Packages */ = {
isa = PBXGroup;
children = (
505B0A312CEB3FAB0092FC35 /* Intelligents */,
);
path = Packages;
sourceTree = "<group>";
};
7F8756D8B27F46E3366F6CEA /* Pods */ = {
isa = PBXGroup;
children = (
@@ -98,19 +109,21 @@
path = Cookie;
sourceTree = "<group>";
};
9D90BE1A2CCB9876006677DB /* plugins */ = {
9D90BE1A2CCB9876006677DB /* Plugins */ = {
isa = PBXGroup;
children = (
9D90BE192CCB9876006677DB /* Cookie */,
);
path = plugins;
path = Plugins;
sourceTree = "<group>";
};
9D90BE242CCB9876006677DB /* App */ = {
isa = PBXGroup;
children = (
9D90BE1A2CCB9876006677DB /* plugins */,
9D90BE1B2CCB9876006677DB /* AFFiNEViewController.swift */,
505B0A322CEB3FAB0092FC35 /* Packages */,
9D90BE1A2CCB9876006677DB /* Plugins */,
9D90BE1B2CCB9876006677DB /* AffineViewController.swift */,
505B0A352CEB48B10092FC35 /* RootViewController.swift */,
9D90BE1C2CCB9876006677DB /* AppDelegate.swift */,
9D90BE1D2CCB9876006677DB /* Assets.xcassets */,
9D90BE1E2CCB9876006677DB /* capacitor.config.json */,
@@ -236,8 +249,9 @@
files = (
9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */,
9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */,
505B0A362CEB48B10092FC35 /* RootViewController.swift in Sources */,
9D6A85332CCF6DA700DAB35F /* HashcashPlugin.swift in Sources */,
9D90BE272CCB9876006677DB /* AFFiNEViewController.swift in Sources */,
9D90BE272CCB9876006677DB /* AffineViewController.swift in Sources */,
9D90BE282CCB9876006677DB /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -384,7 +398,7 @@
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 73YMMDVT2M;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -414,7 +428,7 @@
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 73YMMDVT2M;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -454,6 +468,13 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCSwiftPackageProductDependency section */
505B0A332CEB3FB10092FC35 /* Intelligents */ = {
isa = XCSwiftPackageProductDependency;
productName = Intelligents;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 504EC2FC1FED79650016851F /* Project object */;
}
@@ -1,15 +0,0 @@
import UIKit
import Capacitor
class AFFiNEViewController: CAPBridgeViewController {
override func viewDidLoad() {
super.viewDidLoad()
webView?.allowsBackForwardNavigationGestures = true
}
override func capacitorDidLoad() {
bridge?.registerPluginInstance(CookiePlugin())
bridge?.registerPluginInstance(HashcashPlugin())
}
}
@@ -0,0 +1,28 @@
import UIKit
import Capacitor
import Intelligents
class AFFiNEViewController: CAPBridgeViewController {
override func viewDidLoad() {
super.viewDidLoad()
webView?.allowsBackForwardNavigationGestures = true
self.navigationController?.navigationBar.isHidden = true
installIntelligentsButton()
}
override func capacitorDidLoad() {
bridge?.registerPluginInstance(CookiePlugin())
bridge?.registerPluginInstance(HashcashPlugin())
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
navigationController?.setNavigationBarHidden(false, animated: animated)
self.presentIntelligentsButton()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
dismissIntelligentsButton()
}
}
@@ -1,15 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23094" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23084"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
</dependencies>
<scenes>
<!--FiNE View Controller-->
<!--Root View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="AFFiNEViewController" customModule="App" customModuleProvider="target" sceneMemberID="viewController"/>
<viewController id="BYZ-38-t0r" customClass="RootViewController" customModule="App" customModuleProvider="target" sceneMemberID="viewController"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="140" y="-1"/>
@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleAllowMixedLocalizations</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
@@ -0,0 +1,19 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "Intelligents",
defaultLocalization: "en",
platforms: [
.iOS(.v14),
.macCatalyst(.v14),
],
products: [
.library(name: "Intelligents", targets: ["Intelligents"]),
],
targets: [
.target(name: "Intelligents"),
]
)
@@ -0,0 +1,13 @@
//
// Constant.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
enum Constant {
static let affineTabbarHeight: CGFloat = 44
static let affineTintColor: UIColor = .init(red: 30 / 255, green: 150 / 255, blue: 235 / 255, alpha: 1.0)
}
@@ -0,0 +1,19 @@
//
// Ext+String.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import Foundation
extension String {
func localized() -> String {
let ans = NSLocalizedString(self, bundle: Bundle.module, comment: "")
guard !ans.isEmpty else {
assertionFailure()
return self
}
return ans
}
}
@@ -0,0 +1,37 @@
//
// Ext+UIView.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
extension UIView {
var parentViewController: UIViewController? {
var responder: UIResponder? = self
while responder != nil {
if let responder = responder as? UIViewController {
return responder
}
responder = responder?.next
}
return nil
}
#if DEBUG
func debugFrame() {
layer.borderWidth = 1
layer.borderColor = [
UIColor.red,
.green,
.blue,
.yellow,
.cyan,
.magenta,
.orange,
].map(\.cgColor).randomElement()
subviews.forEach { $0.debugFrame() }
}
#endif
}
@@ -0,0 +1,38 @@
//
// Ext+UIViewController.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
extension UIViewController {
func presentIntoCurrentContext(withTargetController targetController: UIViewController, animated: Bool = true) {
if let nav = self as? UINavigationController {
nav.pushViewController(targetController, animated: animated)
} else if let nav = navigationController {
nav.pushViewController(targetController, animated: animated)
} else {
present(targetController, animated: animated, completion: nil)
}
}
func dismissInContext() {
if let nav = navigationController {
nav.popViewController(animated: true)
} else {
dismiss(animated: true, completion: nil)
}
}
func hideKeyboardWhenTappedAround() {
let tap = UITapGestureRecognizer(target: self, action: #selector(UIViewController.dismissKeyboard))
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
}
@objc func dismissKeyboard() {
view.endEditing(true)
}
}
@@ -0,0 +1,14 @@
//
// Ext+print.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import Foundation
public func print(_ items: Any..., separator: String = " ", terminator: String = "\n") {
#if DEBUG
Swift.print(items, separator: separator, terminator: terminator)
#endif
}
@@ -0,0 +1,4 @@
// The Swift Programming Language
// https://docs.swift.org/swift-book
enum Intelligents {}
@@ -0,0 +1,82 @@
//
// IntelligentsButton+Control.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
public extension UIViewController {
@discardableResult
func installIntelligentsButton() -> IntelligentsButton {
print("[*] \(#function)")
if let button = findIntelligentsButton() { return button }
let button = IntelligentsButton()
view.addSubview(button)
view.bringSubviewToFront(button)
button.translatesAutoresizingMaskIntoConstraints = false
[
button.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20),
button.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20 - Constant.affineTabbarHeight),
button.widthAnchor.constraint(equalToConstant: 50),
button.heightAnchor.constraint(equalToConstant: 50),
].forEach { $0.isActive = true }
button.transform = .init(scaleX: 0, y: 0)
view.layoutIfNeeded()
return button
}
private func findIntelligentsButton() -> IntelligentsButton? {
for subview in view.subviews { // for for depth 1
if let button = subview as? IntelligentsButton {
return button
}
}
return nil
}
func presentIntelligentsButton() {
guard let button = findIntelligentsButton() else { return }
print("[*] \(button) is calling \(#function)")
button.alpha = 0
button.isHidden = false
button.setNeedsLayout()
view.layoutIfNeeded()
UIView.animate(
withDuration: 0.5,
delay: 0,
usingSpringWithDamping: 1.0,
initialSpringVelocity: 0.8
) {
button.alpha = 1
button.transform = .identity
button.setNeedsLayout()
self.view.layoutIfNeeded()
}
}
func dismissIntelligentsButton() {
guard let button = findIntelligentsButton() else { return }
print("[*] \(button) is calling \(#function)")
button.setNeedsLayout()
view.layoutIfNeeded()
UIView.animate(
withDuration: 0.5,
delay: 0,
usingSpringWithDamping: 1.0,
initialSpringVelocity: 0.8
) {
button.alpha = 0
button.transform = .init(scaleX: 0, y: 0)
button.setNeedsLayout()
self.view.layoutIfNeeded()
} completion: { _ in
button.isHidden = true
}
}
}
@@ -0,0 +1,74 @@
//
// IntelligentsButton.swift
//
//
// Created by on 2024/11/18.
//
import ColorfulX
import UIKit
// floating button to open intelligent panel
public class IntelligentsButton: UIView {
let image = UIImageView()
let background = UIView()
public init() {
super.init(frame: .zero)
background.backgroundColor = .white
addSubview(background)
background.translatesAutoresizingMaskIntoConstraints = false
[
background.leadingAnchor.constraint(equalTo: leadingAnchor),
background.trailingAnchor.constraint(equalTo: trailingAnchor),
background.topAnchor.constraint(equalTo: topAnchor),
background.bottomAnchor.constraint(equalTo: bottomAnchor),
].forEach { $0.isActive = true }
image.image = .init(named: "spark", in: .module, with: .none)
image.contentMode = .scaleAspectFit
image.tintColor = Constant.affineTintColor
addSubview(image)
let imageInsetValue: CGFloat = 12
image.translatesAutoresizingMaskIntoConstraints = false
[
image.leadingAnchor.constraint(equalTo: leadingAnchor, constant: imageInsetValue),
image.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -imageInsetValue),
image.topAnchor.constraint(equalTo: topAnchor, constant: imageInsetValue),
image.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -imageInsetValue),
].forEach { $0.isActive = true }
// layer.shadowColor = UIColor.black.withAlphaComponent(0.1).cgColor
// layer.shadowOffset = CGSize(width: 0, height: 0)
// layer.shadowRadius = 8
clipsToBounds = true
layer.borderWidth = 2
layer.borderColor = UIColor.gray.withAlphaComponent(0.1).cgColor
let tap = UITapGestureRecognizer(target: self, action: #selector(tapped))
addGestureRecognizer(tap)
isUserInteractionEnabled = true
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
override public func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.width / 2
}
@objc func tapped() {
guard let controller = parentViewController else {
assertionFailure()
return
}
let targetController = IntelligentsChatController()
controller.presentIntoCurrentContext(withTargetController: targetController)
}
}
@@ -0,0 +1,39 @@
//
// ChatTableView+BaseCell.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
extension ChatTableView {
class BaseCell: UITableViewCell {
var inset: UIEdgeInsets = .zero {
didSet { setNeedsLayout() }
}
let containerView = UIView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
contentView.addSubview(containerView)
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
override func layoutSubviews() {
super.layoutSubviews()
containerView.frame = contentView.bounds.inset(by: inset)
}
func update(via _: AnyObject?) {
assertionFailure() // "should be override"
}
}
}
@@ -0,0 +1,54 @@
//
// ChatTableView+Data.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
extension ChatTableView {
struct DataElement {
enum CellType: String, CaseIterable {
case base
}
let type: CellType
let object: AnyObject?
init(type: CellType, object: AnyObject?) {
self.type = type
self.object = object
}
}
}
extension ChatTableView.DataElement.CellType {
var cellClassType: ChatTableView.BaseCell.Type {
switch self {
case .base:
ChatTableView.BaseCell.self
}
}
var cellIdentifier: String {
NSStringFromClass(cellClassType)
}
}
extension ChatTableView: UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in _: UITableView) -> Int {
1
}
func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
dataSource.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: dataSource[indexPath.row].type.cellIdentifier, for: indexPath) as! BaseCell
let object = dataSource[indexPath.row].object
cell.update(via: object)
return cell
}
}
@@ -0,0 +1,48 @@
//
// ChatTableView.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
class ChatTableView: UIView {
let tableView = UITableView()
var dataSource: [DataElement] = []
init() {
super.init(frame: .zero)
for eachCase in DataElement.CellType.allCases {
let cellClass = eachCase.cellClassType
tableView.register(cellClass, forCellReuseIdentifier: eachCase.cellIdentifier)
}
tableView.backgroundColor = .clear
tableView.delegate = self
tableView.dataSource = self
addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
[
tableView.topAnchor.constraint(equalTo: topAnchor),
tableView.leadingAnchor.constraint(equalTo: leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: bottomAnchor),
].forEach { $0.isActive = true }
let foot = UIView()
foot.translatesAutoresizingMaskIntoConstraints = false
foot.heightAnchor.constraint(equalToConstant: 200).isActive = true
foot.widthAnchor.constraint(equalToConstant: 200).isActive = true
tableView.tableFooterView = foot
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
}
@@ -0,0 +1,114 @@
//
// AttachmentBannerView.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
private let attachmentSize: CGFloat = 100
private let attachmentSpacing: CGFloat = 16
class AttachmentBannerView: UIScrollView {
var attachments: [UIImage] = [] {
didSet {
rebuildViews()
}
}
override var intrinsicContentSize: CGSize {
if attachments.isEmpty { return .zero }
return .init(
width: (attachmentSize + attachmentSize) * CGFloat(attachments.count)
- attachmentSpacing,
height: attachmentSize
)
}
init() {
super.init(frame: .zero)
translatesAutoresizingMaskIntoConstraints = false
showsHorizontalScrollIndicator = false
showsVerticalScrollIndicator = false
rebuildViews()
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
func rebuildViews() {
subviews.forEach { $0.removeFromSuperview() }
for (index, attachment) in attachments.enumerated() {
let view = AttachmentPreviewView()
view.imageView.image = attachment
addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
view.frame = .init(
origin: .init(
x: (attachmentSize + attachmentSpacing) * CGFloat(index),
y: 0
),
size: .init(width: attachmentSize, height: attachmentSize)
)
}
invalidateIntrinsicContentSize()
contentSize = intrinsicContentSize
}
}
extension AttachmentBannerView {
class AttachmentPreviewView: UIView {
let imageView = UIImageView()
let deleteButton = UIButton()
override var intrinsicContentSize: CGSize {
.init(width: attachmentSize, height: attachmentSize)
}
init() {
super.init(frame: .zero)
addSubview(imageView)
addSubview(deleteButton)
layer.cornerRadius = 8
clipsToBounds = true
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
[
imageView.topAnchor.constraint(equalTo: topAnchor),
imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
].forEach { $0.isActive = true }
deleteButton.setImage(.init(named: "close", in: .module, with: nil), for: .normal)
deleteButton.imageView?.contentMode = .scaleAspectFit
deleteButton.tintColor = .white
deleteButton.translatesAutoresizingMaskIntoConstraints = false
[
deleteButton.topAnchor.constraint(equalTo: topAnchor, constant: 4),
deleteButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4),
deleteButton.widthAnchor.constraint(equalToConstant: 32),
deleteButton.heightAnchor.constraint(equalToConstant: 32),
].forEach { $0.isActive = true }
[
widthAnchor.constraint(equalToConstant: attachmentSize),
heightAnchor.constraint(equalToConstant: attachmentSize),
].forEach { $0.isActive = true }
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
}
}
@@ -0,0 +1,38 @@
//
// File.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
import Combine
extension InputEditView {
class ViewModel: ObservableObject {
var cancellables: Set<AnyCancellable> = []
@Published var text: String = ""
@Published var attachments: [UIImage] = []
init() {
}
deinit {
cancellables.forEach { $0.cancel() }
cancellables.removeAll()
}
}
}
extension InputEditView.ViewModel: Hashable, Equatable {
func hash(into hasher: inout Hasher) {
hasher.combine(text)
hasher.combine(attachments)
}
static func == (lhs: InputEditView.ViewModel, rhs: InputEditView.ViewModel) -> Bool {
lhs.hashValue == rhs.hashValue
}
}
@@ -0,0 +1,115 @@
//
// InputEditView.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
import Combine
class InputEditView: UIView, UITextViewDelegate {
let mainStack = UIStackView()
let attachmentsEditor = AttachmentBannerView()
let textEditor = PlainTextEditView()
let placeholderLabel = UILabel()
let controlBanner = TextEditControlBanner()
let viewModel = ViewModel()
var placeholderText: String = "" {
didSet {
placeholderLabel.text = placeholderText
}
}
init() {
super.init(frame: .zero)
addSubview(mainStack)
mainStack.translatesAutoresizingMaskIntoConstraints = false
mainStack.axis = .vertical
mainStack.spacing = 16
mainStack.alignment = .fill
mainStack.distribution = .equalSpacing
[
mainStack.topAnchor.constraint(equalTo: topAnchor),
mainStack.leadingAnchor.constraint(equalTo: leadingAnchor),
mainStack.trailingAnchor.constraint(equalTo: trailingAnchor),
mainStack.bottomAnchor.constraint(equalTo: bottomAnchor),
].forEach { $0.isActive = true }
textEditor.delegate = self
textEditor.heightAnchor.constraint(greaterThanOrEqualToConstant: 64).isActive = true
[
attachmentsEditor, textEditor, controlBanner,
].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
mainStack.addArrangedSubview($0)
[
$0.leadingAnchor.constraint(equalTo: mainStack.leadingAnchor),
$0.trailingAnchor.constraint(equalTo: mainStack.trailingAnchor),
].forEach { $0.isActive = true }
}
textEditor.addSubview(placeholderLabel)
placeholderLabel.textColor = .label.withAlphaComponent(0.25)
placeholderLabel.font = textEditor.font
placeholderLabel.translatesAutoresizingMaskIntoConstraints = false
[
placeholderLabel.leadingAnchor.constraint(equalTo: textEditor.leadingAnchor, constant: 2),
placeholderLabel.trailingAnchor.constraint(equalTo: textEditor.trailingAnchor, constant: -2),
placeholderLabel.topAnchor.constraint(equalTo: textEditor.topAnchor, constant: 0),
].forEach { $0.isActive = true }
viewModel.objectWillChange
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.updateValues()
}
.store(in: &viewModel.cancellables)
updateValues()
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
func textViewDidChange(_ textView: UITextView) {
viewModel.text = textView.text
}
func textViewDidBeginEditing(_ textView: UITextView) {
updatePlaceholderVisibility()
}
func textViewDidEndEditing(_ textView: UITextView) {
updatePlaceholderVisibility()
}
func updatePlaceholderVisibility() {
let visible = viewModel.text.isEmpty && !textEditor.isFirstResponder
UIView.animate(withDuration: 0.25) {
self.placeholderLabel.alpha = visible ? 1 : 0
}
}
func updateValues() {
UIView.animate(
withDuration: 0.5,
delay: 0,
usingSpringWithDamping: 1.0,
initialSpringVelocity: 0.8
) { [self] in
if textEditor.text != viewModel.text {
textEditor.text = viewModel.text
}
if attachmentsEditor.attachments != viewModel.attachments {
attachmentsEditor.attachments = viewModel.attachments
}
self.parentViewController?.view.layoutIfNeeded()
}
}
}
@@ -0,0 +1,37 @@
//
// TextEditView.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
class PlainTextEditView: UITextView, UITextViewDelegate {
init() {
super.init(frame: .zero, textContainer: nil)
delegate = self
tintColor = Constant.affineTintColor
linkTextAttributes = [:]
showsVerticalScrollIndicator = false
showsHorizontalScrollIndicator = false
textContainer.lineFragmentPadding = .zero
textAlignment = .natural
backgroundColor = .clear
textContainerInset = .zero
textContainer.lineBreakMode = .byTruncatingTail
isScrollEnabled = false
clipsToBounds = false
isEditable = true
isSelectable = true
isScrollEnabled = false
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
}
@@ -0,0 +1,62 @@
//
// TextEditControlBanner.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
class TextEditControlBanner: UIStackView {
static let height: CGFloat = 32
let cameraButton = UIButton()
let photoButton = UIButton()
let spacer = UIView()
let sendButton = UIButton()
init() {
super.init(frame: .zero)
axis = .horizontal
spacing = 16
alignment = .center
distribution = .fill
[
heightAnchor.constraint(equalToConstant: Self.height),
].forEach { $0.isActive = true }
[
cameraButton, photoButton,
sendButton,
].forEach {
$0.widthAnchor.constraint(equalToConstant: Self.height).isActive = true
$0.heightAnchor.constraint(equalToConstant: Self.height).isActive = true
}
[
cameraButton, photoButton,
spacer,
sendButton,
].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
addArrangedSubview($0)
}
cameraButton.setImage(.init(systemName: "camera"), for: .normal)
cameraButton.tintColor = .label
photoButton.setImage(.init(systemName: "photo"), for: .normal)
photoButton.tintColor = .label
sendButton.setImage(.init(systemName: "paperplane.fill"), for: .normal)
sendButton.tintColor = .label
}
@available(*, unavailable)
required init(coder _: NSCoder) {
fatalError()
}
}
@@ -0,0 +1,108 @@
//
// IntelligentsChatController+Header.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
extension IntelligentsChatController {
class Header: UIView {
static let height: CGFloat = 44
let contentView = UIView()
let titleLabel = UILabel()
let dropMenu = UIButton()
let backButton = UIButton()
let rightBarItemsStack = UIStackView()
let moreMenu = UIButton()
init() {
super.init(frame: .zero)
setupLayout()
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
@objc func navigateActionBack() {
parentViewController?.dismissInContext()
}
}
}
private extension IntelligentsChatController.Header {
func setupLayout() {
contentView.translatesAutoresizingMaskIntoConstraints = false
addSubview(contentView)
[
contentView.leadingAnchor.constraint(equalTo: leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: bottomAnchor),
contentView.heightAnchor.constraint(equalToConstant: Self.height),
].forEach { $0.isActive = true }
titleLabel.textColor = .label
titleLabel.font = .systemFont(
ofSize: UIFont.labelFontSize,
weight: .semibold
)
backButton.setImage(
UIImage(systemName: "chevron.left"),
for: .normal
)
backButton.tintColor = Constant.affineTintColor
backButton.addTarget(self, action: #selector(navigateActionBack), for: .touchUpInside)
dropMenu.setImage(
.init(systemName: "chevron.down")?.withRenderingMode(.alwaysTemplate),
for: .normal
)
dropMenu.tintColor = .gray.withAlphaComponent(0.5)
contentView.addSubview(titleLabel)
contentView.addSubview(backButton)
contentView.addSubview(dropMenu)
contentView.addSubview(rightBarItemsStack)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
backButton.translatesAutoresizingMaskIntoConstraints = false
dropMenu.translatesAutoresizingMaskIntoConstraints = false
rightBarItemsStack.translatesAutoresizingMaskIntoConstraints = false
rightBarItemsStack.axis = .horizontal
rightBarItemsStack.spacing = 10
rightBarItemsStack.alignment = .center
rightBarItemsStack.distribution = .equalSpacing
[
backButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
backButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
backButton.widthAnchor.constraint(equalToConstant: 44),
backButton.heightAnchor.constraint(equalToConstant: 44),
rightBarItemsStack.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
rightBarItemsStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10),
rightBarItemsStack.heightAnchor.constraint(equalToConstant: 44),
titleLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
titleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: backButton.trailingAnchor, constant: 10),
dropMenu.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
dropMenu.widthAnchor.constraint(equalToConstant: 44),
dropMenu.heightAnchor.constraint(equalToConstant: 44),
titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: dropMenu.leadingAnchor, constant: -10),
].forEach { $0.isActive = true }
rightBarItemsStack.addArrangedSubview(moreMenu)
moreMenu.setImage(
.init(systemName: "ellipsis.circle"),
for: .normal
)
moreMenu.tintColor = Constant.affineTintColor
}
}
@@ -0,0 +1,62 @@
//
// IntelligentsChatController+InputBox.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import UIKit
extension IntelligentsChatController {
class InputBox: UIView {
let backgroundView = UIView()
let editor = InputEditView()
init() {
super.init(frame: .zero)
setupLayout()
editor.textEditor.font = UIFont.systemFont(ofSize: UIFont.labelFontSize)
editor.placeholderText = "Summarize this article for me...".localized()
backgroundView.backgroundColor = .systemBackground
backgroundView.layer.cornerRadius = 16
backgroundView.layer.shadowColor = UIColor.black.withAlphaComponent(0.25).cgColor
backgroundView.layer.shadowOffset = .init(width: 0, height: 0)
backgroundView.layer.shadowRadius = 8
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
}
}
private extension IntelligentsChatController.InputBox {
func setupLayout() {
addSubview(backgroundView)
backgroundView.translatesAutoresizingMaskIntoConstraints = false
addSubview(editor)
editor.translatesAutoresizingMaskIntoConstraints = false
let inset: CGFloat = 16
[
editor.leadingAnchor.constraint(equalTo: leadingAnchor, constant: inset),
editor.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -inset),
editor.topAnchor.constraint(equalTo: topAnchor, constant: inset),
editor.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -inset),
].forEach { $0.isActive = true }
[
backgroundView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0),
backgroundView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0),
backgroundView.topAnchor.constraint(equalTo: topAnchor, constant: 0),
backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 128),
].forEach { $0.isActive = true }
}
}
@@ -0,0 +1,71 @@
//
// IntelligentsChatController.swift
//
//
// Created by on 2024/11/18.
//
import UIKit
class IntelligentsChatController: UIViewController {
let header = Header()
let inputBox = InputBox()
let tableView = ChatTableView()
override var title: String? {
set {
super.title = newValue
header.titleLabel.text = newValue
}
get {
super.title
}
}
init() {
super.init(nibName: nil, bundle: nil)
title = "Chat with AI".localized()
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
override func viewDidLoad() {
super.viewDidLoad()
assert(navigationController != nil)
view.backgroundColor = .secondarySystemBackground
hideKeyboardWhenTappedAround()
setupLayout()
}
func setupLayout() {
view.addSubview(header)
header.translatesAutoresizingMaskIntoConstraints = false
[
header.topAnchor.constraint(equalTo: view.topAnchor),
header.leadingAnchor.constraint(equalTo: view.leadingAnchor),
header.trailingAnchor.constraint(equalTo: view.trailingAnchor),
header.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 44),
].forEach { $0.isActive = true }
view.addSubview(inputBox)
inputBox.translatesAutoresizingMaskIntoConstraints = false
[
inputBox.leadingAnchor.constraint(equalTo: view.leadingAnchor),
inputBox.trailingAnchor.constraint(equalTo: view.trailingAnchor),
inputBox.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
].forEach { $0.isActive = true }
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
[
tableView.topAnchor.constraint(equalTo: header.bottomAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: inputBox.topAnchor),
].forEach { $0.isActive = true }
}
}
@@ -0,0 +1,27 @@
//
// Chat.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import Foundation
struct Chat: Codable {
enum ParticipantType: String, Codable, Equatable {
case user
case bot
}
var participant: ParticipantType
typealias MarkdownDocument = String
var content: MarkdownDocument
var date: Date
init(participant: ParticipantType, content: MarkdownDocument, date: Date = .init()) {
self.participant = participant
self.content = content
self.date = date
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "close.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,5 @@
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.922363" y="0.5" width="17" height="17" rx="8.5" fill="#2A2A2A"/>
<rect x="0.922363" y="0.5" width="17" height="17" rx="8.5" stroke="#E6E6E6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.1572 5.73483C6.30364 5.58839 6.54108 5.58839 6.68753 5.73483L9.42236 8.46967L12.1572 5.73484C12.3036 5.58839 12.5411 5.58839 12.6875 5.73484C12.834 5.88128 12.834 6.11872 12.6875 6.26517L9.95269 9L12.6875 11.7348C12.834 11.8813 12.834 12.1187 12.6875 12.2652C12.5411 12.4116 12.3036 12.4116 12.1572 12.2652L9.42236 9.53033L6.68753 12.2652C6.54108 12.4116 6.30364 12.4116 6.1572 12.2652C6.01075 12.1187 6.01075 11.8813 6.1572 11.7348L8.89203 9L6.1572 6.26516C6.01075 6.11872 6.01075 5.88128 6.1572 5.73483Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 831 B

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "spark.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
@@ -0,0 +1,8 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M12.1738 5.49104C12.1329 5.13024 11.8278 4.85751 11.4647 4.85714C11.1016 4.85677 10.796 5.12887 10.7544 5.48959C10.4839 7.83515 9.78261 9.48448 8.65126 10.6158C7.51992 11.7472 5.87058 12.4485 3.52502 12.719C3.16431 12.7606 2.89221 13.0662 2.89258 13.4293C2.89295 13.7924 3.16568 14.0975 3.52647 14.1383C5.83327 14.3996 7.51766 15.1006 8.67586 16.2379C9.82971 17.3709 10.5456 19.0198 10.7525 21.3489C10.7853 21.7178 11.0945 22.0004 11.4648 22C11.8351 21.9996 12.1437 21.7162 12.1756 21.3473C12.3739 19.0565 13.0892 17.3729 14.2487 16.2133C15.4083 15.0537 17.092 14.3385 19.3827 14.1402C19.7517 14.1083 20.035 13.7997 20.0354 13.4294C20.0359 13.0591 19.7532 12.7499 19.3843 12.7171C17.0553 12.5102 15.4063 11.7943 14.2733 10.6404C13.136 9.48222 12.435 7.79783 12.1738 5.49104Z"
fill="#1E96EB" />
<path
d="M19.8353 2.24651C19.8194 2.1062 19.7007 2.00014 19.5595 2C19.4183 1.99986 19.2995 2.10567 19.2833 2.24595C19.1781 3.15811 18.9054 3.79952 18.4654 4.23949C18.0254 4.67946 17.384 4.95218 16.4719 5.05739C16.3316 5.07356 16.2258 5.19241 16.2259 5.33362C16.2261 5.47482 16.3321 5.59345 16.4724 5.60935C17.3695 5.71096 18.0246 5.98357 18.475 6.42584C18.9237 6.86644 19.2021 7.50771 19.2826 8.41347C19.2953 8.55691 19.4156 8.66683 19.5596 8.66667C19.7036 8.6665 19.8236 8.55632 19.836 8.41284C19.9131 7.52199 20.1913 6.86723 20.6422 6.41629C21.0931 5.96534 21.7479 5.68719 22.6388 5.61008C22.7822 5.59766 22.8924 5.47765 22.8926 5.33365C22.8927 5.18964 22.7828 5.06939 22.6394 5.05664C21.7336 4.97619 21.0924 4.69777 20.6517 4.24905C20.2095 3.79864 19.9369 3.1436 19.8353 2.24651Z"
fill="#1E96EB" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,13 @@
/*
Localizable.strings
Intelligents
Created by 秋星桥 on 2024/11/18.
*/
/* No comment provided by engineer. */
"Chat with AI" = "Chat with AI";
/* No comment provided by engineer. */
"Summarize this article for me..." = "Summarize this article for me...";
@@ -0,0 +1,13 @@
/*
Localizable.strings
Intelligents
Created by 秋星桥 on 2024/11/18.
*/
/* No comment provided by engineer. */
"Chat with AI" = "与 AI 聊天";
/* No comment provided by engineer. */
"Summarize this article for me..." = "请为我总结这份文档...";
@@ -0,0 +1,38 @@
//
// RootViewController.swift
// App
//
// Created by on 2024/11/18.
//
import UIKit
@objc
class RootViewController: UINavigationController {
override init(rootViewController: UIViewController) {
fatalError() // "you are not allowed to call this"
}
override init(navigationBarClass: AnyClass?, toolbarClass: AnyClass?) {
fatalError() // "you are not allowed to call this"
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commitInit()
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
fatalError() // "you are not allowed to call this"
}
func commitInit() {
assert(viewControllers.isEmpty)
viewControllers = [AFFiNEViewController()]
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
}
}
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
@@ -0,0 +1,14 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "Intelligents",
products: [
.library(name: "Intelligents", targets: ["Intelligents"]),
],
targets: [
.target(name: "Intelligents"),
]
)
@@ -0,0 +1,4 @@
// The Swift Programming Language
// https://docs.swift.org/swift-book
class Intelligents {}
@@ -0,0 +1,13 @@
//
// IntelligentsButton.swift
//
//
// Created by on 2024/11/18.
//
import UIKit
// floating button to open intelligent panel
class IntelligentsButton: UIView {
let image = UIImageView()
}
@@ -0,0 +1,14 @@
//
// IntelligentsChatController.swift
//
//
// Created by on 2024/11/18.
//
import UIKit
class IntelligentsChatController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}