@@ -41,6 +41,7 @@ pub struct Context {
4141mut:
4242 user User
4343 current_path string
44+ page_title string
4445 page_gen_time string
4546 page_gen_start i64
4647 is_tree bool
@@ -147,6 +148,7 @@ pub fn (mut app App) init_server() {
147148
148149pub fn (mut app App) before_request(mut ctx Context) bool {
149150 ctx.page_gen_start = time.ticks()
151+ ctx.set_page_title_from_path(ctx.req.url)
150152 $if trace_prealloc ? {
151153 unsafe { prealloc_scope_checkpoint(c'gitly before_request start') }
152154 }
@@ -225,6 +227,93 @@ pub fn (mut ctx Context) redirect_to_repository(username string, repo_name strin
225227 return ctx.redirect('/${username}/${repo_name}')
226228}
227229
230+fn (mut ctx Context) set_page_title(parts []string) {
231+ mut clean_parts := []string{}
232+ for part in parts {
233+ clean := part.replace('\r', ' ').replace('\n', ' ').trim_space()
234+ if clean != '' {
235+ clean_parts << clean
236+ }
237+ }
238+ clean_parts << 'Gitly'
239+ ctx.page_title = clean_parts.join(' - ')
240+}
241+
242+fn (mut ctx Context) set_page_title_from_path(raw_url string) {
243+ path := raw_url.all_before('?').trim('/')
244+ if path == '' {
245+ ctx.set_page_title([]string{})
246+ return
247+ }
248+
249+ segments := path.split('/')
250+ if segments.len >= 2 && segments[0] == 'api' {
251+ ctx.set_page_title([]string{})
252+ return
253+ }
254+ if segments.len >= 4 && segments[2] in ['issue', 'pull'] {
255+ ctx.set_page_title(['${page_title_label(segments[2])} #${segments[3]}',
256+ '${segments[0]}/${segments[1]}'])
257+ return
258+ }
259+ if segments.len >= 3 {
260+ ctx.set_page_title([page_title_label(segments[2]), '${segments[0]}/${segments[1]}'])
261+ return
262+ }
263+ if segments.len == 2 {
264+ if is_user_page_segment(segments[1]) {
265+ ctx.set_page_title([page_title_label(segments[1]), segments[0]])
266+ } else {
267+ ctx.set_page_title(['${segments[0]}/${segments[1]}'])
268+ }
269+ return
270+ }
271+ ctx.set_page_title([page_title_label(segments[0])])
272+}
273+
274+fn is_user_page_segment(slug string) bool {
275+ return slug in ['feed', 'issues', 'pulls', 'repos', 'settings', 'stars']
276+}
277+
278+fn page_title_label(slug string) string {
279+ return match slug {
280+ '2fa' { 'Two-factor authentication' }
281+ 'api-tokens' { 'API tokens' }
282+ 'blob' { 'File' }
283+ 'branches' { 'Branches' }
284+ 'ci' { 'CI' }
285+ 'commit' { 'Commit' }
286+ 'compare' { 'New pull request' }
287+ 'contributors' { 'Contributors' }
288+ 'discussions' { 'Discussions' }
289+ 'edit' { 'Edit file' }
290+ 'feed' { 'Feed' }
291+ 'issue' { 'Issue' }
292+ 'issues' { 'Issues' }
293+ 'login' { 'Login' }
294+ 'milestones' { 'Milestones' }
295+ 'new' { 'New repository' }
296+ 'open-source' { 'Open source' }
297+ 'organizations' { 'Organizations' }
298+ 'pricing' { 'Pricing' }
299+ 'projects' { 'Projects' }
300+ 'pull' { 'Pull request' }
301+ 'pulls' { 'Pull requests' }
302+ 'register' { 'Register' }
303+ 'releases' { 'Releases' }
304+ 'repos' { 'Repositories' }
305+ 'search' { 'Search' }
306+ 'security' { 'Security' }
307+ 'settings' { 'Settings' }
308+ 'ssh-keys' { 'SSH keys' }
309+ 'stars' { 'Stars' }
310+ 'tag' { 'Tag' }
311+ 'tree' { 'Code' }
312+ 'webhooks' { 'Webhooks' }
313+ else { slug.replace('-', ' ').replace('_', ' ') }
314+ }
315+}
316+
228317fn (mut app App) create_tables() ! {
229318 sql app.db {
230319 create table Repo
@@ -29,11 +29,12 @@ fn (mut app App) handle_issues_count(username string, repo_name string) veb.Resu
2929}
3030
3131@['/:username/:repo_name/issues/new']
32-pub fn (mut app App) new_issue(username string, repo_name string) veb.Result {
32+pub fn (mut app App) new_issue(mut ctx Context, username string, repo_name string) veb.Result {
3333 if !ctx.logged_in {
3434 return ctx.not_found()
3535 }
3636 repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() }
37+ ctx.set_page_title(['New issue', '${repo.user_name}/${repo.name}'])
3738 return $veb.html()
3839}
3940
@@ -116,6 +117,7 @@ pub fn (mut app App) issues(mut ctx Context, username string, repo_name string,
116117 }
117118 page_count := calculate_pages(repo.nr_open_issues, commits_per_page)
118119 prev_page, next_page := generate_prev_next_pages(page_i)
120+ ctx.set_page_title(['Issues', '${repo.user_name}/${repo.name}'])
119121 return $veb.html()
120122}
121123
@@ -124,6 +126,7 @@ pub fn (mut app App) issue(mut ctx Context, username string, repo_name string, i
124126 repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() }
125127 issue := app.find_issue_by_id(id.int()) or { return ctx.not_found() }
126128 issue_author := app.get_user_by_id(issue.author_id) or { return ctx.not_found() }
129+ ctx.set_page_title(['${issue.title} #${issue.id}', '${repo.user_name}/${repo.name}'])
127130 mut comments_with_users := []CommentWithUser{}
128131 mut comment := Comment{}
129132 mut comment_author := User{}
@@ -200,5 +203,13 @@ pub fn (mut app App) user_issues(mut ctx Context, username string, tab string) v
200203 'user-issues-sidebar__item'
201204 }
202205 show_repo_link := true
206+ tab_title := match current_tab {
207+ 'assigned' { 'Assigned issues' }
208+ 'mentioned' { 'Mentioned issues' }
209+ 'activity' { 'Issue activity' }
210+ else { 'Created issues' }
211+ }
212+
213+ ctx.set_page_title([tab_title, user.username])
203214 return $veb.html()
204215}
@@ -191,6 +191,13 @@ pub fn (mut app App) repo_pulls(mut ctx Context, username string, repo_name stri
191191 tab_open_class := if current_tab == 'open' { 'pr-tab pr-tab--active' } else { 'pr-tab' }
192192 tab_merged_class := if current_tab == 'merged' { 'pr-tab pr-tab--active' } else { 'pr-tab' }
193193 tab_closed_class := if current_tab == 'closed' { 'pr-tab pr-tab--active' } else { 'pr-tab' }
194+ tab_title := match current_tab {
195+ 'closed' { 'Closed pull requests' }
196+ 'merged' { 'Merged pull requests' }
197+ else { 'Open pull requests' }
198+ }
199+
200+ ctx.set_page_title([tab_title, '${repo.user_name}/${repo.name}'])
194201 return $veb.html('templates/pulls.html')
195202}
196203
@@ -226,6 +233,7 @@ pub fn (mut app App) new_pull_request_form(mut ctx Context, username string, rep
226233 }
227234 }
228235 }
236+ ctx.set_page_title(['New pull request', '${repo.user_name}/${repo.name}'])
229237 return $veb.html('templates/new/pull.html')
230238}
231239
@@ -329,6 +337,7 @@ pub fn (mut app App) pull_request(mut ctx Context, username string, repo_name st
329337 can_merge := is_repo_owner && pr.is_open()
330338 can_close := pr.is_open() && (is_repo_owner || pr.author_id == ctx.user.id)
331339 can_reopen := pr.is_closed() && (is_repo_owner || pr.author_id == ctx.user.id)
340+ ctx.set_page_title(['${pr.title} #${pr.id}', '${repo.user_name}/${repo.name}'])
332341 return $veb.html('templates/pull.html')
333342}
334343
@@ -365,6 +374,7 @@ pub fn (mut app App) pull_request_files(mut ctx Context, username string, repo_n
365374 }
366375 can_comment := ctx.logged_in && pr.is_open()
367376 line_comment_placeholder := veb.tr(ctx.lang.str(), 'pr_line_comment_placeholder').trim_space()
377+ ctx.set_page_title(['${pr.title} #${pr.id}', 'Files changed', '${repo.user_name}/${repo.name}'])
368378 return $veb.html('templates/pull_files.html')
369379}
370380
@@ -614,6 +624,7 @@ pub fn (mut app App) handle_get_user_pulls(mut ctx Context, username string) veb
614624 pr.repo_name = r.name
615625 prs_with_repo << pr
616626 }
627+ ctx.set_page_title(['Pull requests', user.username])
617628 return $veb.html('templates/user_pulls.html')
618629}
619630
@@ -352,6 +352,21 @@ fn (r &Repo) last_updated_str() string {
352352 return time.unix(r.latest_commit_at).relative()
353353}
354354
355+fn (r &Repo) created_str() string {
356+ if r.created_at <= 0 {
357+ return ''
358+ }
359+ return time.unix(r.created_at).relative()
360+}
361+
362+fn (r &Repo) last_activity_str() string {
363+ activity_at := if r.latest_commit_at > 0 { r.latest_commit_at } else { r.created_at }
364+ if activity_at <= 0 {
365+ return ''
366+ }
367+ return time.unix(activity_at).relative()
368+}
369+
355370fn (mut app App) delete_repository(id int, path string, name string) ! {
356371 sql app.db {
357372 update Repo set is_deleted = true where id == id
@@ -1 +1 @@
1-3e6e162
1+63435c2
@@ -748,10 +748,19 @@ form {
748748}
749749
750750.repo-card__header {
751+ display: flex;
752+ align-items: flex-start;
753+ justify-content: space-between;
754+ gap: 12px;
755+ margin-bottom: 4px;
756+}
757+
758+.repo-card__heading {
751759 display: flex;
752760 align-items: center;
753761 gap: 10px;
754- margin-bottom: 4px;
762+ flex-wrap: wrap;
763+ min-width: 0;
755764}
756765
757766.repo-card__title {
@@ -764,6 +773,18 @@ form {
764773 }
765774}
766775
776+.repo-card__dates {
777+ display: flex;
778+ flex-direction: column;
779+ align-items: flex-end;
780+ gap: 3px;
781+ flex-shrink: 0;
782+ color: $gray-dark;
783+ font-size: 12px;
784+ line-height: 1.35;
785+ text-align: right;
786+}
787+
767788.repo-card__footer {
768789 display: flex;
769790 align-items: center;
@@ -839,6 +860,18 @@ form {
839860 color: $gray-dark;
840861}
841862
863+@media (max-width: 700px) {
864+ .repo-card__header {
865+ flex-direction: column;
866+ align-items: flex-start;
867+ }
868+
869+ .repo-card__dates {
870+ align-items: flex-start;
871+ text-align: left;
872+ }
873+}
874+
842875.repos-empty {
843876 color: $gray-dark;
844877 margin: 30px 0;
@@ -1,5 +1,5 @@
11<meta charset='utf-8'>
2-<title>Gitly</title>
2+<title>@ctx.page_title</title>
33<meta name="description" content="Open-source development platform written in V">
44<meta name="viewport" content="width=device-width, initial-scale=1">
55<link rel='icon' type='image/svg+xml' href='/assets/favicon.svg' sizes='any'>
@@ -9,4 +9,3 @@
99
1010<script src="/js/gitly.js?v=@{app.started_at}"></script>
1111
12-
@@ -19,11 +19,21 @@
1919 @for repo in repos
2020 <div class="repo-card">
2121 <div class="repo-card__header">
22- <a class="repo-card__title" href="/@user.username/@repo.name">@repo.name</a>
23- @if repo.is_public
24- <span class="repo-card__badge">Public</span>
25- @else
26- <span class="repo-card__badge repo-card__badge--private">Private</span>
22+ <div class="repo-card__heading">
23+ <a class="repo-card__title" href="/@user.username/@repo.name">@repo.name</a>
24+ @if repo.is_public
25+ <span class="repo-card__badge">Public</span>
26+ @else
27+ <span class="repo-card__badge repo-card__badge--private">Private</span>
28+ @end
29+ </div>
30+ @if repo.latest_commit_at > 0 || repo.created_at > 0
31+ <div class="repo-card__dates">
32+ @if repo.created_at > 0
33+ <span>Created @{repo.created_str()}</span>
34+ @end
35+ <span>Last activity @{repo.last_activity_str()}</span>
36+ </div>
2737 @end
2838 </div>
2939 @if repo.description.len > 0
@@ -37,9 +47,6 @@
3747 @repo.lang_stats[0].name
3848 </span>
3949 @end
40- @if repo.latest_commit_at > 0
41- <span class="repo-card__updated">Updated @{repo.last_updated_str()}</span>
42- @end
4350 </div>
4451 @if repo.activity_buckets.len > 0
4552 <svg class="repo-card__activity" viewBox="0 0 120 28" preserveAspectRatio="none">