mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +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",
|
||||
defaultLocalization: "en",
|
||||
platforms: [
|
||||
.iOS(.v14),
|
||||
.macCatalyst(.v14),
|
||||
.iOS(.v15),
|
||||
.macCatalyst(.v15),
|
||||
],
|
||||
products: [
|
||||
.library(name: "Intelligents", targets: ["Intelligents"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.4.1"),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "Intelligents"),
|
||||
.target(name: "Intelligents", dependencies: [
|
||||
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
|
||||
]),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -7,19 +7,56 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
private let initialInsetValue: CGFloat = 24
|
||||
|
||||
extension ChatTableView {
|
||||
class BaseCell: UITableViewCell {
|
||||
var inset: UIEdgeInsets = .zero {
|
||||
didSet { setNeedsLayout() }
|
||||
var inset: UIEdgeInsets { // available for overrides
|
||||
.init(
|
||||
top: initialInsetValue / 2,
|
||||
left: initialInsetValue,
|
||||
bottom: initialInsetValue / 2,
|
||||
right: initialInsetValue
|
||||
)
|
||||
}
|
||||
|
||||
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?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
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)
|
||||
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)
|
||||
@@ -27,13 +64,13 @@ extension ChatTableView {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
containerView.frame = contentView.bounds.inset(by: inset)
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
viewModel = nil
|
||||
}
|
||||
|
||||
func update(via _: AnyObject?) {
|
||||
assertionFailure() // "should be override"
|
||||
func update(via object: AnyObject?) {
|
||||
_ = 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 {
|
||||
enum CellType: String, CaseIterable {
|
||||
case base
|
||||
case chat
|
||||
}
|
||||
|
||||
let type: CellType
|
||||
@@ -28,6 +29,8 @@ extension ChatTableView.DataElement.CellType {
|
||||
switch self {
|
||||
case .base:
|
||||
ChatTableView.BaseCell.self
|
||||
case .chat:
|
||||
ChatTableView.ChatCell.self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,51 @@ class ChatTableView: UIView {
|
||||
foot.heightAnchor.constraint(equalToConstant: 200).isActive = true
|
||||
foot.widthAnchor.constraint(equalToConstant: 200).isActive = true
|
||||
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)
|
||||
|
||||
@@ -20,3 +20,12 @@
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"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. */
|
||||
"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