Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gitframe/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ def _rows(snapshot: Snapshot) -> list[dict[str, str]]:
"sha": commit.short_oid,
"repo": commit.repository,
"when": github.relative_time(commit.committed_at),
"url": commit.url,
}
for commit in snapshot.commits
]
Expand Down
8 changes: 7 additions & 1 deletion gitframe/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,12 @@ def _history(repo: dict[str, Any]) -> dict[str, Any]:


def _repo_commits(repo: dict[str, Any]) -> list[CommitEntry]:
"""Every commit the history carried, in the order the API returned it."""
"""Every commit the history carried, in the order the API returned it.

The commit's page is built from the repository's URL rather than asked
for, so a snapshot cached before the rows were links still opens them.
"""
repo_url = repo.get("url") or ""
entries: list[CommitEntry] = []
for commit in _history(repo).get("nodes") or []:
author = (commit.get("author") or {}).get("user") or {}
Expand All @@ -217,6 +222,7 @@ def _repo_commits(repo: dict[str, Any]) -> list[CommitEntry]:
committed_at=commit["committedDate"],
author_login=author.get("login") or "",
author_avatar_url=author.get("avatarUrl") or "",
url=f"{repo_url}/commit/{commit['oid']}" if repo_url else "",
)
)
return entries
Expand Down
1 change: 1 addition & 0 deletions gitframe/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class CommitEntry:
committed_at: str
author_login: str = ""
author_avatar_url: str = ""
url: str = ""

