mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 05:14:54 +00:00
Draw the Cells
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "networkimage",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/gonzalezreal/NetworkImage",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "2849f5323265386e200484b0d0f896e73c3411b9",
|
||||||
|
"version" : "6.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-cmark",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/swiftlang/swift-cmark",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "3ccff77b2dc5b96b77db3da0d68d28068593fa53",
|
||||||
|
"version" : "0.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-markdown-ui",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/gonzalezreal/swift-markdown-ui",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "5f613358148239d0292c0cef674a3c2314737f9e",
|
||||||
|
"version" : "2.4.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 2
|
||||||
|
}
|
||||||
@@ -7,13 +7,18 @@ let package = Package(
|
|||||||
name: "Intelligents",
|
name: "Intelligents",
|
||||||
defaultLocalization: "en",
|
defaultLocalization: "en",
|
||||||
platforms: [
|
platforms: [
|
||||||
.iOS(.v14),
|
.iOS(.v15),
|
||||||
.macCatalyst(.v14),
|
.macCatalyst(.v15),
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
.library(name: "Intelligents", targets: ["Intelligents"]),
|
.library(name: "Intelligents", targets: ["Intelligents"]),
|
||||||
],
|
],
|
||||||
|
dependencies: [
|
||||||
|
.package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.4.1"),
|
||||||
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(name: "Intelligents"),
|
.target(name: "Intelligents", dependencies: [
|
||||||
|
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
|
||||||
|
]),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,19 +7,56 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
private let initialInsetValue: CGFloat = 24
|
||||||
|
|
||||||
extension ChatTableView {
|
extension ChatTableView {
|
||||||
class BaseCell: UITableViewCell {
|
class BaseCell: UITableViewCell {
|
||||||
var inset: UIEdgeInsets = .zero {
|
var inset: UIEdgeInsets { // available for overrides
|
||||||
didSet { setNeedsLayout() }
|
.init(
|
||||||
|
top: initialInsetValue / 2,
|
||||||
|
left: initialInsetValue,
|
||||||
|
bottom: initialInsetValue / 2,
|
||||||
|
right: initialInsetValue
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let containerView = UIView()
|
let containerView = UIView()
|
||||||
|
let roundedBackgroundView = UIView()
|
||||||
|
|
||||||
|
var viewModel: AnyObject? {
|
||||||
|
didSet { update(via: viewModel) }
|
||||||
|
}
|
||||||
|
|
||||||
|
var isBackgroundColorActivated = false {
|
||||||
|
didSet {
|
||||||
|
roundedBackgroundView.backgroundColor = isBackgroundColorActivated
|
||||||
|
? .systemGray.withAlphaComponent(0.25)
|
||||||
|
: .clear
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
selectionStyle = .none
|
selectionStyle = .none
|
||||||
backgroundColor = .clear
|
backgroundColor = .clear
|
||||||
|
|
||||||
|
contentView.addSubview(roundedBackgroundView)
|
||||||
|
roundedBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
[ // inset half of the container view
|
||||||
|
roundedBackgroundView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: inset.left / 2),
|
||||||
|
roundedBackgroundView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -inset.right / 2),
|
||||||
|
roundedBackgroundView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: inset.top / 2),
|
||||||
|
roundedBackgroundView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -inset.bottom / 2),
|
||||||
|
].forEach { $0.isActive = true }
|
||||||
|
|
||||||
contentView.addSubview(containerView)
|
contentView.addSubview(containerView)
|
||||||
|
containerView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
[
|
||||||
|
containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: inset.left),
|
||||||
|
containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -inset.right),
|
||||||
|
containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: inset.top),
|
||||||
|
containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -inset.bottom),
|
||||||
|
].forEach { $0.isActive = true }
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(*, unavailable)
|
@available(*, unavailable)
|
||||||
@@ -27,13 +64,13 @@ extension ChatTableView {
|
|||||||
fatalError()
|
fatalError()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func layoutSubviews() {
|
override func prepareForReuse() {
|
||||||
super.layoutSubviews()
|
super.prepareForReuse()
|
||||||
containerView.frame = contentView.bounds.inset(by: inset)
|
viewModel = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(via _: AnyObject?) {
|
func update(via object: AnyObject?) {
|
||||||
assertionFailure() // "should be override"
|
_ = object
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
//
|
||||||
|
// ChatTableView+ChatCell.swift
|
||||||
|
// Intelligents
|
||||||
|
//
|
||||||
|
// Created by 秋星桥 on 2024/11/18.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MarkdownUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension ChatTableView {
|
||||||
|
class ChatCell: BaseCell {
|
||||||
|
let avatarView = CircleImageView()
|
||||||
|
let titleLabel = UILabel()
|
||||||
|
let markdownContainer = UIView()
|
||||||
|
|
||||||
|
var markdownView: UIView?
|
||||||
|
var removableConstraints: [NSLayoutConstraint] = []
|
||||||
|
|
||||||
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
|
||||||
|
let spacingElement: CGFloat = 12
|
||||||
|
let avatarSize: CGFloat = 24
|
||||||
|
|
||||||
|
containerView.addSubview(avatarView)
|
||||||
|
avatarView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
[
|
||||||
|
avatarView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
||||||
|
avatarView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||||
|
avatarView.widthAnchor.constraint(equalToConstant: avatarSize),
|
||||||
|
avatarView.heightAnchor.constraint(equalToConstant: avatarSize),
|
||||||
|
].forEach { $0.isActive = true }
|
||||||
|
|
||||||
|
titleLabel.font = .systemFont(ofSize: UIFont.labelFontSize, weight: .bold)
|
||||||
|
containerView.addSubview(titleLabel)
|
||||||
|
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
[
|
||||||
|
titleLabel.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: spacingElement),
|
||||||
|
titleLabel.centerYAnchor.constraint(equalTo: avatarView.centerYAnchor),
|
||||||
|
titleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
||||||
|
].forEach { $0.isActive = true }
|
||||||
|
|
||||||
|
containerView.addSubview(markdownContainer)
|
||||||
|
markdownContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
[
|
||||||
|
markdownContainer.topAnchor.constraint(greaterThanOrEqualTo: avatarView.bottomAnchor, constant: spacingElement),
|
||||||
|
markdownContainer.topAnchor.constraint(greaterThanOrEqualTo: titleLabel.bottomAnchor, constant: spacingElement),
|
||||||
|
markdownContainer.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 0),
|
||||||
|
markdownContainer.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 0),
|
||||||
|
markdownContainer.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0),
|
||||||
|
].forEach { $0.isActive = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder _: NSCoder) {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func update(via object: AnyObject?) {
|
||||||
|
super.update(via: object)
|
||||||
|
|
||||||
|
guard let viewModel = object as? ViewModel else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch viewModel.participant {
|
||||||
|
case .system:
|
||||||
|
avatarView.image = UIImage(systemName: "gearshape.fill")
|
||||||
|
titleLabel.text = "System".localized()
|
||||||
|
case .assistant:
|
||||||
|
avatarView.image = UIImage(named: "spark", in: .module, with: .none)
|
||||||
|
titleLabel.text = "AFFiNE AI".localized()
|
||||||
|
case .user:
|
||||||
|
avatarView.image = UIImage(systemName: "person.fill")
|
||||||
|
titleLabel.text = "You".localized()
|
||||||
|
}
|
||||||
|
|
||||||
|
removableConstraints.forEach { $0.isActive = false }
|
||||||
|
if let markdownView { markdownView.removeFromSuperview() }
|
||||||
|
markdownContainer.subviews.forEach { $0.removeFromSuperview() }
|
||||||
|
|
||||||
|
let hostingView: UIView = UIHostingView(
|
||||||
|
rootView: Markdown(.init(viewModel.markdownDocument))
|
||||||
|
)
|
||||||
|
defer { markdownView = hostingView }
|
||||||
|
markdownContainer.addSubview(hostingView)
|
||||||
|
|
||||||
|
hostingView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
[
|
||||||
|
hostingView.topAnchor.constraint(equalTo: markdownContainer.topAnchor),
|
||||||
|
hostingView.leadingAnchor.constraint(equalTo: markdownContainer.leadingAnchor),
|
||||||
|
hostingView.trailingAnchor.constraint(lessThanOrEqualTo: markdownContainer.trailingAnchor),
|
||||||
|
hostingView.bottomAnchor.constraint(equalTo: markdownContainer.bottomAnchor),
|
||||||
|
].forEach {
|
||||||
|
$0.isActive = true
|
||||||
|
removableConstraints.append($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ChatTableView.ChatCell {
|
||||||
|
class ViewModel {
|
||||||
|
let participant: Participant
|
||||||
|
let markdownDocument: String
|
||||||
|
|
||||||
|
init(participant: Participant, markdownDocument: String) {
|
||||||
|
self.participant = participant
|
||||||
|
self.markdownDocument = markdownDocument
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ChatTableView.ChatCell.ViewModel {
|
||||||
|
enum Participant {
|
||||||
|
case user
|
||||||
|
case assistant
|
||||||
|
case system
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ extension ChatTableView {
|
|||||||
struct DataElement {
|
struct DataElement {
|
||||||
enum CellType: String, CaseIterable {
|
enum CellType: String, CaseIterable {
|
||||||
case base
|
case base
|
||||||
|
case chat
|
||||||
}
|
}
|
||||||
|
|
||||||
let type: CellType
|
let type: CellType
|
||||||
@@ -28,6 +29,8 @@ extension ChatTableView.DataElement.CellType {
|
|||||||
switch self {
|
switch self {
|
||||||
case .base:
|
case .base:
|
||||||
ChatTableView.BaseCell.self
|
ChatTableView.BaseCell.self
|
||||||
|
case .chat:
|
||||||
|
ChatTableView.ChatCell.self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,51 @@ class ChatTableView: UIView {
|
|||||||
foot.heightAnchor.constraint(equalToConstant: 200).isActive = true
|
foot.heightAnchor.constraint(equalToConstant: 200).isActive = true
|
||||||
foot.widthAnchor.constraint(equalToConstant: 200).isActive = true
|
foot.widthAnchor.constraint(equalToConstant: 200).isActive = true
|
||||||
tableView.tableFooterView = foot
|
tableView.tableFooterView = foot
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.dataSource = [
|
||||||
|
.init(type: .chat, object: ChatCell.ViewModel(
|
||||||
|
participant: .system,
|
||||||
|
markdownDocument: "Welcome to Intelligents"
|
||||||
|
)),
|
||||||
|
.init(type: .chat, object: ChatCell.ViewModel(
|
||||||
|
participant: .user,
|
||||||
|
markdownDocument: "Please summarize this article for me"
|
||||||
|
)),
|
||||||
|
.init(type: .chat, object: ChatCell.ViewModel(
|
||||||
|
participant: .assistant,
|
||||||
|
markdownDocument: ###"""
|
||||||
|
**Activation Code Usage Limits**
|
||||||
|
|
||||||
|
A single activation code can be used on multiple devices.
|
||||||
|
|
||||||
|
**Note:** A single activation code is intended for use on a reasonable number of devices by one user.
|
||||||
|
|
||||||
|
Excessive activation requests may result in the activation code being banned. Any bans are subject to manual review and are operated by staff.
|
||||||
|
|
||||||
|
`The limit is up to 5 devices per year or 10 activation requests within the same period.`
|
||||||
|
"""###
|
||||||
|
)),
|
||||||
|
.init(type: .chat, object: ChatCell.ViewModel(
|
||||||
|
participant: .user,
|
||||||
|
markdownDocument: ###"""
|
||||||
|
**Download Axchange from the App Store**
|
||||||
|
|
||||||
|
You can download Axchange from the App Store:
|
||||||
|
|
||||||
|
- [https://apps.apple.com/app/axchange-adb-file-transfer/id6737504944](https://apps.apple.com/app/axchange-adb-file-transfer/id6737504944)
|
||||||
|
|
||||||
|
The version downloaded this way does not require activation to use.
|
||||||
|
"""###
|
||||||
|
)),
|
||||||
|
.init(type: .chat, object: ChatCell.ViewModel(
|
||||||
|
participant: .assistant,
|
||||||
|
markdownDocument: "GOOD"
|
||||||
|
)),
|
||||||
|
]
|
||||||
|
|
||||||
|
self.tableView.reloadData()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(*, unavailable)
|
@available(*, unavailable)
|
||||||
|
|||||||
@@ -20,3 +20,12 @@
|
|||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Summarize this article for me..." = "Summarize this article for me...";
|
"Summarize this article for me..." = "Summarize this article for me...";
|
||||||
|
|
||||||
|
/* No comment provided by engineer. */
|
||||||
|
"System" = "System";
|
||||||
|
|
||||||
|
/* No comment provided by engineer. */
|
||||||
|
"AFFiNE AI" = "AFFiNE AI";
|
||||||
|
|
||||||
|
/* No comment provided by engineer. */
|
||||||
|
"You" = "You";
|
||||||
|
|||||||
@@ -20,3 +20,12 @@
|
|||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Summarize this article for me..." = "请为我总结这份文档...";
|
"Summarize this article for me..." = "请为我总结这份文档...";
|
||||||
|
|
||||||
|
/* No comment provided by engineer. */
|
||||||
|
"System" = "系统";
|
||||||
|
|
||||||
|
/* No comment provided by engineer. */
|
||||||
|
"AFFiNE AI" = "AFFiNE AI";
|
||||||
|
|
||||||
|
/* No comment provided by engineer. */
|
||||||
|
"You" = "你";
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
//
|
||||||
|
// CircleImageView.swift
|
||||||
|
// Intelligents
|
||||||
|
//
|
||||||
|
// Created by 秋星桥 on 2024/12/13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class CircleImageView: UIImageView {
|
||||||
|
init() {
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
contentMode = .scaleAspectFill
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder _: NSCoder) {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
clipsToBounds = true
|
||||||
|
layer.cornerRadius = (bounds.width + bounds.height) / 2 / 2
|
||||||
|
layer.masksToBounds = true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
//
|
||||||
|
// UIHostingView.swift
|
||||||
|
// Intelligents
|
||||||
|
//
|
||||||
|
// Created by 秋星桥 on 2024/12/13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class UIHostingView<Content: View>: UIView {
|
||||||
|
private let hostingViewController: UIHostingController<Content>
|
||||||
|
|
||||||
|
var rootView: Content {
|
||||||
|
get { hostingViewController.rootView }
|
||||||
|
set { hostingViewController.rootView = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
init(rootView: Content) {
|
||||||
|
hostingViewController = UIHostingController(rootView: rootView)
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
hostingViewController.view?.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(hostingViewController.view)
|
||||||
|
if let view = hostingViewController.view {
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
view.isOpaque = false
|
||||||
|
addSubview(view)
|
||||||
|
let constraints = [
|
||||||
|
view.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
view.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
view.leftAnchor.constraint(equalTo: leftAnchor),
|
||||||
|
view.rightAnchor.constraint(equalTo: rightAnchor),
|
||||||
|
]
|
||||||
|
NSLayoutConstraint.activate(constraints)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder _: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||||
|
hostingViewController.sizeThatFits(in: size)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user