Show milestone and pull request states

This commit is contained in:
Georg Bauer
2026-07-31 18:39:10 +02:00
parent acf2c839ad
commit 128be97e20
6 changed files with 148 additions and 33 deletions

View File

@@ -2617,6 +2617,7 @@ public func FfiConverterTypeMilestonePage_lower(_ value: MilestonePage) -> RustB
public struct MilestoneRow: Equatable, Hashable {
public var id: Int64
public var state: String
public var title: String
public var description: String
public var meta: String
@@ -2626,8 +2627,9 @@ public struct MilestoneRow: Equatable, Hashable {
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(id: Int64, title: String, description: String, meta: String, hasIssues: Bool, progress: Double, progressAccessibility: String) {
public init(id: Int64, state: String, title: String, description: String, meta: String, hasIssues: Bool, progress: Double, progressAccessibility: String) {
self.id = id
self.state = state
self.title = title
self.description = description
self.meta = meta
@@ -2653,6 +2655,7 @@ public struct FfiConverterTypeMilestoneRow: FfiConverterRustBuffer {
return
try MilestoneRow(
id: FfiConverterInt64.read(from: &buf),
state: FfiConverterString.read(from: &buf),
title: FfiConverterString.read(from: &buf),
description: FfiConverterString.read(from: &buf),
meta: FfiConverterString.read(from: &buf),
@@ -2664,6 +2667,7 @@ public struct FfiConverterTypeMilestoneRow: FfiConverterRustBuffer {
public static func write(_ value: MilestoneRow, into buf: inout [UInt8]) {
FfiConverterInt64.write(value.id, into: &buf)
FfiConverterString.write(value.state, into: &buf)
FfiConverterString.write(value.title, into: &buf)
FfiConverterString.write(value.description, into: &buf)
FfiConverterString.write(value.meta, into: &buf)
@@ -2799,6 +2803,7 @@ public func FfiConverterTypePullListPage_lower(_ value: PullListPage) -> RustBuf
public struct PullPage: Equatable, Hashable {
public var title: String
public var state: String
public var meta: String
public var body: String
public var filesRef: String
@@ -2808,8 +2813,9 @@ public struct PullPage: Equatable, Hashable {
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(title: String, meta: String, body: String, filesRef: String, files: [FileRow], comments: [CommentRow], hasMore: Bool) {
public init(title: String, state: String, meta: String, body: String, filesRef: String, files: [FileRow], comments: [CommentRow], hasMore: Bool) {
self.title = title
self.state = state
self.meta = meta
self.body = body
self.filesRef = filesRef
@@ -2835,6 +2841,7 @@ public struct FfiConverterTypePullPage: FfiConverterRustBuffer {
return
try PullPage(
title: FfiConverterString.read(from: &buf),
state: FfiConverterString.read(from: &buf),
meta: FfiConverterString.read(from: &buf),
body: FfiConverterString.read(from: &buf),
filesRef: FfiConverterString.read(from: &buf),
@@ -2846,6 +2853,7 @@ public struct FfiConverterTypePullPage: FfiConverterRustBuffer {
public static func write(_ value: PullPage, into buf: inout [UInt8]) {
FfiConverterString.write(value.title, into: &buf)
FfiConverterString.write(value.state, into: &buf)
FfiConverterString.write(value.meta, into: &buf)
FfiConverterString.write(value.body, into: &buf)
FfiConverterString.write(value.filesRef, into: &buf)
@@ -2875,16 +2883,18 @@ public struct PullRow: Equatable, Hashable {
public var number: Int64
public var owner: String
public var repository: String
public var state: String
public var title: String
public var summary: String
public var meta: String
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(number: Int64, owner: String, repository: String, title: String, summary: String, meta: String) {
public init(number: Int64, owner: String, repository: String, state: String, title: String, summary: String, meta: String) {
self.number = number
self.owner = owner
self.repository = repository
self.state = state
self.title = title
self.summary = summary
self.meta = meta
@@ -2909,6 +2919,7 @@ public struct FfiConverterTypePullRow: FfiConverterRustBuffer {
number: FfiConverterInt64.read(from: &buf),
owner: FfiConverterString.read(from: &buf),
repository: FfiConverterString.read(from: &buf),
state: FfiConverterString.read(from: &buf),
title: FfiConverterString.read(from: &buf),
summary: FfiConverterString.read(from: &buf),
meta: FfiConverterString.read(from: &buf)
@@ -2919,6 +2930,7 @@ public struct FfiConverterTypePullRow: FfiConverterRustBuffer {
FfiConverterInt64.write(value.number, into: &buf)
FfiConverterString.write(value.owner, into: &buf)
FfiConverterString.write(value.repository, into: &buf)
FfiConverterString.write(value.state, into: &buf)
FfiConverterString.write(value.title, into: &buf)
FfiConverterString.write(value.summary, into: &buf)
FfiConverterString.write(value.meta, into: &buf)

View File

@@ -518,6 +518,7 @@ final class MilestoneViewController: RefreshingTableViewController {
super.viewDidLoad()
tableView.register(MilestoneCell.self, forCellReuseIdentifier: "milestone")
tableView.register(IssueCell.self, forCellReuseIdentifier: "issue")
tableView.register(PullCell.self, forCellReuseIdentifier: "pull")
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 118
let editButton = UIBarButtonItem(
@@ -633,11 +634,8 @@ final class MilestoneViewController: RefreshingTableViewController {
cell.configure(page.issues[indexPath.row])
return cell
}
let cell = tableView.dequeueReusableCell(withIdentifier: "pull")
?? UITableViewCell(style: .subtitle, reuseIdentifier: "pull")
let pull = page.pulls[indexPath.row]
configureTextCell(cell, title: pull.title, detail: "\(pull.summary)\n\(pull.meta)")
cell.accessoryType = .disclosureIndicator
let cell = tableView.dequeueReusableCell(withIdentifier: "pull", for: indexPath) as! PullCell
cell.configure(page.pulls[indexPath.row])
return cell
}
@@ -668,6 +666,7 @@ final class MilestoneViewController: RefreshingTableViewController {
}
final class MilestoneCell: UITableViewCell {
private let stateIcon = UIImageView()
private let titleLabel = UILabel()
private let descriptionLabel = UILabel()
private let metaLabel = UILabel()
@@ -677,6 +676,9 @@ final class MilestoneCell: UITableViewCell {
super.init(style: style, reuseIdentifier: reuseIdentifier)
titleLabel.font = .preferredFont(forTextStyle: .headline)
titleLabel.numberOfLines = 2
let titleStack = UIStackView(arrangedSubviews: [stateIcon, titleLabel])
titleStack.alignment = .firstBaseline
titleStack.spacing = 8
descriptionLabel.font = .preferredFont(forTextStyle: .subheadline)
descriptionLabel.textColor = .secondaryLabel
descriptionLabel.numberOfLines = 2
@@ -684,7 +686,9 @@ final class MilestoneCell: UITableViewCell {
metaLabel.textColor = .tertiaryLabel
metaLabel.numberOfLines = 2
progress.progressTintColor = .systemGreen
let stack = UIStackView(arrangedSubviews: [titleLabel, descriptionLabel, progress, metaLabel])
let stack = UIStackView(
arrangedSubviews: [titleStack, descriptionLabel, progress, metaLabel]
)
stack.axis = .vertical
stack.spacing = 7
stack.translatesAutoresizingMaskIntoConstraints = false
@@ -702,6 +706,12 @@ final class MilestoneCell: UITableViewCell {
func configure(_ row: MilestoneRow, disclosure: Bool) {
accessoryType = disclosure ? .disclosureIndicator : .none
configureOpenClosedStateIcon(
stateIcon,
state: row.state,
subject: "milestone",
textStyle: .headline
)
titleLabel.text = row.title
descriptionLabel.text = row.description
metaLabel.text = row.meta
@@ -712,6 +722,58 @@ final class MilestoneCell: UITableViewCell {
}
}
final class PullCell: UITableViewCell {
private let stateIcon = UIImageView()
private let titleLabel = UILabel()
private let summaryLabel = UILabel()
private let metaLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
accessoryType = .disclosureIndicator
titleLabel.font = .preferredFont(forTextStyle: .headline)
titleLabel.numberOfLines = 2
let titleStack = UIStackView(arrangedSubviews: [stateIcon, titleLabel])
titleStack.alignment = .firstBaseline
titleStack.spacing = 8
summaryLabel.font = .preferredFont(forTextStyle: .subheadline)
summaryLabel.textColor = .secondaryLabel
summaryLabel.numberOfLines = 2
metaLabel.font = .preferredFont(forTextStyle: .caption1)
metaLabel.textColor = .tertiaryLabel
metaLabel.numberOfLines = 2
[titleLabel, summaryLabel, metaLabel].forEach {
$0.adjustsFontForContentSizeCategory = true
}
let stack = UIStackView(arrangedSubviews: [titleStack, summaryLabel, metaLabel])
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: PullRow) {
configureOpenClosedStateIcon(
stateIcon,
state: row.state,
subject: "pull request",
textStyle: .headline
)
titleLabel.text = row.title
summaryLabel.text = row.summary
metaLabel.text = row.meta
}
}
@MainActor
final class PullsViewController: RefreshingTableViewController {
private let context: AppContext
@@ -733,6 +795,9 @@ final class PullsViewController: RefreshingTableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(PullCell.self, forCellReuseIdentifier: "pull")
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 116
navigationItem.leftBarButtonItem = UIBarButtonItem(
image: context.symbol("server.rack"),
primaryAction: UIAction { [weak self] _ in
@@ -821,22 +886,11 @@ final class PullsViewController: RefreshingTableViewController {
_ 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.title,
detail: "\(row.summary)\n\(row.meta)"
)
cell.accessoryType = .disclosureIndicator
let cell = tableView.dequeueReusableCell(withIdentifier: "pull", for: indexPath) as! PullCell
cell.configure(rows[indexPath.row])
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]

View File

@@ -227,7 +227,8 @@ final class IssueViewController: MarkdownPageViewController {
if let page {
replaceContent(detailViews(
title: page.title,
issueState: page.state,
state: page.state,
stateSubject: "issue",
meta: page.meta,
body: page.body,
comments: page.comments,
@@ -305,6 +306,8 @@ final class PullViewController: MarkdownPageViewController {
if let page {
var views = detailViews(
title: page.title,
state: page.state,
stateSubject: "pull request",
meta: page.meta,
body: page.body,
comments: []
@@ -1138,7 +1141,8 @@ final class DiffViewController: UIViewController {
private func detailViews(
title: String,
issueState: String? = nil,
state: String? = nil,
stateSubject: String = "",
meta: String,
body: String,
comments: [CommentRow],
@@ -1156,7 +1160,10 @@ private func detailViews(
metaLabel.textColor = .secondaryLabel
metaLabel.numberOfLines = 0
let bodyView = markdownView(body)
var views: [UIView] = [issueState.map { issueTitle(titleLabel, state: $0) } ?? titleLabel, metaLabel]
var views: [UIView] = [
state.map { stateTitle(titleLabel, state: $0, subject: stateSubject) } ?? titleLabel,
metaLabel,
]
if !milestone.isEmpty {
let milestoneLabel = UILabel()
milestoneLabel.attributedText = symbolText(
@@ -1172,9 +1179,9 @@ private func detailViews(
return views + [separator(), bodyView] + commentViews(comments, editComment: editComment)
}
private func issueTitle(_ titleLabel: UILabel, state: String) -> UIView {
private func stateTitle(_ titleLabel: UILabel, state: String, subject: String) -> UIView {
let icon = UIImageView()
configureIssueStateIcon(icon, state: state, textStyle: .title1)
configureOpenClosedStateIcon(icon, state: state, subject: subject, textStyle: .title1)
let stack = UIStackView(arrangedSubviews: [icon, titleLabel])
stack.axis = .horizontal
stack.alignment = .firstBaseline

View File

@@ -267,17 +267,32 @@ func configureIssueStateIcon(
_ icon: UIImageView,
state: String,
textStyle: UIFont.TextStyle
) {
configureOpenClosedStateIcon(icon, state: state, subject: "issue", textStyle: textStyle)
}
func configureOpenClosedStateIcon(
_ icon: UIImageView,
state: String,
subject: String,
textStyle: UIFont.TextStyle
) {
let symbol: String
let color: UIColor
let accessibilityLabel: String
switch state {
case "open":
(symbol, color, accessibilityLabel) = ("exclamationmark.circle.fill", .systemGreen, "Open issue")
(symbol, color, accessibilityLabel) = (
"exclamationmark.circle.fill", .systemGreen, "Open \(subject)"
)
case "closed":
(symbol, color, accessibilityLabel) = ("checkmark.circle.fill", .systemPurple, "Closed issue")
(symbol, color, accessibilityLabel) = (
"checkmark.circle.fill", .systemPurple, "Closed \(subject)"
)
default:
(symbol, color, accessibilityLabel) = ("questionmark.circle.fill", .systemGray, "Unknown issue state")
(symbol, color, accessibilityLabel) = (
"questionmark.circle.fill", .systemGray, "Unknown \(subject) state"
)
}
icon.image = UIImage(systemName: symbol)
icon.tintColor = color