@property
def short_oid(self) -> str:
Expand Down
77 changes: 74 additions & 3 deletions qml/ActivityList.qml
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,56 @@ import "."
// The commit rows: headline, short sha, the repository it landed in, and how
// long ago it was committed. `Config.activityRows` sets how many there are;
// the taller window is what paid for them.
//
// The rows are links to their commit on GitHub, and the card is the last of
// the Overview column's keyboard regions, below the grid. The cursor and the
// pointer draw the same mark, the grey `Config.hover` fill, and the last
// device to speak owns it - the Repositories list's rule.
Card {
id: root

required property var api

// False while the window's cursor sits outside this card.
property bool focused: false
property int cursor: 0
property bool mouseLeads: false

// Clamped here too: the card is sized for exactly this many rows, and a
// cache written before the count changed is longer.
readonly property var rows: root.api.commits.slice(0, Config.activityRows)

title: "Recent Activity"
glyph: Config.glyphCommits
height: Config.activityHeight

// A refresh can bring fewer rows than the cursor was walking.
onRowsChanged: if (root.rows.length > 0)
root.cursor = Math.min(root.cursor, root.rows.length - 1)

// False when `k` leaves the first row: the grid above takes it over.
function moveVertical(delta) {
const next = root.cursor + delta;
if (next < 0)
return false;
root.cursor = Math.min(next, Math.max(root.rows.length - 1, 0));
return true;
}

function activate() {
root.open(root.rows[root.cursor]);
}

// Same as the Overview avatar: the browser takes over from here, so the
// window closes behind it, and the open goes through the backend so it
// outlives the quit.
function open(commit) {
if (commit && commit.url) {
root.api.openUrl(commit.url);
Qt.quit();
}
}

Column {
anchors.fill: parent
spacing: 0
Expand All @@ -27,16 +68,33 @@ Card {
}

Repeater {
// Clamped here too: the card is sized for exactly this many
// rows, and a cache written before the count changed is longer.
model: root.api.commits.slice(0, Config.activityRows)
model: root.rows

Item {
id: row

required property int index
required property var modelData

width: root.width - 2 * Config.cardPad
height: Config.activityRowHeight

// Bleeds `gap` into the card's padding on both sides, so the
// text does not sit flush against the edge of its own fill.
Rectangle {
anchors.fill: parent
anchors.leftMargin: -Config.gap
anchors.rightMargin: -Config.gap
color: (root.mouseLeads && area.containsMouse)
|| (root.focused && !root.mouseLeads
&& row.index === root.cursor)
? Config.hover : Config.clear

Behavior on color {
ColorAnimation { duration: Config.dur(Config.animFast) }
}
}

Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
Expand Down Expand Up @@ -87,6 +145,19 @@ Card {
font.family: Config.font
font.pixelSize: Config.fontSize
}

MouseArea {
id: area

anchors.fill: parent
anchors.leftMargin: -Config.gap
anchors.rightMargin: -Config.gap
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: root.mouseLeads = true
onPositionChanged: root.mouseLeads = true
onClicked: root.open(row.modelData)
}
}
}
}
Expand Down
12 changes: 8 additions & 4 deletions qml/Heatmap.qml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import "."
// The card holds two of the Overview column's keyboard regions, stacked the
// way they are drawn: the year control on top and the grid below it.
// `Overview.qml` calls the move functions below; leaving the year upwards is
// its business, since the avatar above belongs to another card.
// its business, since the avatar above belongs to another card, and so is
// leaving the grid downwards, onto Recent Activity.
Card {
id: root

Expand Down Expand Up @@ -134,21 +135,24 @@ Card {
return true;
}

// False when the cursor would step below its week: the grid's bottom
// edge, where Recent Activity takes the cursor over.
function moveVertical(delta) {
if (root.region < 0 || !root.cursorCell) {
if (delta > 0 && root.cursorIso !== "")
root.region = 0;
return;
return true;
}
const week = root.weeks[root.cursorCol];
const row = root.cursorRow + delta;
if (row < week[0].row) {
root.region = -1;
return;
return true;
}
if (row > week[week.length - 1].row)
return;
return false;
root.setCursor(root.cursorCol, row);
return true;
}

// -- year --------------------------------------------------------------
Expand Down
26 changes: 20 additions & 6 deletions qml/Overview.qml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import "."

// The content column of the Overview screen: the four blocks stacked with the
// same gap. It also owns its own chain of keyboard regions - the avatar on
// top, then the year control, then the grid - the way the heatmap owns the two
// it draws. `Main.qml` still handles every key; this only exposes the moves.
// top, then the year control, then the grid, then Recent Activity - the way
// the heatmap owns the two it draws. `Main.qml` still handles every key; this only exposes the moves.
Item {
id: root

Expand All @@ -16,18 +16,21 @@ Item {
// The topmost region. It starts on the avatar, so the first `l` out of the
// sidebar lands there; after that the position is simply kept.
property bool onAvatar: true
// The bottom region, below the grid.
property bool onActivity: false

// Part of the interface `Main.qml` calls on whichever column is on show:
// a key takes the highlight back from the pointer.
function releaseMouse() {
heatmap.mouseLeads = false;
activity.mouseLeads = false;
}

// Returns false when the cursor is already at this column's left edge and
// the key belongs to the sidebar instead.
function moveHorizontal(delta) {
if (root.onAvatar)
return delta > 0; // nothing sits right of the avatar either
if (root.onAvatar || root.onActivity)
return delta > 0; // nothing sits beside either of them
return heatmap.moveHorizontal(delta);
}

Expand All @@ -37,16 +40,24 @@ Item {
root.onAvatar = false;
return;
}
if (root.onActivity) {
if (!activity.moveVertical(delta))
root.onActivity = false;
return;
}
if (delta < 0 && heatmap.region < 0) {
root.onAvatar = true;
return;
}
heatmap.moveVertical(delta);
if (!heatmap.moveVertical(delta) && activity.rows.length > 0)
root.onActivity = true;
}

function activate() {
if (root.onAvatar)
header.open();
else if (root.onActivity)
activity.activate();
}

Column {
Expand Down Expand Up @@ -77,12 +88,15 @@ Item {

width: Config.heatmapCardWidth
api: root.api
focused: root.focused && !root.onAvatar
focused: root.focused && !root.onAvatar && !root.onActivity
}

ActivityList {
id: activity

width: Config.heatmapCardWidth
api: root.api
focused: root.focused && root.onActivity
}
}

Expand Down
Loading