From cc0e0b1afb9d8e496bb2de9849994f8b5163c647 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sat, 16 May 2026 11:23:27 +0300 Subject: [PATCH] delete repo confirm modal; header/settings i18n; add es/jp/cn/pt languages; issue title markdown; tree poller path --- gitly.v | 9 +- issue.v | 28 +-- main.v | 16 +- repo/repo_routes.v | 3 +- repo/repo_template.v | 8 + static/assets/version | 2 +- static/css/gitly.scss | 386 ++++++++++++++++++++++++++++++++++- static/js/tree.js | 71 +++---- templates/issue.html | 4 +- templates/layout/header.html | 30 +-- templates/new.html | 7 +- templates/repo/settings.html | 77 ++++++- translations/en.tr | 90 +++++++- translations/ru.tr | 92 ++++++++- 14 files changed, 728 insertions(+), 95 deletions(-) diff --git a/gitly.v b/gitly.v index 1f8806e..7cf5fdc 100644 --- a/gitly.v +++ b/gitly.v @@ -158,7 +158,14 @@ pub fn (mut app App) before_request(mut ctx Context) bool { unsafe { prealloc_scope_checkpoint(c'gitly loaded user') } } lang_cookie := ctx.get_cookie('lang') or { '' } - ctx.lang = if lang_cookie == 'ru' { .ru } else { .en } + ctx.lang = match lang_cookie { + 'ru' { Lang.ru } + 'es' { Lang.es } + 'jp' { Lang.jp } + 'cn' { Lang.cn } + 'pt' { Lang.pt } + else { Lang.en } + } $if trace_prealloc ? { unsafe { prealloc_scope_checkpoint(c'gitly loaded lang') } diff --git a/issue.v b/issue.v index 148b682..8d3312f 100644 --- a/issue.v +++ b/issue.v @@ -4,6 +4,7 @@ module main import time import veb +import highlight struct Issue { id int @[primary; sql: serial] @@ -161,20 +162,19 @@ fn html_escape_text(s string) string { return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"') } -// formatted_title HTML-escapes the issue title and converts `backtick`-quoted -// segments into spans so titles like `unknown method or field: ` + "`db.pg.Row.val`" + `` render nicely. +// formatted_title renders the issue title as inline markdown so titles like +// `unknown method or field: ` + "`db.pg.Row.val`" + `` get spans and +// other inline markup. The wrapping

tag added by the markdown converter is +// stripped so the title stays inline. fn (i &Issue) formatted_title() veb.RawHtml { - parts := i.title.split('`') - mut out := '' - for idx, p in parts { - if idx % 2 == 0 { - out += html_escape_text(p) - } else if idx == parts.len - 1 { - // Unmatched trailing backtick: keep as literal. - out += '`' + html_escape_text(p) - } else { - out += '' + html_escape_text(p) + '' - } + rendered := highlight.convert_markdown_to_html(i.title).trim_space() + if rendered.starts_with('

') && rendered.ends_with('

') { + return rendered[3..rendered.len - 4] } - return out + return rendered +} + +// formatted_body renders the issue text as markdown. +fn (i &Issue) formatted_body() veb.RawHtml { + return highlight.convert_markdown_to_html(i.text) } diff --git a/main.v b/main.v index 477b717..a8fa998 100644 --- a/main.v +++ b/main.v @@ -5,10 +5,18 @@ import config enum Lang { en ru + es + jp + cn + pt } -const tr_menu_en = '' -const tr_menu_ru = '' +const tr_menu_en = '' +const tr_menu_ru = '' +const tr_menu_es = '' +const tr_menu_jp = '' +const tr_menu_cn = '' +const tr_menu_pt = '' fn get_port(conf config.Config) int { // Priority: -p flag > GITLY_PORT env > config.json port > 8080 @@ -48,5 +56,9 @@ fn build_tr_menu(cur_lang Lang) string { return match cur_lang { .ru { tr_menu_ru } .en { tr_menu_en } + .es { tr_menu_es } + .jp { tr_menu_jp } + .cn { tr_menu_cn } + .pt { tr_menu_pt } } } diff --git a/repo/repo_routes.v b/repo/repo_routes.v index 76f75fc..fa0ced9 100644 --- a/repo/repo_routes.v +++ b/repo/repo_routes.v @@ -581,7 +581,8 @@ pub fn (mut app App) handle_api_repo_watch(mut ctx Context, repo_id_str string) } // API: get file listing with commit info for a directory (used by JS polling) -@['/api/v1/repos/:repo_id_str/files'] +// Path uses /tree/files to avoid colliding with /api/v1/repos/:username/:repo_name. +@['/api/v1/repos/:repo_id_str/tree/files'] pub fn (mut app App) handle_api_repo_files(mut ctx Context, repo_id_str string) veb.Result { repo_id := repo_id_str.int() repo := app.find_repo_by_id(repo_id) or { return ctx.json_error('Not found') } diff --git a/repo/repo_template.v b/repo/repo_template.v index 73f94a5..dbd03ca 100644 --- a/repo/repo_template.v +++ b/repo/repo_template.v @@ -65,3 +65,11 @@ fn (r &Repo) format_nr_topics(lang Lang) veb.RawHtml { fn (r &Repo) format_nr_releases(lang Lang) veb.RawHtml { return format_count(r.nr_releases, 'releases_count', lang) } + +fn (r &Repo) format_nr_stars(lang Lang) veb.RawHtml { + return format_count(r.nr_stars, 'stars_count', lang) +} + +fn (mut app App) format_nr_watchers(repo_id int, lang Lang) veb.RawHtml { + return format_count(app.get_count_repo_watchers(repo_id), 'watchers_count', lang) +} diff --git a/static/assets/version b/static/assets/version index e31469b..de91cf9 100644 --- a/static/assets/version +++ b/static/assets/version @@ -1 +1 @@ -3fe5149 \ No newline at end of file +b8b99f0 \ No newline at end of file diff --git a/static/css/gitly.scss b/static/css/gitly.scss index ebb27e9..8ef9e63 100644 --- a/static/css/gitly.scss +++ b/static/css/gitly.scss @@ -608,6 +608,10 @@ form { flex: 1; min-width: 200px; box-sizing: border-box; + + &::placeholder { + color: $gray-dark; + } } .settings-form__submit { @@ -621,11 +625,18 @@ form { color: #cf222e !important; border-color: rgba(207, 34, 46, 0.4) !important; - &:hover { + &:hover:not(:disabled) { color: $white !important; background-color: #cf222e !important; border-color: #cf222e !important; } + + &:disabled { + color: rgba(207, 34, 46, 0.5) !important; + border-color: rgba(207, 34, 46, 0.2) !important; + background-color: transparent !important; + cursor: not-allowed; + } } .settings-section--danger { @@ -677,6 +688,98 @@ form { min-width: 280px; } +.confirm-modal { + position: fixed; + inset: 0; + background-color: rgba(15, 17, 21, 0.55); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 20px; + + &[hidden] { + display: none; + } +} + +.confirm-modal__dialog { + background-color: #cf222e; + color: $white; + border-radius: $small-radius; + max-width: 480px; + width: 100%; + padding: 28px; + box-shadow: 0 18px 50px rgba(0, 0, 0, 0.4); +} + +.confirm-modal__alert { + margin: 0 0 16px; + font-size: 16px; + font-weight: 700; + line-height: 1.35; +} + +.confirm-modal__intro, +.confirm-modal__outro, +.confirm-modal__question { + margin: 0 0 10px; + font-size: 14px; + color: $white; +} + +.confirm-modal__question { + margin-top: 16px; + font-weight: 600; +} + +.confirm-modal__stats { + margin: 0 0 10px; + padding-left: 20px; + font-size: 14px; + color: $white; + + li { + margin: 2px 0; + } +} + +.confirm-modal__actions { + display: flex; + gap: 10px; + justify-content: flex-end; + margin-top: 20px; +} + +.confirm-modal__btn { + padding: 7px 16px; + font-size: 14px; + font-weight: 600; + border-radius: $small-radius; + border: 1px solid $white; + background-color: transparent; + color: $white; + cursor: pointer; + transition: background-color 0.12s, color 0.12s; + + &:hover { + background-color: $white; + color: #cf222e; + } +} + +.confirm-modal__btn--danger { + background-color: $white; + color: #cf222e; + border-color: $white; + + &:hover { + background-color: #b51d28; + color: $white; + border-color: $white; + } +} + .new-repo { max-width: 640px; } @@ -1583,7 +1686,288 @@ form { font-size: 13px; } +.inline-form { + display: inline-block; + margin: 0; +} + +.link-button { + font-size: 13px; + padding: 5px 12px; + background-color: $white; + color: $black; + border: 1px solid $gray; + border-radius: $small-radius; + cursor: pointer; + font-family: inherit; + line-height: 1.4; + transition: background-color 0.07s, border-color 0.07s; + + &:hover { + background-color: #f3f4f6; + text-decoration: none; + } +} + +.link-button--danger { + color: #cf222e; + border-color: rgba(207, 34, 46, 0.3); + + &:hover { + background-color: #cf222e; + color: $white; + border-color: #cf222e; + } +} + +.project-desc { + color: $gray-dark; + margin: -8px 0 20px; + font-size: 14px; + line-height: 1.5; +} +.kanban-board { + display: flex; + gap: 14px; + align-items: flex-start; + overflow-x: auto; + padding: 4px 0 16px; + margin: 0 -4px; +} + +.kanban-column { + flex: 0 0 280px; + background-color: #f6f8fa; + border: 1px solid $gray; + border-radius: $medium-radius; + display: flex; + flex-direction: column; + max-height: calc(100vh - 220px); +} + +.kanban-column--add { + background-color: transparent; + border: 1px dashed $gray; + + .kanban-card-form { + border-top: none; + padding: 12px; + } +} + +.kanban-column__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 10px 12px; + border-bottom: 1px solid $gray; + + h3 { + margin: 0; + font-size: 13px; + font-weight: 600; + color: $black; + text-transform: uppercase; + letter-spacing: 0.4px; + } + + .inline-form { + display: inline; + margin: 0; + } + + .link-button--danger { + color: $gray-dark; + font-size: 18px; + line-height: 1; + background: none; + border: none; + cursor: pointer; + padding: 0 4px; + opacity: 0; + transition: opacity 0.1s, color 0.1s, background-color 0.1s; + + &:hover { + color: #cf222e; + background: none; + } + } +} + +.kanban-column:hover .kanban-column__header .link-button--danger { + opacity: 1; +} + +.kanban-cards { + list-style: none; + margin: 0; + padding: 10px; + display: flex; + flex-direction: column; + gap: 8px; + overflow-y: auto; + flex: 1; +} + +.kanban-cards__empty { + color: $gray-dark; + font-size: 13px; + text-align: center; + padding: 16px 8px; + border: 1px dashed $gray; + border-radius: $small-radius; + background-color: $white; +} + +.kanban-card { + background-color: $white; + border: 1px solid $gray; + border-radius: $small-radius; + padding: 10px 12px; + box-shadow: 0 1px 0 rgba(27, 31, 36, 0.04); + transition: box-shadow 0.07s, border-color 0.07s; + + &:hover { + border-color: #b8b8b8; + box-shadow: 0 1px 3px rgba(27, 31, 36, 0.08); + + .kanban-card__actions { + opacity: 1; + } + } +} + +.kanban-card__title { + font-weight: 500; + font-size: 14px; + color: $black; + line-height: 1.4; +} + +.kanban-card__note { + color: $gray-dark; + font-size: 12px; + margin: 6px 0 0; + line-height: 1.45; + white-space: pre-wrap; +} + +.kanban-card__actions { + display: flex; + align-items: center; + gap: 8px; + margin-top: 10px; + padding-top: 8px; + border-top: 1px solid $gray-light; + opacity: 0; + transition: opacity 0.1s; + + .inline-form { + display: inline-flex; + gap: 4px; + margin: 0; + align-items: center; + } + + select { + font-size: 12px; + padding: 3px 6px; + border: 1px solid $gray; + border-radius: $small-radius; + background-color: $white; + color: $black; + max-width: 130px; + } + + .link-button { + font-size: 12px; + padding: 3px 8px; + background-color: $white; + border: 1px solid $gray; + border-radius: $small-radius; + color: $black; + cursor: pointer; + + &:hover { + background-color: #f3f4f6; + } + } + + .link-button--danger { + color: #cf222e; + margin-left: auto; + border-color: transparent; + background: none; + padding: 3px 4px; + + &:hover { + background-color: rgba(207, 34, 46, 0.08); + } + } +} + +.kanban-card-form { + display: flex; + flex-direction: column; + gap: 6px; + padding: 10px; + border-top: 1px solid $gray; + background-color: $white; + border-bottom-left-radius: $medium-radius; + border-bottom-right-radius: $medium-radius; + + input[type="text"], + textarea { + font-size: 13px; + padding: 6px 8px; + border: 1px solid $gray; + border-radius: $small-radius; + font-family: inherit; + color: $black; + background-color: $white; + box-sizing: border-box; + width: 100%; + resize: vertical; + + &:focus { + outline: none; + border-color: #0969da; + box-shadow: 0 0 0 2px rgba(9, 105, 218, 0.18); + } + } + + textarea { + min-height: 38px; + } + + .link-button { + align-self: flex-start; + font-size: 12px; + padding: 5px 12px; + background-color: #1f883d; + color: $white; + border: 1px solid rgba(31, 136, 61, 0.6); + border-radius: $small-radius; + font-weight: 500; + cursor: pointer; + + &:hover { + background-color: #1a7f37; + } + } +} + +.kanban-column--add .kanban-card-form .link-button { + background-color: $white; + color: $black; + border-color: $gray; + + &:hover { + background-color: #f3f4f6; + } +} diff --git a/static/js/tree.js b/static/js/tree.js index 83c15a6..97a76eb 100644 --- a/static/js/tree.js +++ b/static/js/tree.js @@ -116,54 +116,51 @@ if (watchButtonEl) { if (!hasMissingInfo()) return; const path = typeof CURRENT_PATH !== "undefined" ? CURRENT_PATH : ""; - const apiUrl = "/api/v1/repos/" + REPO_ID + "/files?branch=" + + const apiUrl = "/api/v1/repos/" + REPO_ID + "/tree/files?branch=" + encodeURIComponent(BRANCH_NAME) + "&path=" + encodeURIComponent(path); let attempts = 0; - const maxAttempts = 30; + const maxAttempts = 60; + + function applyFiles(files) { + for (const file of files) { + if (file.last_msg) { + const msgEl = findDataEl("data-msg-for", file.name); + if (msgEl) { + const link = msgEl.querySelector("a"); + if (link && link.textContent.trim() === "") { + link.textContent = file.last_msg; + if (file.last_hash) { + link.href = "/" + REPO_USER + "/" + REPO_NAME + "/commit/" + file.last_hash; + } + } + } + } + const timeEl = findDataEl("data-time-for", file.name); + if (timeEl && timeEl.textContent.trim() === "" && file.last_time) { + timeEl.textContent = file.last_time; + } + if (TREE_FOLDER_SIZE_ENABLED && file.size) { + const sizeEl = findDataEl("data-size-for", file.name); + if (sizeEl && sizeEl.textContent.trim() === "") { + sizeEl.textContent = file.size; + } + } + } + } function poll() { attempts++; fetch(apiUrl) .then(function(r) { return r.json(); }) .then(function(data) { - if (!data.success || !data.result) return; - - let stillMissing = false; - for (const file of data.result) { - const sizeEl = TREE_FOLDER_SIZE_ENABLED ? findDataEl("data-size-for", file.name) : null; - if (!file.last_msg || (sizeEl && !file.size)) { - stillMissing = true; - } - if (file.last_msg) { - const msgEl = findDataEl("data-msg-for", file.name); - if (msgEl) { - const link = msgEl.querySelector("a"); - if (link && link.textContent.trim() === "") { - link.textContent = file.last_msg; - if (file.last_hash) { - link.href = "/" + REPO_USER + "/" + REPO_NAME + "/commit/" + file.last_hash; - } - } - } - } - if (sizeEl && file.size) { - if (sizeEl && sizeEl.textContent.trim() === "") { - sizeEl.textContent = file.size; - } - } - const timeEl = findDataEl("data-time-for", file.name); - if (timeEl && timeEl.textContent.trim() === "" && file.last_time) { - timeEl.textContent = file.last_time; - } - } - - if (stillMissing && attempts < maxAttempts) { - setTimeout(poll, 2000); + if (data && data.success && Array.isArray(data.result)) { + applyFiles(data.result); } }) - .catch(function() { - if (attempts < maxAttempts) { + .catch(function() {}) + .finally(function() { + if (hasMissingInfo() && attempts < maxAttempts) { setTimeout(poll, 2000); } }); diff --git a/templates/issue.html b/templates/issue.html index aaa99a4..5f2bf7e 100644 --- a/templates/issue.html +++ b/templates/issue.html @@ -14,12 +14,12 @@ @include 'layout/repo_menu.html'
-

@issue.title #@issue.id

+

@{issue.formatted_title()} #@issue.id

diff --git a/templates/layout/header.html b/templates/layout/header.html index fd56317..6834791 100644 --- a/templates/layout/header.html +++ b/templates/layout/header.html @@ -10,7 +10,7 @@
- +
@if ctx.logged_in @@ -25,31 +25,31 @@ } .header-dropdown { - .username { - Signed in as - @ctx.user.username - } .links { - Profile - Repositories - Issues - Stars - Feed + %header_repositories + %header_profile + %header_issues + %header_stars + %header_feed } .links { - Settings + %header_settings @if ctx.user.is_admin - Admin Panel + %header_admin_panel @end } + .username { + %header_signed_in_as + @ctx.user.username + } .links { - Sign out + %header_sign_out } } @else - - + + @end diff --git a/templates/new.html b/templates/new.html index 463c3d3..a8af06e 100644 --- a/templates/new.html +++ b/templates/new.html @@ -6,9 +6,10 @@ @include 'layout/header.html' -
+
+
-

%new_repo_title

+

%new_repo_title pepehands

@if ctx.form_error != '' @@ -22,7 +23,6 @@ @app.config.hostname/@ctx.user.username/
-

%new_repo_name_hint

@@ -64,6 +64,7 @@
+
@include '../layout/footer.html' diff --git a/translations/en.tr b/translations/en.tr index dc8ac4d..6f01236 100644 --- a/translations/en.tr +++ b/translations/en.tr @@ -200,9 +200,6 @@ Create a new repository new_repo_name Name ----- -new_repo_name_hint -Great repository names are short and memorable. ------ new_repo_description Description ----- @@ -260,6 +257,12 @@ tag|tags|tags topics_count discussion|discussions|discussions ----- +stars_count +star|stars|stars +----- +watchers_count +watcher|watchers|watchers +----- ci_label CI ----- @@ -683,4 +686,85 @@ This token is shown only once. Copy it now: api_docs API documentation ----- +repo_delete_warning +This repo will be permanently removed with no way to revert this action. +----- +repo_delete_public_intro +It's a public repository with: +----- +repo_delete_public_outro +All of them will be lost. +----- +repo_delete_confirm_question +Are you sure you want to continue? +----- +repo_delete_confirm_yes +Yes, delete repository +----- +repo_delete_cancel +Cancel +----- +settings_title +Settings +----- +danger_zone +Danger zone +----- +transfer_ownership +Transfer ownership +----- +transfer_ownership_desc +Move this repository to another user. Type the full path to confirm. +----- +new_owner_username +New owner username +----- +move_repo +Move repo +----- +delete_repo_title +Delete this repository +----- +delete_repo_desc +Once deleted, the repository is gone. Type the full path to confirm. +----- +delete_repo +Delete repo +----- +header_search_placeholder +Search... +----- +header_login +Log in +----- +header_register +Register +----- +header_profile +Profile +----- +header_repositories +Repositories +----- +header_issues +Issues +----- +header_stars +Stars +----- +header_feed +Feed +----- +header_settings +Settings +----- +header_admin_panel +Admin Panel +----- +header_signed_in_as +Signed in as +----- +header_sign_out +Sign out +----- diff --git a/translations/ru.tr b/translations/ru.tr index 545f694..fb02de8 100644 --- a/translations/ru.tr +++ b/translations/ru.tr @@ -195,14 +195,11 @@ open_source_outro В Gitly другой подход: каждая функция — часть общей кодовой базы с открытым исходным кодом. Нет Enterprise Edition, нет платных модулей и нет отдельного «premium»-репозитория. ----- new_repo_title -Создать новый репозиторий +Создать новый репозиторий KEKW ----- new_repo_name Название ----- -new_repo_name_hint -Хорошие названия репозиториев короткие и запоминающиеся. ------ new_repo_description Описание ----- @@ -260,6 +257,12 @@ tags_count topics_count обсуждение|обсуждения|обсуждений ----- +stars_count +звезда|звезды|звёзд +----- +watchers_count +наблюдатель|наблюдателя|наблюдателей +----- ci_label CI ----- @@ -683,4 +686,85 @@ api_token_show_once api_docs Документация API ----- +repo_delete_warning +Этот репозиторий будет удалён навсегда без возможности восстановления. +----- +repo_delete_public_intro +Это публичный репозиторий со следующей статистикой: +----- +repo_delete_public_outro +Всё это будет потеряно. +----- +repo_delete_confirm_question +Вы уверены, что хотите продолжить? +----- +repo_delete_confirm_yes +Да, удалить репозиторий +----- +repo_delete_cancel +Отмена +----- +settings_title +Настройки +----- +danger_zone +Опасная зона +----- +transfer_ownership +Передать владение +----- +transfer_ownership_desc +Перенести этот репозиторий другому пользователю. Введите полный путь для подтверждения. +----- +new_owner_username +Имя нового владельца +----- +move_repo +Передать репозиторий +----- +delete_repo_title +Удалить этот репозиторий +----- +delete_repo_desc +После удаления репозиторий пропадёт. Введите полный путь для подтверждения. +----- +delete_repo +Удалить репозиторий +----- +header_search_placeholder +Поиск... +----- +header_login +Войти +----- +header_register +Регистрация +----- +header_profile +Профиль +----- +header_repositories +Репозитории +----- +header_issues +Задачи +----- +header_stars +Звёзды +----- +header_feed +Лента +----- +header_settings +Настройки +----- +header_admin_panel +Панель администратора +----- +header_signed_in_as +Вы вошли как +----- +header_sign_out +Выйти +----- -- 2.39.5