From 03ce80574e9d3cb5892de161e92055e76b30af1c Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Mon, 29 Jun 2026 15:18:25 +0300 Subject: [PATCH] unique page titles --- gitly.v | 89 ++++++++++++++++++++++++++++++++++++++ issue_routes.v | 13 +++++- pr_routes.v | 11 +++++ repo/repo.v | 15 +++++++ static/assets/version | 2 +- static/css/gitly.scss | 35 ++++++++++++++- templates/layout/head.html | 3 +- templates/user/repos.html | 23 ++++++---- 8 files changed, 178 insertions(+), 13 deletions(-) diff --git a/gitly.v b/gitly.v index 8e8144e..d664635 100644 --- a/gitly.v +++ b/gitly.v @@ -41,6 +41,7 @@ pub struct Context { mut: user User current_path string + page_title string page_gen_time string page_gen_start i64 is_tree bool @@ -147,6 +148,7 @@ pub fn (mut app App) init_server() { pub fn (mut app App) before_request(mut ctx Context) bool { ctx.page_gen_start = time.ticks() + ctx.set_page_title_from_path(ctx.req.url) $if trace_prealloc ? { unsafe { prealloc_scope_checkpoint(c'gitly before_request start') } } @@ -225,6 +227,93 @@ pub fn (mut ctx Context) redirect_to_repository(username string, repo_name strin return ctx.redirect('/${username}/${repo_name}') } +fn (mut ctx Context) set_page_title(parts []string) { + mut clean_parts := []string{} + for part in parts { + clean := part.replace('\r', ' ').replace('\n', ' ').trim_space() + if clean != '' { + clean_parts << clean + } + } + clean_parts << 'Gitly' + ctx.page_title = clean_parts.join(' - ') +} + +fn (mut ctx Context) set_page_title_from_path(raw_url string) { + path := raw_url.all_before('?').trim('/') + if path == '' { + ctx.set_page_title([]string{}) + return + } + + segments := path.split('/') + if segments.len >= 2 && segments[0] == 'api' { + ctx.set_page_title([]string{}) + return + } + if segments.len >= 4 && segments[2] in ['issue', 'pull'] { + ctx.set_page_title(['${page_title_label(segments[2])} #${segments[3]}', + '${segments[0]}/${segments[1]}']) + return + } + if segments.len >= 3 { + ctx.set_page_title([page_title_label(segments[2]), '${segments[0]}/${segments[1]}']) + return + } + if segments.len == 2 { + if is_user_page_segment(segments[1]) { + ctx.set_page_title([page_title_label(segments[1]), segments[0]]) + } else { + ctx.set_page_title(['${segments[0]}/${segments[1]}']) + } + return + } + ctx.set_page_title([page_title_label(segments[0])]) +} + +fn is_user_page_segment(slug string) bool { + return slug in ['feed', 'issues', 'pulls', 'repos', 'settings', 'stars'] +} + +fn page_title_label(slug string) string { + return match slug { + '2fa' { 'Two-factor authentication' } + 'api-tokens' { 'API tokens' } + 'blob' { 'File' } + 'branches' { 'Branches' } + 'ci' { 'CI' } + 'commit' { 'Commit' } + 'compare' { 'New pull request' } + 'contributors' { 'Contributors' } + 'discussions' { 'Discussions' } + 'edit' { 'Edit file' } + 'feed' { 'Feed' } + 'issue' { 'Issue' } + 'issues' { 'Issues' } + 'login' { 'Login' } + 'milestones' { 'Milestones' } + 'new' { 'New repository' } + 'open-source' { 'Open source' } + 'organizations' { 'Organizations' } + 'pricing' { 'Pricing' } + 'projects' { 'Projects' } + 'pull' { 'Pull request' } + 'pulls' { 'Pull requests' } + 'register' { 'Register' } + 'releases' { 'Releases' } + 'repos' { 'Repositories' } + 'search' { 'Search' } + 'security' { 'Security' } + 'settings' { 'Settings' } + 'ssh-keys' { 'SSH keys' } + 'stars' { 'Stars' } + 'tag' { 'Tag' } + 'tree' { 'Code' } + 'webhooks' { 'Webhooks' } + else { slug.replace('-', ' ').replace('_', ' ') } + } +} + fn (mut app App) create_tables() ! { sql app.db { create table Repo diff --git a/issue_routes.v b/issue_routes.v index ed19676..46bab27 100644 --- a/issue_routes.v +++ b/issue_routes.v @@ -29,11 +29,12 @@ fn (mut app App) handle_issues_count(username string, repo_name string) veb.Resu } @['/:username/:repo_name/issues/new'] -pub fn (mut app App) new_issue(username string, repo_name string) veb.Result { +pub fn (mut app App) new_issue(mut ctx Context, username string, repo_name string) veb.Result { if !ctx.logged_in { return ctx.not_found() } repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() } + ctx.set_page_title(['New issue', '${repo.user_name}/${repo.name}']) return $veb.html() } @@ -116,6 +117,7 @@ pub fn (mut app App) issues(mut ctx Context, username string, repo_name string, } page_count := calculate_pages(repo.nr_open_issues, commits_per_page) prev_page, next_page := generate_prev_next_pages(page_i) + ctx.set_page_title(['Issues', '${repo.user_name}/${repo.name}']) return $veb.html() } @@ -124,6 +126,7 @@ pub fn (mut app App) issue(mut ctx Context, username string, repo_name string, i repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() } issue := app.find_issue_by_id(id.int()) or { return ctx.not_found() } issue_author := app.get_user_by_id(issue.author_id) or { return ctx.not_found() } + ctx.set_page_title(['${issue.title} #${issue.id}', '${repo.user_name}/${repo.name}']) mut comments_with_users := []CommentWithUser{} mut comment := Comment{} mut comment_author := User{} @@ -200,5 +203,13 @@ pub fn (mut app App) user_issues(mut ctx Context, username string, tab string) v 'user-issues-sidebar__item' } show_repo_link := true + tab_title := match current_tab { + 'assigned' { 'Assigned issues' } + 'mentioned' { 'Mentioned issues' } + 'activity' { 'Issue activity' } + else { 'Created issues' } + } + + ctx.set_page_title([tab_title, user.username]) return $veb.html() } diff --git a/pr_routes.v b/pr_routes.v index ae6b2d6..3346012 100644 --- a/pr_routes.v +++ b/pr_routes.v @@ -191,6 +191,13 @@ pub fn (mut app App) repo_pulls(mut ctx Context, username string, repo_name stri tab_open_class := if current_tab == 'open' { 'pr-tab pr-tab--active' } else { 'pr-tab' } tab_merged_class := if current_tab == 'merged' { 'pr-tab pr-tab--active' } else { 'pr-tab' } tab_closed_class := if current_tab == 'closed' { 'pr-tab pr-tab--active' } else { 'pr-tab' } + tab_title := match current_tab { + 'closed' { 'Closed pull requests' } + 'merged' { 'Merged pull requests' } + else { 'Open pull requests' } + } + + ctx.set_page_title([tab_title, '${repo.user_name}/${repo.name}']) return $veb.html('templates/pulls.html') } @@ -226,6 +233,7 @@ pub fn (mut app App) new_pull_request_form(mut ctx Context, username string, rep } } } + ctx.set_page_title(['New pull request', '${repo.user_name}/${repo.name}']) return $veb.html('templates/new/pull.html') } @@ -329,6 +337,7 @@ pub fn (mut app App) pull_request(mut ctx Context, username string, repo_name st can_merge := is_repo_owner && pr.is_open() can_close := pr.is_open() && (is_repo_owner || pr.author_id == ctx.user.id) can_reopen := pr.is_closed() && (is_repo_owner || pr.author_id == ctx.user.id) + ctx.set_page_title(['${pr.title} #${pr.id}', '${repo.user_name}/${repo.name}']) return $veb.html('templates/pull.html') } @@ -365,6 +374,7 @@ pub fn (mut app App) pull_request_files(mut ctx Context, username string, repo_n } can_comment := ctx.logged_in && pr.is_open() line_comment_placeholder := veb.tr(ctx.lang.str(), 'pr_line_comment_placeholder').trim_space() + ctx.set_page_title(['${pr.title} #${pr.id}', 'Files changed', '${repo.user_name}/${repo.name}']) return $veb.html('templates/pull_files.html') } @@ -614,6 +624,7 @@ pub fn (mut app App) handle_get_user_pulls(mut ctx Context, username string) veb pr.repo_name = r.name prs_with_repo << pr } + ctx.set_page_title(['Pull requests', user.username]) return $veb.html('templates/user_pulls.html') } diff --git a/repo/repo.v b/repo/repo.v index 9bfd103..2db4077 100644 --- a/repo/repo.v +++ b/repo/repo.v @@ -352,6 +352,21 @@ fn (r &Repo) last_updated_str() string { return time.unix(r.latest_commit_at).relative() } +fn (r &Repo) created_str() string { + if r.created_at <= 0 { + return '' + } + return time.unix(r.created_at).relative() +} + +fn (r &Repo) last_activity_str() string { + activity_at := if r.latest_commit_at > 0 { r.latest_commit_at } else { r.created_at } + if activity_at <= 0 { + return '' + } + return time.unix(activity_at).relative() +} + fn (mut app App) delete_repository(id int, path string, name string) ! { sql app.db { update Repo set is_deleted = true where id == id diff --git a/static/assets/version b/static/assets/version index 469a03c..56dd9e8 100644 --- a/static/assets/version +++ b/static/assets/version @@ -1 +1 @@ -3e6e162 \ No newline at end of file +63435c2 \ No newline at end of file diff --git a/static/css/gitly.scss b/static/css/gitly.scss index e53b0b3..351b65a 100644 --- a/static/css/gitly.scss +++ b/static/css/gitly.scss @@ -748,10 +748,19 @@ form { } .repo-card__header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + margin-bottom: 4px; +} + +.repo-card__heading { display: flex; align-items: center; gap: 10px; - margin-bottom: 4px; + flex-wrap: wrap; + min-width: 0; } .repo-card__title { @@ -764,6 +773,18 @@ form { } } +.repo-card__dates { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 3px; + flex-shrink: 0; + color: $gray-dark; + font-size: 12px; + line-height: 1.35; + text-align: right; +} + .repo-card__footer { display: flex; align-items: center; @@ -839,6 +860,18 @@ form { color: $gray-dark; } +@media (max-width: 700px) { + .repo-card__header { + flex-direction: column; + align-items: flex-start; + } + + .repo-card__dates { + align-items: flex-start; + text-align: left; + } +} + .repos-empty { color: $gray-dark; margin: 30px 0; diff --git a/templates/layout/head.html b/templates/layout/head.html index 3b4a08e..b2e7b50 100644 --- a/templates/layout/head.html +++ b/templates/layout/head.html @@ -1,5 +1,5 @@ -Gitly +@ctx.page_title @@ -9,4 +9,3 @@ - diff --git a/templates/user/repos.html b/templates/user/repos.html index a4cb6ad..9917235 100644 --- a/templates/user/repos.html +++ b/templates/user/repos.html @@ -19,11 +19,21 @@ @for repo in repos
- @repo.name - @if repo.is_public - Public - @else - Private +
+ @repo.name + @if repo.is_public + Public + @else + Private + @end +
+ @if repo.latest_commit_at > 0 || repo.created_at > 0 +
+ @if repo.created_at > 0 + Created @{repo.created_str()} + @end + Last activity @{repo.last_activity_str()} +
@end
@if repo.description.len > 0 @@ -37,9 +47,6 @@ @repo.lang_stats[0].name @end - @if repo.latest_commit_at > 0 - Updated @{repo.last_updated_str()} - @end
@if repo.activity_buckets.len > 0 -- 2.39.5