654
ios/Sources/ContentScreens.swift
Normal file
654
ios/Sources/ContentScreens.swift
Normal file
@@ -0,0 +1,654 @@
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
final class IssuesViewController: RefreshingTableViewController {
|
||||
private let context: AppContext
|
||||
private let owner: String
|
||||
private let repository: String
|
||||
private var rows: [IssueRow] = []
|
||||
|
||||
init(context: AppContext, owner: String, repository: String) {
|
||||
self.context = context
|
||||
self.owner = owner
|
||||
self.repository = repository
|
||||
super.init()
|
||||
title = repository
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
tableView.register(IssueCell.self, forCellReuseIdentifier: "issue")
|
||||
updateFilterMenu()
|
||||
loadContent(refreshing: false)
|
||||
}
|
||||
|
||||
override func loadContent(refreshing: Bool) {
|
||||
beginLoading(refreshing: refreshing)
|
||||
loadingTask?.cancel()
|
||||
loadingTask = Task {
|
||||
do {
|
||||
rows = try await context.core.issues(owner: owner, repository: repository)
|
||||
tableView.reloadData()
|
||||
let status = context.core.settings().issueStatus
|
||||
tableView.backgroundView = rows.isEmpty
|
||||
? EmptyBackgroundView(
|
||||
title: "No \(status) issues",
|
||||
detail: "No issues match the selected status."
|
||||
)
|
||||
: nil
|
||||
} catch {
|
||||
if !Task.isCancelled { show(error: error) }
|
||||
}
|
||||
endLoading()
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
rows.count
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath
|
||||
) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "issue", for: indexPath) as! IssueCell
|
||||
cell.configure(rows[indexPath.row])
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
navigationController?.pushViewController(
|
||||
IssueViewController(
|
||||
context: context,
|
||||
owner: owner,
|
||||
repository: repository,
|
||||
number: rows[indexPath.row].number
|
||||
),
|
||||
animated: true
|
||||
)
|
||||
}
|
||||
|
||||
private func updateFilterMenu() {
|
||||
let current = context.core.settings().issueStatus
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
image: context.symbol("line.3.horizontal.decrease.circle"),
|
||||
menu: UIMenu(children: ["open", "closed"].map { status in
|
||||
UIAction(
|
||||
title: status.capitalized,
|
||||
state: current == status ? .on : .off
|
||||
) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
do {
|
||||
try self.context.core.setIssueStatus(status: status)
|
||||
self.updateFilterMenu()
|
||||
self.loadContent(refreshing: false)
|
||||
} catch {
|
||||
self.show(error: error)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
final class IssueCell: UITableViewCell {
|
||||
private let titleLabel = UILabel()
|
||||
private let summaryLabel = UILabel()
|
||||
private let labels = UIStackView()
|
||||
private let metaLabel = UILabel()
|
||||
private let milestoneLabel = UILabel()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
accessoryType = .disclosureIndicator
|
||||
titleLabel.font = .preferredFont(forTextStyle: .headline)
|
||||
titleLabel.numberOfLines = 2
|
||||
summaryLabel.font = .preferredFont(forTextStyle: .subheadline)
|
||||
summaryLabel.textColor = .secondaryLabel
|
||||
summaryLabel.numberOfLines = 2
|
||||
labels.axis = .horizontal
|
||||
labels.spacing = 5
|
||||
metaLabel.font = .preferredFont(forTextStyle: .caption1)
|
||||
metaLabel.textColor = .tertiaryLabel
|
||||
metaLabel.numberOfLines = 2
|
||||
milestoneLabel.font = .preferredFont(forTextStyle: .caption1)
|
||||
milestoneLabel.adjustsFontForContentSizeCategory = true
|
||||
let stack = UIStackView(
|
||||
arrangedSubviews: [titleLabel, summaryLabel, labels, metaLabel, milestoneLabel]
|
||||
)
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 6
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(stack)
|
||||
NSLayoutConstraint.activate([
|
||||
stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||
stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8),
|
||||
stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
|
||||
stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10),
|
||||
])
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||
|
||||
func configure(_ row: IssueRow) {
|
||||
titleLabel.text = row.title
|
||||
summaryLabel.text = row.summary
|
||||
metaLabel.text = row.meta
|
||||
milestoneLabel.isHidden = row.milestone.isEmpty
|
||||
milestoneLabel.attributedText = symbolText(
|
||||
"flag.fill",
|
||||
text: row.milestone,
|
||||
font: milestoneLabel.font,
|
||||
color: .tertiaryLabel
|
||||
)
|
||||
milestoneLabel.accessibilityLabel = row.milestone.isEmpty
|
||||
? nil
|
||||
: "Milestone \(row.milestone)"
|
||||
labels.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
labels.isHidden = row.labels.isEmpty
|
||||
for label in row.labels.prefix(3) {
|
||||
let view = UILabel()
|
||||
view.text = " \(label.name) "
|
||||
view.font = .preferredFont(forTextStyle: .caption2)
|
||||
view.textColor = label.light ? .black : .white
|
||||
view.backgroundColor = UIColor(hex: label.color)
|
||||
view.layer.cornerRadius = 9
|
||||
view.clipsToBounds = true
|
||||
labels.addArrangedSubview(view)
|
||||
}
|
||||
labels.addArrangedSubview(UIView())
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class MilestonesViewController: RefreshingTableViewController {
|
||||
private let context: AppContext
|
||||
private let owner: String
|
||||
private let repository: String
|
||||
private var rows: [MilestoneRow] = []
|
||||
|
||||
init(context: AppContext, owner: String, repository: String) {
|
||||
self.context = context
|
||||
self.owner = owner
|
||||
self.repository = repository
|
||||
super.init()
|
||||
title = repository
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
tableView.register(MilestoneCell.self, forCellReuseIdentifier: "milestone")
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.estimatedRowHeight = 118
|
||||
loadContent(refreshing: false)
|
||||
}
|
||||
|
||||
override func loadContent(refreshing: Bool) {
|
||||
beginLoading(refreshing: refreshing)
|
||||
loadingTask?.cancel()
|
||||
loadingTask = Task {
|
||||
do {
|
||||
rows = try await context.core.milestones(owner: owner, repository: repository)
|
||||
tableView.reloadData()
|
||||
tableView.backgroundView = rows.isEmpty
|
||||
? EmptyBackgroundView(
|
||||
title: "No milestones",
|
||||
detail: "This repository does not have any milestones."
|
||||
)
|
||||
: nil
|
||||
} catch {
|
||||
if !Task.isCancelled { show(error: error) }
|
||||
}
|
||||
endLoading()
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
rows.count
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath
|
||||
) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(
|
||||
withIdentifier: "milestone",
|
||||
for: indexPath
|
||||
) as! MilestoneCell
|
||||
cell.configure(rows[indexPath.row], disclosure: true)
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
navigationController?.pushViewController(
|
||||
MilestoneViewController(
|
||||
context: context,
|
||||
owner: owner,
|
||||
repository: repository,
|
||||
id: rows[indexPath.row].id
|
||||
),
|
||||
animated: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class MilestoneViewController: RefreshingTableViewController {
|
||||
private let context: AppContext
|
||||
private let owner: String
|
||||
private let repository: String
|
||||
private let id: Int64
|
||||
private var page: MilestonePage?
|
||||
|
||||
init(context: AppContext, owner: String, repository: String, id: Int64) {
|
||||
self.context = context
|
||||
self.owner = owner
|
||||
self.repository = repository
|
||||
self.id = id
|
||||
super.init()
|
||||
title = "Milestone"
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
tableView.register(MilestoneCell.self, forCellReuseIdentifier: "milestone")
|
||||
tableView.register(IssueCell.self, forCellReuseIdentifier: "issue")
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.estimatedRowHeight = 118
|
||||
loadContent(refreshing: false)
|
||||
}
|
||||
|
||||
override func loadContent(refreshing: Bool) {
|
||||
beginLoading(refreshing: refreshing)
|
||||
loadingTask?.cancel()
|
||||
loadingTask = Task {
|
||||
do {
|
||||
page = try await context.core.milestone(
|
||||
owner: owner,
|
||||
repository: repository,
|
||||
id: id
|
||||
)
|
||||
title = page?.milestone.title
|
||||
tableView.reloadData()
|
||||
} catch {
|
||||
if !Task.isCancelled { show(error: error) }
|
||||
}
|
||||
endLoading()
|
||||
}
|
||||
}
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int { page == nil ? 0 : 2 }
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
section == 0 ? 1 : page?.issues.count ?? 0
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
section == 1 ? "Issues" : nil
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
||||
section == 1 && page?.issues.isEmpty == true ? "No issues are assigned to this milestone." : nil
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath
|
||||
) -> UITableViewCell {
|
||||
guard let page else { return UITableViewCell() }
|
||||
if indexPath.section == 0 {
|
||||
let cell = tableView.dequeueReusableCell(
|
||||
withIdentifier: "milestone",
|
||||
for: indexPath
|
||||
) as! MilestoneCell
|
||||
cell.configure(page.milestone, disclosure: false)
|
||||
return cell
|
||||
}
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "issue", for: indexPath) as! IssueCell
|
||||
cell.configure(page.issues[indexPath.row])
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard indexPath.section == 1, let issue = page?.issues[indexPath.row] else { return }
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
navigationController?.pushViewController(
|
||||
IssueViewController(
|
||||
context: context,
|
||||
owner: owner,
|
||||
repository: repository,
|
||||
number: issue.number
|
||||
),
|
||||
animated: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
final class MilestoneCell: UITableViewCell {
|
||||
private let titleLabel = UILabel()
|
||||
private let descriptionLabel = UILabel()
|
||||
private let metaLabel = UILabel()
|
||||
private let progress = UIProgressView(progressViewStyle: .bar)
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
titleLabel.font = .preferredFont(forTextStyle: .headline)
|
||||
titleLabel.numberOfLines = 2
|
||||
descriptionLabel.font = .preferredFont(forTextStyle: .subheadline)
|
||||
descriptionLabel.textColor = .secondaryLabel
|
||||
descriptionLabel.numberOfLines = 2
|
||||
metaLabel.font = .preferredFont(forTextStyle: .caption1)
|
||||
metaLabel.textColor = .tertiaryLabel
|
||||
metaLabel.numberOfLines = 2
|
||||
progress.progressTintColor = .systemGreen
|
||||
let stack = UIStackView(arrangedSubviews: [titleLabel, descriptionLabel, progress, metaLabel])
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 7
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(stack)
|
||||
NSLayoutConstraint.activate([
|
||||
stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||
stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
||||
stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12),
|
||||
stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -12),
|
||||
])
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||
|
||||
func configure(_ row: MilestoneRow, disclosure: Bool) {
|
||||
accessoryType = disclosure ? .disclosureIndicator : .none
|
||||
titleLabel.text = row.title
|
||||
descriptionLabel.text = row.description
|
||||
metaLabel.text = row.meta
|
||||
let total = row.openIssues + row.closedIssues
|
||||
progress.progress = total == 0 ? 0 : Float(row.closedIssues) / Float(total)
|
||||
progress.trackTintColor = total == 0 ? .systemGray5 : .systemOrange
|
||||
progress.accessibilityLabel = "Milestone progress"
|
||||
progress.accessibilityValue = "\(row.closedIssues) closed, \(row.openIssues) open"
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class PullsViewController: RefreshingTableViewController {
|
||||
private let context: AppContext
|
||||
private var rows: [PullRow] = []
|
||||
|
||||
init(context: AppContext) {
|
||||
self.context = context
|
||||
super.init()
|
||||
title = "Pull Requests"
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
updateFilterMenu()
|
||||
loadContent(refreshing: false)
|
||||
}
|
||||
|
||||
override func loadContent(refreshing: Bool) {
|
||||
guard context.core.activeServerIndex() != nil else {
|
||||
tableView.backgroundView = EmptyBackgroundView(
|
||||
title: "No server selected",
|
||||
detail: "Open Issues or Repos to select a server or add your first one."
|
||||
)
|
||||
refreshControl?.endRefreshing()
|
||||
return
|
||||
}
|
||||
beginLoading(refreshing: refreshing)
|
||||
loadingTask?.cancel()
|
||||
loadingTask = Task {
|
||||
do {
|
||||
rows = try await context.core.pulls()
|
||||
tableView.reloadData()
|
||||
let status = context.core.settings().pullStatus
|
||||
tableView.backgroundView = rows.isEmpty
|
||||
? EmptyBackgroundView(
|
||||
title: "No \(status) pull requests",
|
||||
detail: "No pull requests match the selected status."
|
||||
)
|
||||
: nil
|
||||
} catch {
|
||||
if !Task.isCancelled { show(error: error) }
|
||||
}
|
||||
endLoading()
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
rows.count
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath
|
||||
) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "pull")
|
||||
?? UITableViewCell(style: .subtitle, reuseIdentifier: "pull")
|
||||
let row = rows[indexPath.row]
|
||||
configureTextCell(
|
||||
cell,
|
||||
title: "\(row.repository) #\(row.number)\n\(row.title)",
|
||||
detail: "\(row.summary)\n\(row.meta)"
|
||||
)
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
116
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
let row = rows[indexPath.row]
|
||||
navigationController?.pushViewController(
|
||||
PullViewController(
|
||||
context: context,
|
||||
owner: row.owner,
|
||||
repository: row.repository,
|
||||
number: row.number
|
||||
),
|
||||
animated: true
|
||||
)
|
||||
}
|
||||
|
||||
private func updateFilterMenu() {
|
||||
let current = context.core.settings().pullStatus
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
image: context.symbol("line.3.horizontal.decrease.circle"),
|
||||
menu: UIMenu(children: ["open", "closed"].map { status in
|
||||
UIAction(title: status.capitalized, state: current == status ? .on : .off) {
|
||||
[weak self] _ in
|
||||
guard let self else { return }
|
||||
do {
|
||||
try self.context.core.setPullStatus(status: status)
|
||||
self.updateFilterMenu()
|
||||
self.loadContent(refreshing: false)
|
||||
} catch {
|
||||
self.show(error: error)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class CommitsViewController: RefreshingTableViewController {
|
||||
private let context: AppContext
|
||||
private let owner: String
|
||||
private let repository: String
|
||||
private var page: CommitPage?
|
||||
private var branch: String?
|
||||
|
||||
init(context: AppContext, owner: String, repository: String) {
|
||||
self.context = context
|
||||
self.owner = owner
|
||||
self.repository = repository
|
||||
super.init()
|
||||
title = repository
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
tableView.register(CommitCell.self, forCellReuseIdentifier: "commit")
|
||||
updateBranchMenu()
|
||||
loadContent(refreshing: false)
|
||||
}
|
||||
|
||||
override func loadContent(refreshing: Bool) {
|
||||
beginLoading(refreshing: refreshing)
|
||||
loadingTask?.cancel()
|
||||
loadingTask = Task {
|
||||
do {
|
||||
page = try await context.core.commits(
|
||||
owner: owner,
|
||||
repository: repository,
|
||||
branch: branch
|
||||
)
|
||||
updateBranchMenu()
|
||||
tableView.reloadData()
|
||||
tableView.backgroundView = page?.commits.isEmpty == true
|
||||
? EmptyBackgroundView(title: "No commits", detail: "This repository has no commit history.")
|
||||
: nil
|
||||
} catch {
|
||||
if !Task.isCancelled { show(error: error) }
|
||||
}
|
||||
endLoading()
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
page?.commits.count ?? 0
|
||||
}
|
||||
|
||||
override func tableView(
|
||||
_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath
|
||||
) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "commit", for: indexPath) as! CommitCell
|
||||
if let page { cell.configure(page.commits[indexPath.row], laneCount: page.laneCount) }
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
86
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
guard let row = page?.commits[indexPath.row] else { return }
|
||||
navigationController?.pushViewController(
|
||||
FilesViewController(
|
||||
context: context,
|
||||
owner: owner,
|
||||
repository: repository,
|
||||
sha: row.sha
|
||||
),
|
||||
animated: true
|
||||
)
|
||||
}
|
||||
|
||||
private func updateBranchMenu() {
|
||||
let branches = page?.branches ?? []
|
||||
let choices: [String?] = [nil] + branches.map(Optional.some)
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
title: branch ?? "All",
|
||||
menu: UIMenu(children: choices.map { choice in
|
||||
UIAction(
|
||||
title: choice ?? "All",
|
||||
state: choice == branch ? .on : .off
|
||||
) { [weak self] _ in
|
||||
self?.branch = choice
|
||||
self?.updateBranchMenu()
|
||||
self?.loadContent(refreshing: false)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
final class CommitCell: UITableViewCell {
|
||||
private var row: CommitRow?
|
||||
private var laneCount: UInt32 = 0
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
accessoryType = .disclosureIndicator
|
||||
backgroundColor = .systemBackground
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||
|
||||
func configure(_ row: CommitRow, laneCount: UInt32) {
|
||||
self.row = row
|
||||
self.laneCount = laneCount
|
||||
var content = defaultContentConfiguration()
|
||||
content.directionalLayoutMargins.leading = laneCount == 0 ? 0 : min(72, CGFloat(laneCount) * 10 + 8)
|
||||
content.text = row.title
|
||||
content.secondaryText = row.refs.isEmpty
|
||||
? "\(row.meta)\n\(row.sha.prefix(8))"
|
||||
: "\(row.refs)\n\(row.meta) · \(row.sha.prefix(8))"
|
||||
content.secondaryTextProperties.numberOfLines = 2
|
||||
contentConfiguration = content
|
||||
setNeedsDisplay()
|
||||
}
|
||||
|
||||
override func draw(_ rect: CGRect) {
|
||||
super.draw(rect)
|
||||
guard let row, laneCount > 0, let context = UIGraphicsGetCurrentContext() else { return }
|
||||
let spacing: CGFloat = 10
|
||||
let centerY = bounds.midY
|
||||
for lane in 0..<laneCount {
|
||||
let x = 10 + CGFloat(lane) * spacing
|
||||
let color = laneColor(lane)
|
||||
context.setStrokeColor(color.cgColor)
|
||||
context.setLineWidth(3)
|
||||
if row.topLanes.contains(lane) {
|
||||
context.move(to: CGPoint(x: x, y: 0))
|
||||
context.addLine(to: CGPoint(x: x, y: centerY))
|
||||
context.strokePath()
|
||||
}
|
||||
if row.bottomLanes.contains(lane) {
|
||||
context.move(to: CGPoint(x: x, y: centerY))
|
||||
context.addLine(to: CGPoint(x: x, y: bounds.height))
|
||||
context.strokePath()
|
||||
}
|
||||
if row.connections.contains(lane), lane > 0 {
|
||||
context.move(to: CGPoint(x: x - spacing, y: centerY))
|
||||
context.addLine(to: CGPoint(x: x, y: centerY))
|
||||
context.strokePath()
|
||||
}
|
||||
if row.nodeLane == lane {
|
||||
context.setFillColor(color.cgColor)
|
||||
context.fillEllipse(in: CGRect(x: x - 5, y: centerY - 5, width: 10, height: 10))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func laneColor(_ lane: UInt32) -> UIColor {
|
||||
UIColor(hue: CGFloat((Double(lane) * 137.508).truncatingRemainder(dividingBy: 360)) / 360,
|
||||
saturation: 0.78, brightness: 0.78, alpha: 1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user