Align repo action buttons
- SHA
8ae944ae448ed2ab21ebc58e30502aa0eb998a2b- Parents
-
598b09f - Tree
73fe7a3
8ae944a
8ae944ae448ed2ab21ebc58e30502aa0eb998a2b598b09f
73fe7a3internal/web/handlers/repo/code.gomodified@@ -168,6 +168,7 @@ func (h *Handlers) renderRepoTree(w http.ResponseWriter, r *http.Request, cc *co | ||
| 168 | 168 | "SSHEnabled": h.d.CloneURLs.SSHEnabled, |
| 169 | 169 | "SSHCloneURL": h.cloneSSH(cc.owner, cc.row.Name), |
| 170 | 170 | "RepoTopics": topics, |
| 171 | + "RepoActions": h.repoActions(r, cc.row.ID), | |
| 171 | 172 | "RepoCounts": h.subnavCounts(r.Context(), cc.row.ID, cc.row.ForkCount), |
| 172 | 173 | "CanSettings": h.canViewSettings(middleware.CurrentUserFromContext(r.Context())), |
| 173 | 174 | "ActiveSubnav": "code", |
internal/web/handlers/repo/issues.gomodified@@ -156,6 +156,7 @@ func (h *Handlers) issuesList(w http.ResponseWriter, r *http.Request) { | ||
| 156 | 156 | "Page": page, |
| 157 | 157 | "PerPage": perPage, |
| 158 | 158 | "CSRFToken": middleware.CSRFTokenForRequest(r), |
| 159 | + "RepoActions": h.repoActions(r, row.ID), | |
| 159 | 160 | "RepoCounts": h.subnavCounts(r.Context(), row.ID, row.ForkCount), |
| 160 | 161 | "CanSettings": h.canViewSettings(middleware.CurrentUserFromContext(r.Context())), |
| 161 | 162 | "ActiveSubnav": "issues", |
@@ -176,6 +177,7 @@ func (h *Handlers) issueNewForm(w http.ResponseWriter, r *http.Request) { | ||
| 176 | 177 | "Owner": owner.Username, |
| 177 | 178 | "Repo": row, |
| 178 | 179 | "CSRFToken": middleware.CSRFTokenForRequest(r), |
| 180 | + "RepoActions": h.repoActions(r, row.ID), | |
| 179 | 181 | "RepoCounts": h.subnavCounts(r.Context(), row.ID, row.ForkCount), |
| 180 | 182 | "CanSettings": h.canViewSettings(middleware.CurrentUserFromContext(r.Context())), |
| 181 | 183 | "ActiveSubnav": "issues", |
@@ -238,6 +240,7 @@ func (h *Handlers) renderIssueCreateError(w http.ResponseWriter, r *http.Request | ||
| 238 | 240 | "FormBody": body, |
| 239 | 241 | "Error": msg, |
| 240 | 242 | "CSRFToken": middleware.CSRFTokenForRequest(r), |
| 243 | + "RepoActions": h.repoActions(r, row.ID), | |
| 241 | 244 | "RepoCounts": h.subnavCounts(r.Context(), row.ID, row.ForkCount), |
| 242 | 245 | "CanSettings": h.canViewSettings(middleware.CurrentUserFromContext(r.Context())), |
| 243 | 246 | "ActiveSubnav": "issues", |
@@ -349,6 +352,7 @@ func (h *Handlers) issueView(w http.ResponseWriter, r *http.Request) { | ||
| 349 | 352 | "CanEditIssueMilestone": policy.Can(r.Context(), pdeps, actor, policy.ActionIssueLabel, repoRef).Allow, |
| 350 | 353 | "CanLockIssue": policy.Can(r.Context(), pdeps, actor, policy.ActionIssueClose, repoRef).Allow, |
| 351 | 354 | "CSRFToken": middleware.CSRFTokenForRequest(r), |
| 355 | + "RepoActions": h.repoActions(r, row.ID), | |
| 352 | 356 | "RepoCounts": h.subnavCounts(r.Context(), row.ID, row.ForkCount), |
| 353 | 357 | "CanSettings": h.canViewSettings(viewer), |
| 354 | 358 | "ActiveSubnav": "issues", |
internal/web/handlers/repo/labels_milestones.gomodified@@ -35,6 +35,7 @@ func (h *Handlers) labelsList(w http.ResponseWriter, r *http.Request) { | ||
| 35 | 35 | "Labels": labels, |
| 36 | 36 | "CanManageIssue": canManage, |
| 37 | 37 | "CSRFToken": middleware.CSRFTokenForRequest(r), |
| 38 | + "RepoActions": h.repoActions(r, row.ID), | |
| 38 | 39 | "RepoCounts": h.subnavCounts(r.Context(), row.ID, row.ForkCount), |
| 39 | 40 | "CanSettings": h.canViewSettings(viewer), |
| 40 | 41 | "ActiveSubnav": "issues", |
@@ -137,6 +138,7 @@ func (h *Handlers) milestonesList(w http.ResponseWriter, r *http.Request) { | ||
| 137 | 138 | "Milestones": ms, |
| 138 | 139 | "CanManageIssue": canManage, |
| 139 | 140 | "CSRFToken": middleware.CSRFTokenForRequest(r), |
| 141 | + "RepoActions": h.repoActions(r, row.ID), | |
| 140 | 142 | "RepoCounts": h.subnavCounts(r.Context(), row.ID, row.ForkCount), |
| 141 | 143 | "CanSettings": h.canViewSettings(viewer), |
| 142 | 144 | "ActiveSubnav": "issues", |
internal/web/handlers/repo/repo.gomodified@@ -367,6 +367,7 @@ func (h *Handlers) repoHome(w http.ResponseWriter, r *http.Request) { | ||
| 367 | 367 | "HTTPSCloneURL": h.cloneHTTPS(owner, row.Name), |
| 368 | 368 | "SSHEnabled": h.d.CloneURLs.SSHEnabled, |
| 369 | 369 | "SSHCloneURL": h.cloneSSH(owner, row.Name), |
| 370 | + "RepoActions": h.repoActions(r, row.ID), | |
| 370 | 371 | "RepoCounts": h.subnavCounts(r.Context(), row.ID, row.ForkCount), |
| 371 | 372 | "CanSettings": h.canViewSettings(middleware.CurrentUserFromContext(r.Context())), |
| 372 | 373 | "ActiveSubnav": "code", |
internal/web/handlers/repo/repo_actions.goadded@@ -0,0 +1,108 @@ | ||
| 1 | +// SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | |
| 3 | +package repo | |
| 4 | + | |
| 5 | +import ( | |
| 6 | + "net/http" | |
| 7 | + "net/url" | |
| 8 | + "strings" | |
| 9 | + | |
| 10 | + "github.com/tenseleyFlow/shithub/internal/social" | |
| 11 | + "github.com/tenseleyFlow/shithub/internal/web/middleware" | |
| 12 | +) | |
| 13 | + | |
| 14 | +type repoActionView struct { | |
| 15 | + IsLoggedIn bool | |
| 16 | + LoginURL string | |
| 17 | + ReturnTo string | |
| 18 | + Starred bool | |
| 19 | + WatchLevel string | |
| 20 | + WatchOptions []repoWatchOptionView | |
| 21 | +} | |
| 22 | + | |
| 23 | +type repoWatchOptionView struct { | |
| 24 | + Level string | |
| 25 | + Label string | |
| 26 | + Description string | |
| 27 | + Checked bool | |
| 28 | +} | |
| 29 | + | |
| 30 | +func (h *Handlers) repoActions(r *http.Request, repoID int64) repoActionView { | |
| 31 | + viewer := middleware.CurrentUserFromContext(r.Context()) | |
| 32 | + returnTo := r.URL.RequestURI() | |
| 33 | + if returnTo == "" { | |
| 34 | + returnTo = r.URL.Path | |
| 35 | + } | |
| 36 | + if returnTo == "" { | |
| 37 | + returnTo = "/" | |
| 38 | + } | |
| 39 | + out := repoActionView{ | |
| 40 | + IsLoggedIn: !viewer.IsAnonymous(), | |
| 41 | + LoginURL: "/login?next=" + url.QueryEscape(returnTo), | |
| 42 | + ReturnTo: returnTo, | |
| 43 | + WatchLevel: string(social.WatchParticipating), | |
| 44 | + } | |
| 45 | + if viewer.IsAnonymous() { | |
| 46 | + out.WatchOptions = repoWatchOptions(social.WatchParticipating) | |
| 47 | + return out | |
| 48 | + } | |
| 49 | + deps := h.socialDeps() | |
| 50 | + starred, err := social.HasStar(r.Context(), deps, viewer.ID, repoID) | |
| 51 | + if err != nil { | |
| 52 | + h.d.Logger.WarnContext(r.Context(), "repo actions: star lookup", "error", err, "repo_id", repoID, "user_id", viewer.ID) | |
| 53 | + } else { | |
| 54 | + out.Starred = starred | |
| 55 | + } | |
| 56 | + level, err := social.CurrentLevel(r.Context(), deps, viewer.ID, repoID) | |
| 57 | + if err != nil { | |
| 58 | + h.d.Logger.WarnContext(r.Context(), "repo actions: watch lookup", "error", err, "repo_id", repoID, "user_id", viewer.ID) | |
| 59 | + level = social.WatchParticipating | |
| 60 | + } | |
| 61 | + out.WatchLevel = string(level) | |
| 62 | + out.WatchOptions = repoWatchOptions(level) | |
| 63 | + return out | |
| 64 | +} | |
| 65 | + | |
| 66 | +func repoWatchOptions(current social.WatchLevel) []repoWatchOptionView { | |
| 67 | + return []repoWatchOptionView{ | |
| 68 | + { | |
| 69 | + Level: string(social.WatchParticipating), | |
| 70 | + Label: "Participating and @mentions", | |
| 71 | + Description: "Only receive notifications from this repository when participating or mentioned.", | |
| 72 | + Checked: current == social.WatchParticipating, | |
| 73 | + }, | |
| 74 | + { | |
| 75 | + Level: string(social.WatchAll), | |
| 76 | + Label: "All Activity", | |
| 77 | + Description: "Notified of all notifications on this repository.", | |
| 78 | + Checked: current == social.WatchAll, | |
| 79 | + }, | |
| 80 | + { | |
| 81 | + Level: string(social.WatchIgnore), | |
| 82 | + Label: "Ignore", | |
| 83 | + Description: "Never notified.", | |
| 84 | + Checked: current == social.WatchIgnore, | |
| 85 | + }, | |
| 86 | + } | |
| 87 | +} | |
| 88 | + | |
| 89 | +func redirectAfterRepoAction(w http.ResponseWriter, r *http.Request, fallback string) { | |
| 90 | + dest := fallback | |
| 91 | + if err := r.ParseForm(); err == nil { | |
| 92 | + if returnTo := strings.TrimSpace(r.PostFormValue("return_to")); safeLocalPath(returnTo) { | |
| 93 | + dest = returnTo | |
| 94 | + } | |
| 95 | + } | |
| 96 | + http.Redirect(w, r, dest, http.StatusSeeOther) | |
| 97 | +} | |
| 98 | + | |
| 99 | +func safeLocalPath(path string) bool { | |
| 100 | + if path == "" || !strings.HasPrefix(path, "/") || strings.HasPrefix(path, "//") { | |
| 101 | + return false | |
| 102 | + } | |
| 103 | + u, err := url.Parse(path) | |
| 104 | + if err != nil { | |
| 105 | + return false | |
| 106 | + } | |
| 107 | + return !u.IsAbs() && u.Host == "" && strings.HasPrefix(u.Path, "/") | |
| 108 | +} | |
internal/web/handlers/repo/repo_test.gomodified@@ -149,6 +149,23 @@ func withViewer(req *http.Request, viewer middleware.CurrentUser) *http.Request | ||
| 149 | 149 | return req.WithContext(middleware.WithCurrentUserForTest(req.Context(), viewer)) |
| 150 | 150 | } |
| 151 | 151 | |
| 152 | +func TestSafeLocalPath(t *testing.T) { | |
| 153 | + t.Parallel() | |
| 154 | + tests := map[string]bool{ | |
| 155 | + "/alice/repo": true, | |
| 156 | + "/alice/repo/issues/1?x=1": true, | |
| 157 | + "": false, | |
| 158 | + "alice/repo": false, | |
| 159 | + "//evil.example/path": false, | |
| 160 | + "https://evil.example": false, | |
| 161 | + } | |
| 162 | + for path, want := range tests { | |
| 163 | + if got := safeLocalPath(path); got != want { | |
| 164 | + t.Errorf("safeLocalPath(%q) = %v; want %v", path, got, want) | |
| 165 | + } | |
| 166 | + } | |
| 167 | +} | |
| 168 | + | |
| 152 | 169 | // callLoad invokes loadRepoAndAuthorize via a test handler so we can |
| 153 | 170 | // exercise the chi URL-param plumbing the way the real router does. |
| 154 | 171 | // Returns (status, ok) — `ok` is what loadRepoAndAuthorize returned. |
internal/web/render/octicons.gomodified@@ -70,6 +70,8 @@ func BuiltinOcticons() OcticonResolver { | ||
| 70 | 70 | // S29: notification bell for the top-bar inbox link. |
| 71 | 71 | "bell": trustedSVG(`<svg xmlns="http://www.w3.org/2000/svg" ` + cls + |
| 72 | 72 | `><path d="M8 16a2 2 0 0 0 1.985-1.75c.017-.137-.097-.25-.235-.25h-3.5c-.138 0-.252.113-.235.25A2 2 0 0 0 8 16zM3 5a5 5 0 0 1 10 0v2.947c0 .05.015.098.042.139l1.703 2.555A1.519 1.519 0 0 1 13.482 13H2.518a1.519 1.519 0 0 1-1.263-2.36l1.703-2.554A.255.255 0 0 0 3 7.947zm5-3.5A3.5 3.5 0 0 0 4.5 5v2.947c0 .346-.102.683-.294.97l-1.703 2.556a.018.018 0 0 0-.003.01.017.017 0 0 0 .002.012.017.017 0 0 0 .015.005h10.964a.017.017 0 0 0 .016-.005.018.018 0 0 0 0-.022l-1.703-2.555a1.749 1.749 0 0 1-.294-.97V5A3.5 3.5 0 0 0 8 1.5z"/></svg>`), |
| 73 | + "triangle-down": trustedSVG(`<svg xmlns="http://www.w3.org/2000/svg" ` + cls + | |
| 74 | + `><path d="m4.427 7.427 3.396 3.396a.25.25 0 0 0 .354 0l3.396-3.396A.25.25 0 0 0 11.396 7H4.604a.25.25 0 0 0-.177.427Z"/></svg>`), | |
| 73 | 75 | // Repo "Code" dropdown chrome (clone widget). |
| 74 | 76 | "download": trustedSVG(`<svg xmlns="http://www.w3.org/2000/svg" ` + cls + |
| 75 | 77 | `><path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14zm5.78-2.92a.749.749 0 0 1-1.06 0L3.72 7.78a.749.749 0 1 1 1.06-1.06L7.25 9.19V1.75a.75.75 0 0 1 1.5 0v7.44l2.47-2.47a.749.749 0 1 1 1.06 1.06z"/></svg>`), |
internal/web/static/css/shithub.cssmodified@@ -1125,23 +1125,109 @@ code { | ||
| 1125 | 1125 | .shithub-repo-page-title .shithub-repo-name { font-weight: 600; } |
| 1126 | 1126 | .shithub-repo-title-icon { color: var(--fg-muted); display: inline-flex; align-items: center; } |
| 1127 | 1127 | .shithub-repo-actions { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; } |
| 1128 | +.shithub-repo-action-form { margin: 0; } | |
| 1128 | 1129 | .shithub-repo-action { |
| 1129 | 1130 | display: inline-flex; |
| 1130 | 1131 | align-items: center; |
| 1131 | 1132 | gap: 0.35rem; |
| 1132 | - padding: 0.35rem 0.7rem; | |
| 1133 | + min-height: 32px; | |
| 1134 | + padding: 0 0.7rem; | |
| 1133 | 1135 | border: 1px solid var(--border-default); |
| 1134 | 1136 | border-radius: 6px; |
| 1135 | 1137 | color: var(--fg-default); |
| 1136 | 1138 | background: var(--canvas-subtle); |
| 1137 | 1139 | font-size: 0.875rem; |
| 1138 | 1140 | font-weight: 600; |
| 1141 | + line-height: 20px; | |
| 1142 | + white-space: nowrap; | |
| 1143 | + cursor: pointer; | |
| 1144 | +} | |
| 1145 | +.shithub-repo-action:hover { text-decoration: none; background: var(--canvas-inset); } | |
| 1146 | +button.shithub-repo-action { | |
| 1147 | + font-family: inherit; | |
| 1148 | +} | |
| 1149 | +.shithub-repo-action.is-active { | |
| 1150 | + color: var(--accent-fg); | |
| 1139 | 1151 | } |
| 1140 | -.shithub-repo-action span { | |
| 1152 | +.shithub-repo-action-label { color: inherit; } | |
| 1153 | +.shithub-repo-action .shithub-counter { | |
| 1141 | 1154 | color: var(--fg-muted); |
| 1142 | 1155 | font-weight: 600; |
| 1143 | - padding-left: 0.35rem; | |
| 1144 | - border-left: 1px solid var(--border-default); | |
| 1156 | + font-size: 0.75rem; | |
| 1157 | + padding: 0 0.35rem; | |
| 1158 | + min-width: 1.25rem; | |
| 1159 | + border-radius: 999px; | |
| 1160 | + background: rgba(110, 118, 129, 0.18); | |
| 1161 | +} | |
| 1162 | +.shithub-repo-action-menu { position: relative; } | |
| 1163 | +.shithub-repo-action-menu > summary { list-style: none; } | |
| 1164 | +.shithub-repo-action-menu > summary::-webkit-details-marker { display: none; } | |
| 1165 | +.shithub-repo-action-button svg:last-child { | |
| 1166 | + width: 12px; | |
| 1167 | + height: 12px; | |
| 1168 | + color: var(--fg-muted); | |
| 1169 | +} | |
| 1170 | +.shithub-repo-action-popover { | |
| 1171 | + position: absolute; | |
| 1172 | + right: 0; | |
| 1173 | + top: calc(100% + 0.4rem); | |
| 1174 | + z-index: 20; | |
| 1175 | + width: min(340px, calc(100vw - 2rem)); | |
| 1176 | + padding: 0.5rem 0; | |
| 1177 | + border: 1px solid var(--border-default); | |
| 1178 | + border-radius: 8px; | |
| 1179 | + background: var(--canvas-default); | |
| 1180 | + box-shadow: 0 16px 32px rgba(1, 4, 9, 0.2); | |
| 1181 | +} | |
| 1182 | +.shithub-repo-action-popover strong { | |
| 1183 | + display: block; | |
| 1184 | + padding: 0.45rem 0.85rem 0.55rem; | |
| 1185 | + border-bottom: 1px solid var(--border-muted); | |
| 1186 | +} | |
| 1187 | +.shithub-repo-action-option-form { margin: 0; } | |
| 1188 | +.shithub-repo-action-option { | |
| 1189 | + width: 100%; | |
| 1190 | + display: grid; | |
| 1191 | + grid-template-columns: 16px minmax(0, 1fr); | |
| 1192 | + gap: 0.6rem; | |
| 1193 | + padding: 0.65rem 0.85rem; | |
| 1194 | + border: 0; | |
| 1195 | + border-bottom: 1px solid var(--border-muted); | |
| 1196 | + background: transparent; | |
| 1197 | + color: var(--fg-default); | |
| 1198 | + font: inherit; | |
| 1199 | + text-align: left; | |
| 1200 | + cursor: pointer; | |
| 1201 | +} | |
| 1202 | +.shithub-repo-action-option:hover { background: var(--canvas-subtle); } | |
| 1203 | +.shithub-repo-action-radio { | |
| 1204 | + width: 14px; | |
| 1205 | + height: 14px; | |
| 1206 | + margin-top: 0.25rem; | |
| 1207 | + border: 1px solid var(--border-default); | |
| 1208 | + border-radius: 50%; | |
| 1209 | +} | |
| 1210 | +.shithub-repo-action-option.is-selected .shithub-repo-action-radio { | |
| 1211 | + border-color: var(--accent-emphasis); | |
| 1212 | + box-shadow: inset 0 0 0 3px var(--canvas-default); | |
| 1213 | + background: var(--accent-emphasis); | |
| 1214 | +} | |
| 1215 | +.shithub-repo-action-option-title { | |
| 1216 | + display: block; | |
| 1217 | + font-weight: 600; | |
| 1218 | +} | |
| 1219 | +.shithub-repo-action-option-description { | |
| 1220 | + display: block; | |
| 1221 | + margin-top: 0.15rem; | |
| 1222 | + color: var(--fg-muted); | |
| 1223 | + font-size: 0.78rem; | |
| 1224 | + line-height: 1.35; | |
| 1225 | +} | |
| 1226 | +.shithub-repo-action-popover-link { | |
| 1227 | + display: block; | |
| 1228 | + padding: 0.55rem 0.85rem 0.35rem; | |
| 1229 | + color: var(--fg-muted); | |
| 1230 | + font-size: 0.82rem; | |
| 1145 | 1231 | } |
| 1146 | 1232 | .shithub-repo-subnav { |
| 1147 | 1233 | display: flex; |
internal/web/templates/_repo_header.htmlmodified@@ -8,10 +8,73 @@ | ||
| 8 | 8 | <a href="/{{ .Owner }}/{{ .Repo.Name }}" class="shithub-repo-name">{{ .Repo.Name }}</a> |
| 9 | 9 | {{ if eq (printf "%s" .Repo.Visibility) "private" }}<span class="shithub-pill shithub-pill-private">Private</span>{{ else }}<span class="shithub-pill">Public</span>{{ end }} |
| 10 | 10 | </h1> |
| 11 | + {{ $root := . }} | |
| 11 | 12 | <div class="shithub-repo-actions" aria-label="Repository actions"> |
| 12 | - <a href="/{{ .Owner }}/{{ .Repo.Name }}/watchers" class="shithub-repo-action">{{ octicon "eye" }} Watch <span>{{ .Repo.WatcherCount }}</span></a> | |
| 13 | - <a href="/{{ .Owner }}/{{ .Repo.Name }}/forks" class="shithub-repo-action">{{ octicon "repo-forked" }} Fork <span>{{ .Repo.ForkCount }}</span></a> | |
| 14 | - <a href="/{{ .Owner }}/{{ .Repo.Name }}/stargazers" class="shithub-repo-action">{{ octicon "star" }} Star <span>{{ .Repo.StarCount }}</span></a> | |
| 13 | + {{ with .RepoActions }} | |
| 14 | + {{ if .IsLoggedIn }} | |
| 15 | + <details class="shithub-repo-action-menu"> | |
| 16 | + <summary class="shithub-repo-action shithub-repo-action-button" aria-label="Notification settings"> | |
| 17 | + {{ octicon "bell" }} | |
| 18 | + <span class="shithub-repo-action-label">Notifications</span> | |
| 19 | + {{ octicon "triangle-down" }} | |
| 20 | + </summary> | |
| 21 | + <div class="shithub-repo-action-popover" role="menu" aria-label="Notification settings"> | |
| 22 | + <strong>Notifications</strong> | |
| 23 | + {{ range .WatchOptions }} | |
| 24 | + <form method="post" action="/{{ $root.Owner }}/{{ $root.Repo.Name }}/watch" class="shithub-repo-action-option-form"> | |
| 25 | + <input type="hidden" name="csrf_token" value="{{ $root.CSRFToken }}"> | |
| 26 | + <input type="hidden" name="level" value="{{ .Level }}"> | |
| 27 | + <input type="hidden" name="return_to" value="{{ $root.RepoActions.ReturnTo }}"> | |
| 28 | + <button type="submit" class="shithub-repo-action-option{{ if .Checked }} is-selected{{ end }}" role="menuitemradio" aria-checked="{{ .Checked }}"> | |
| 29 | + <span class="shithub-repo-action-radio" aria-hidden="true"></span> | |
| 30 | + <span> | |
| 31 | + <span class="shithub-repo-action-option-title">{{ .Label }}</span> | |
| 32 | + <span class="shithub-repo-action-option-description">{{ .Description }}</span> | |
| 33 | + </span> | |
| 34 | + </button> | |
| 35 | + </form> | |
| 36 | + {{ end }} | |
| 37 | + <a href="/{{ $root.Owner }}/{{ $root.Repo.Name }}/watchers" class="shithub-repo-action-popover-link">{{ $root.Repo.WatcherCount }} watcher{{ if ne $root.Repo.WatcherCount 1 }}s{{ end }}</a> | |
| 38 | + </div> | |
| 39 | + </details> | |
| 40 | + <form method="post" action="/{{ $root.Owner }}/{{ $root.Repo.Name }}/fork" class="shithub-repo-action-form"> | |
| 41 | + <input type="hidden" name="csrf_token" value="{{ $root.CSRFToken }}"> | |
| 42 | + <button type="submit" class="shithub-repo-action"> | |
| 43 | + {{ octicon "repo-forked" }} | |
| 44 | + <span class="shithub-repo-action-label">Fork</span> | |
| 45 | + <span class="shithub-counter">{{ $root.Repo.ForkCount }}</span> | |
| 46 | + </button> | |
| 47 | + </form> | |
| 48 | + <form method="post" action="/{{ $root.Owner }}/{{ $root.Repo.Name }}/{{ if .Starred }}unstar{{ else }}star{{ end }}" class="shithub-repo-action-form"> | |
| 49 | + <input type="hidden" name="csrf_token" value="{{ $root.CSRFToken }}"> | |
| 50 | + <input type="hidden" name="return_to" value="{{ .ReturnTo }}"> | |
| 51 | + <button type="submit" class="shithub-repo-action{{ if .Starred }} is-active{{ end }}" aria-pressed="{{ .Starred }}"> | |
| 52 | + {{ octicon "star" }} | |
| 53 | + <span class="shithub-repo-action-label">{{ if .Starred }}Starred{{ else }}Star{{ end }}</span> | |
| 54 | + <span class="shithub-counter">{{ $root.Repo.StarCount }}</span> | |
| 55 | + </button> | |
| 56 | + </form> | |
| 57 | + {{ else }} | |
| 58 | + <a href="{{ .LoginURL }}" class="shithub-repo-action" aria-label="You must be signed in to change notification settings"> | |
| 59 | + {{ octicon "bell" }} | |
| 60 | + <span class="shithub-repo-action-label">Notifications</span> | |
| 61 | + </a> | |
| 62 | + <a href="{{ .LoginURL }}" class="shithub-repo-action" aria-label="You must be signed in to fork a repository"> | |
| 63 | + {{ octicon "repo-forked" }} | |
| 64 | + <span class="shithub-repo-action-label">Fork</span> | |
| 65 | + <span class="shithub-counter">{{ $root.Repo.ForkCount }}</span> | |
| 66 | + </a> | |
| 67 | + <a href="{{ .LoginURL }}" class="shithub-repo-action" aria-label="You must be signed in to star a repository"> | |
| 68 | + {{ octicon "star" }} | |
| 69 | + <span class="shithub-repo-action-label">Star</span> | |
| 70 | + <span class="shithub-counter">{{ $root.Repo.StarCount }}</span> | |
| 71 | + </a> | |
| 72 | + {{ end }} | |
| 73 | + {{ else }} | |
| 74 | + <a href="/{{ .Owner }}/{{ .Repo.Name }}/watchers" class="shithub-repo-action">{{ octicon "eye" }} Watch <span class="shithub-counter">{{ .Repo.WatcherCount }}</span></a> | |
| 75 | + <a href="/{{ .Owner }}/{{ .Repo.Name }}/forks" class="shithub-repo-action">{{ octicon "repo-forked" }} Fork <span class="shithub-counter">{{ .Repo.ForkCount }}</span></a> | |
| 76 | + <a href="/{{ .Owner }}/{{ .Repo.Name }}/stargazers" class="shithub-repo-action">{{ octicon "star" }} Star <span class="shithub-counter">{{ .Repo.StarCount }}</span></a> | |
| 77 | + {{ end }} | |
| 15 | 78 | </div> |
| 16 | 79 | </div> |
| 17 | 80 | {{ template "repo-subnav" . }} |