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
|
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`.
|
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
|
## Required pre-commit gates
|
||||||
|
|
||||||
Run every gate below from the repository root before committing. Every command
|
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
|
||||||
|
|
||||||
- [ ] Home shows the selected server name, contribution total, 12-month heatmap,
|
- [ ] Home shows the selected server name, contribution total, six labeled
|
||||||
and recent activity.
|
months with gaps between them, and recent activity.
|
||||||
- [ ] Activity rows have the correct icon, repository, summary, and date.
|
- [ ] Activity rows have the correct icon, repository, summary, and date.
|
||||||
- [ ] Tap repository, issue, pull-request, and commit activity. Each opens the
|
- [ ] Tap repository, issue, pull-request, and commit activity. Each opens the
|
||||||
matching native tab and destination.
|
matching native tab and destination.
|
||||||
|
|||||||
@@ -149,6 +149,8 @@ pub struct HeatCell {
|
|||||||
pub week: u32,
|
pub week: u32,
|
||||||
pub day: u32,
|
pub day: u32,
|
||||||
pub level: u32,
|
pub level: u32,
|
||||||
|
pub timestamp: i64,
|
||||||
|
pub contributions: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, uniffi::Record)]
|
#[derive(Clone, uniffi::Record)]
|
||||||
@@ -585,7 +587,7 @@ fn activity_detail(activity: &models::Activity) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn heat_cells(data: &[models::UserHeatmapData]) -> (Vec<HeatCell>, i64) {
|
fn heat_cells(data: &[models::UserHeatmapData]) -> (Vec<HeatCell>, i64) {
|
||||||
const DAYS: i64 = 52 * 7;
|
const DAYS: i64 = 6 * 31;
|
||||||
let latest = data
|
let latest = data
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|entry| entry.timestamp)
|
.filter_map(|entry| entry.timestamp)
|
||||||
@@ -613,6 +615,8 @@ fn heat_cells(data: &[models::UserHeatmapData]) -> (Vec<HeatCell>, i64) {
|
|||||||
} else {
|
} else {
|
||||||
((count * 4 + maximum - 1) / maximum).clamp(1, 4) as u32
|
((count * 4 + maximum - 1) / maximum).clamp(1, 4) as u32
|
||||||
},
|
},
|
||||||
|
timestamp: (start + index as i64) * 86_400,
|
||||||
|
contributions: count,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
(cells, contribution_count)
|
(cells, contribution_count)
|
||||||
@@ -767,8 +771,16 @@ mod tests {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
let (cells, total) = heat_cells(&heatmap);
|
let (cells, total) = heat_cells(&heatmap);
|
||||||
assert_eq!((cells.len(), total), (364, 5));
|
assert_eq!((cells.len(), total), (186, 5));
|
||||||
assert_eq!((cells[362].level, cells[363].level), (1, 4));
|
assert_eq!(
|
||||||
|
(
|
||||||
|
cells[184].level,
|
||||||
|
cells[184].timestamp,
|
||||||
|
cells[184].contributions,
|
||||||
|
cells[185].level,
|
||||||
|
),
|
||||||
|
(1, 100 * 86_400, 1, 4)
|
||||||
|
);
|
||||||
|
|
||||||
let label = models::Label {
|
let label = models::Label {
|
||||||
name: Some("bug".into()),
|
name: Some("bug".into()),
|
||||||
|
|||||||
@@ -1479,13 +1479,17 @@ public struct HeatCell: Equatable, Hashable {
|
|||||||
public var week: UInt32
|
public var week: UInt32
|
||||||
public var day: UInt32
|
public var day: UInt32
|
||||||
public var level: UInt32
|
public var level: UInt32
|
||||||
|
public var timestamp: Int64
|
||||||
|
public var contributions: Int64
|
||||||
|
|
||||||
// Default memberwise initializers are never public by default, so we
|
// Default memberwise initializers are never public by default, so we
|
||||||
// declare one manually.
|
// 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.week = week
|
||||||
self.day = day
|
self.day = day
|
||||||
self.level = level
|
self.level = level
|
||||||
|
self.timestamp = timestamp
|
||||||
|
self.contributions = contributions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1506,7 +1510,9 @@ public struct FfiConverterTypeHeatCell: FfiConverterRustBuffer {
|
|||||||
try HeatCell(
|
try HeatCell(
|
||||||
week: FfiConverterUInt32.read(from: &buf),
|
week: FfiConverterUInt32.read(from: &buf),
|
||||||
day: 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.week, into: &buf)
|
||||||
FfiConverterUInt32.write(value.day, into: &buf)
|
FfiConverterUInt32.write(value.day, into: &buf)
|
||||||
FfiConverterUInt32.write(value.level, 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 {
|
final class HeatmapView: UIView {
|
||||||
private let cells: [HeatCell]
|
private let cells: [HeatCell]
|
||||||
|
private let calendar = Calendar(identifier: .gregorian)
|
||||||
|
|
||||||
init(page: HomePage) {
|
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))
|
super.init(frame: CGRect(x: 0, y: 0, width: 0, height: 132))
|
||||||
backgroundColor = .systemBackground
|
backgroundColor = .systemBackground
|
||||||
let title = UILabel(frame: CGRect(x: 16, y: 12, width: 300, height: 22))
|
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.font = .preferredFont(forTextStyle: .subheadline)
|
||||||
title.textColor = .secondaryLabel
|
title.textColor = .secondaryLabel
|
||||||
addSubview(title)
|
addSubview(title)
|
||||||
let total = UILabel(frame: CGRect(x: 16, y: 104, width: 300, height: 18))
|
let total = UILabel(frame: CGRect(x: 16, y: 108, width: 300, height: 18))
|
||||||
total.text = "\(page.contributionCount) contributions"
|
total.text = "\(cells.reduce(0) { $0 + $1.contributions }) contributions"
|
||||||
total.font = .preferredFont(forTextStyle: .caption1)
|
total.font = .preferredFont(forTextStyle: .caption1)
|
||||||
total.textColor = .tertiaryLabel
|
total.textColor = .tertiaryLabel
|
||||||
addSubview(total)
|
addSubview(total)
|
||||||
@@ -424,9 +438,51 @@ final class HeatmapView: UIView {
|
|||||||
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||||
|
|
||||||
override func draw(_ rect: CGRect) {
|
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 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 {
|
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] = [
|
let colors: [UIColor] = [
|
||||||
.systemGray5,
|
.systemGray5,
|
||||||
UIColor(red: 0.72, green: 0.85, blue: 0.96, alpha: 1),
|
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()
|
colors[Int(min(cell.level, 4))].setFill()
|
||||||
UIBezierPath(
|
UIBezierPath(
|
||||||
roundedRect: CGRect(
|
roundedRect: CGRect(
|
||||||
x: 16 + CGFloat(cell.week) * gap,
|
x: 16 + CGFloat(days / 7) * gap + CGFloat(monthIndex) * monthGap,
|
||||||
y: 43 + CGFloat(cell.day) * gap,
|
y: 48 + CGFloat(calendar.component(.weekday, from: date) - 1) * gap,
|
||||||
width: width,
|
width: width,
|
||||||
height: width
|
height: width
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user