Show six labeled activity months
This commit is contained in:
@@ -6,6 +6,12 @@ Use the `tea` command-line tool as the preferred way to list, inspect, create,
|
||||
and update Gitea issues. Run it from the repository root so it can use the
|
||||
configured remote, for example `tea issues` or `tea issues 1`.
|
||||
|
||||
Run issue-detail commands directly, for example `tea issues 7`. When invoking
|
||||
`tea` through an automated shell runner, allocate a pseudo-terminal (PTY);
|
||||
interactive terminal use needs no extra flag. The detail command's terminal
|
||||
rendering is unreliable without a PTY and may panic inside Gitea SDK version
|
||||
handling.
|
||||
|
||||
## Required pre-commit gates
|
||||
|
||||
Run every gate below from the repository root before committing. Every command
|
||||
|
||||
@@ -108,8 +108,8 @@ xcrun simctl launch booted de.rfc1437.gotcha
|
||||
|
||||
## Home
|
||||
|
||||
- [ ] Home shows the selected server name, contribution total, 12-month heatmap,
|
||||
and recent activity.
|
||||
- [ ] Home shows the selected server name, contribution total, six labeled
|
||||
months with gaps between them, and recent activity.
|
||||
- [ ] Activity rows have the correct icon, repository, summary, and date.
|
||||
- [ ] Tap repository, issue, pull-request, and commit activity. Each opens the
|
||||
matching native tab and destination.
|
||||
|
||||
@@ -149,6 +149,8 @@ pub struct HeatCell {
|
||||
pub week: u32,
|
||||
pub day: u32,
|
||||
pub level: u32,
|
||||
pub timestamp: i64,
|
||||
pub contributions: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
@@ -585,7 +587,7 @@ fn activity_detail(activity: &models::Activity) -> String {
|
||||
}
|
||||
|
||||
fn heat_cells(data: &[models::UserHeatmapData]) -> (Vec<HeatCell>, i64) {
|
||||
const DAYS: i64 = 52 * 7;
|
||||
const DAYS: i64 = 6 * 31;
|
||||
let latest = data
|
||||
.iter()
|
||||
.filter_map(|entry| entry.timestamp)
|
||||
@@ -613,6 +615,8 @@ fn heat_cells(data: &[models::UserHeatmapData]) -> (Vec<HeatCell>, i64) {
|
||||
} else {
|
||||
((count * 4 + maximum - 1) / maximum).clamp(1, 4) as u32
|
||||
},
|
||||
timestamp: (start + index as i64) * 86_400,
|
||||
contributions: count,
|
||||
})
|
||||
.collect();
|
||||
(cells, contribution_count)
|
||||
@@ -767,8 +771,16 @@ mod tests {
|
||||
},
|
||||
];
|
||||
let (cells, total) = heat_cells(&heatmap);
|
||||
assert_eq!((cells.len(), total), (364, 5));
|
||||
assert_eq!((cells[362].level, cells[363].level), (1, 4));
|
||||
assert_eq!((cells.len(), total), (186, 5));
|
||||
assert_eq!(
|
||||
(
|
||||
cells[184].level,
|
||||
cells[184].timestamp,
|
||||
cells[184].contributions,
|
||||
cells[185].level,
|
||||
),
|
||||
(1, 100 * 86_400, 1, 4)
|
||||
);
|
||||
|
||||
let label = models::Label {
|
||||
name: Some("bug".into()),
|
||||
|
||||
@@ -1479,13 +1479,17 @@ public struct HeatCell: Equatable, Hashable {
|
||||
public var week: UInt32
|
||||
public var day: UInt32
|
||||
public var level: UInt32
|
||||
public var timestamp: Int64
|
||||
public var contributions: Int64
|
||||
|
||||
// Default memberwise initializers are never public by default, so we
|
||||
// declare one manually.
|
||||
public init(week: UInt32, day: UInt32, level: UInt32) {
|
||||
public init(week: UInt32, day: UInt32, level: UInt32, timestamp: Int64, contributions: Int64) {
|
||||
self.week = week
|
||||
self.day = day
|
||||
self.level = level
|
||||
self.timestamp = timestamp
|
||||
self.contributions = contributions
|
||||
}
|
||||
|
||||
|
||||
@@ -1506,7 +1510,9 @@ public struct FfiConverterTypeHeatCell: FfiConverterRustBuffer {
|
||||
try HeatCell(
|
||||
week: FfiConverterUInt32.read(from: &buf),
|
||||
day: FfiConverterUInt32.read(from: &buf),
|
||||
level: FfiConverterUInt32.read(from: &buf)
|
||||
level: FfiConverterUInt32.read(from: &buf),
|
||||
timestamp: FfiConverterInt64.read(from: &buf),
|
||||
contributions: FfiConverterInt64.read(from: &buf)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1514,6 +1520,8 @@ public struct FfiConverterTypeHeatCell: FfiConverterRustBuffer {
|
||||
FfiConverterUInt32.write(value.week, into: &buf)
|
||||
FfiConverterUInt32.write(value.day, into: &buf)
|
||||
FfiConverterUInt32.write(value.level, into: &buf)
|
||||
FfiConverterInt64.write(value.timestamp, into: &buf)
|
||||
FfiConverterInt64.write(value.contributions, into: &buf)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2855,4 +2863,4 @@ public func uniffiEnsureGotchaCoreInitialized() {
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:enable all
|
||||
// swiftlint:enable all
|
||||
|
||||
@@ -403,18 +403,32 @@ final class HomeViewController: RefreshingTableViewController {
|
||||
|
||||
final class HeatmapView: UIView {
|
||||
private let cells: [HeatCell]
|
||||
private let calendar = Calendar(identifier: .gregorian)
|
||||
|
||||
init(page: HomePage) {
|
||||
cells = page.heatCells
|
||||
let latest = page.heatCells.last.map {
|
||||
Date(timeIntervalSince1970: TimeInterval($0.timestamp))
|
||||
} ?? Date()
|
||||
let latestMonth = Calendar(identifier: .gregorian).date(
|
||||
from: Calendar(identifier: .gregorian).dateComponents([.year, .month], from: latest)
|
||||
) ?? latest
|
||||
let firstMonth = Calendar(identifier: .gregorian).date(
|
||||
byAdding: .month,
|
||||
value: -5,
|
||||
to: latestMonth
|
||||
) ?? latestMonth
|
||||
cells = page.heatCells.filter {
|
||||
Date(timeIntervalSince1970: TimeInterval($0.timestamp)) >= firstMonth
|
||||
}
|
||||
super.init(frame: CGRect(x: 0, y: 0, width: 0, height: 132))
|
||||
backgroundColor = .systemBackground
|
||||
let title = UILabel(frame: CGRect(x: 16, y: 12, width: 300, height: 22))
|
||||
title.text = "Activity · last 12 months"
|
||||
title.text = "Activity · last 6 months"
|
||||
title.font = .preferredFont(forTextStyle: .subheadline)
|
||||
title.textColor = .secondaryLabel
|
||||
addSubview(title)
|
||||
let total = UILabel(frame: CGRect(x: 16, y: 104, width: 300, height: 18))
|
||||
total.text = "\(page.contributionCount) contributions"
|
||||
let total = UILabel(frame: CGRect(x: 16, y: 108, width: 300, height: 18))
|
||||
total.text = "\(cells.reduce(0) { $0 + $1.contributions }) contributions"
|
||||
total.font = .preferredFont(forTextStyle: .caption1)
|
||||
total.textColor = .tertiaryLabel
|
||||
addSubview(total)
|
||||
@@ -424,9 +438,51 @@ final class HeatmapView: UIView {
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||
|
||||
override func draw(_ rect: CGRect) {
|
||||
let width = max(4, min(6, (bounds.width - 32) / 55))
|
||||
guard
|
||||
let first = cells.first,
|
||||
let last = cells.last
|
||||
else { return }
|
||||
let firstDate = Date(timeIntervalSince1970: TimeInterval(first.timestamp))
|
||||
let lastDate = Date(timeIntervalSince1970: TimeInterval(last.timestamp))
|
||||
guard let firstWeek = calendar.dateInterval(of: .weekOfYear, for: firstDate)?.start else {
|
||||
return
|
||||
}
|
||||
let monthFormatter = DateFormatter()
|
||||
monthFormatter.dateFormat = "MMM"
|
||||
let months = cells.reduce(into: [Date]()) { result, cell in
|
||||
let date = Date(timeIntervalSince1970: TimeInterval(cell.timestamp))
|
||||
let month = calendar.date(
|
||||
from: calendar.dateComponents([.year, .month], from: date)
|
||||
) ?? date
|
||||
if result.last != month { result.append(month) }
|
||||
}
|
||||
let weekCount = CGFloat(
|
||||
(calendar.dateComponents([.day], from: firstWeek, to: lastDate).day ?? 0) / 7 + 1
|
||||
)
|
||||
let monthGap: CGFloat = 4
|
||||
let width = max(4, min(6, (bounds.width - 32) / weekCount - 1))
|
||||
let gap = width + 1
|
||||
let labelAttributes: [NSAttributedString.Key: Any] = [
|
||||
.font: UIFont.preferredFont(forTextStyle: .caption2),
|
||||
.foregroundColor: UIColor.secondaryLabel,
|
||||
]
|
||||
for (index, month) in months.enumerated() {
|
||||
let days = calendar.dateComponents([.day], from: firstWeek, to: month).day ?? 0
|
||||
monthFormatter.string(from: month).draw(
|
||||
at: CGPoint(
|
||||
x: 16 + CGFloat(days / 7) * gap + CGFloat(index) * monthGap,
|
||||
y: 34
|
||||
),
|
||||
withAttributes: labelAttributes
|
||||
)
|
||||
}
|
||||
for cell in cells {
|
||||
let date = Date(timeIntervalSince1970: TimeInterval(cell.timestamp))
|
||||
let days = calendar.dateComponents([.day], from: firstWeek, to: date).day ?? 0
|
||||
let month = calendar.date(
|
||||
from: calendar.dateComponents([.year, .month], from: date)
|
||||
) ?? date
|
||||
let monthIndex = months.firstIndex(of: month) ?? 0
|
||||
let colors: [UIColor] = [
|
||||
.systemGray5,
|
||||
UIColor(red: 0.72, green: 0.85, blue: 0.96, alpha: 1),
|
||||
@@ -437,8 +493,8 @@ final class HeatmapView: UIView {
|
||||
colors[Int(min(cell.level, 4))].setFill()
|
||||
UIBezierPath(
|
||||
roundedRect: CGRect(
|
||||
x: 16 + CGFloat(cell.week) * gap,
|
||||
y: 43 + CGFloat(cell.day) * gap,
|
||||
x: 16 + CGFloat(days / 7) * gap + CGFloat(monthIndex) * monthGap,
|
||||
y: 48 + CGFloat(calendar.component(.weekday, from: date) - 1) * gap,
|
||||
width: width,
|
||||
height: width
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user