Added - Added optional custom branding uploads to the install page.
release-nightly / nightly-binary (push) Has been cancelled
release-nightly / nightly-container (push) Has been cancelled

- 1 - I finalized the installer `Branding` section with optional uploads for `logo.svg`, `logo.png`, `loading.png`, `favicon.svg`, and `favicon.png`, including clear format/size guidance plus a shared-assets checkbox for using one SVG and one PNG upload for both logo and favicon.
- 2 - I implemented backend validation and persistence for all branding uploads in `routers/install/install.go` (expected type checks, 1 MB limit, square PNG with minimum 64x64) and save accepted overrides under `custom/public/assets/img/`.
- 3 - I completed the runtime behavior so uploaded branding files override built-in assets through layered serving, `logo.svg` is mirrored to `gitea.svg` for legacy lookups, post-install progress prefers a custom `loading.png`, and the shared-assets mode hides favicon fields while relabeling logo fields to `Logo & Favicon SVG/PNG`.
- 4 - I manually updated Romanian locale wording for the final branding texts and labels.
This commit is contained in:
2026-05-11 20:38:50 +00:00
parent 7dbc4726fa
commit d81fdfc31f
9 changed files with 297 additions and 28 deletions
+6
View File
@@ -708,3 +708,9 @@ Project Change ID[date-time] - application-version - Type - Summary:
141 - [2026-05-10 19:27:48] - v1.27.0-dev-125-g1525c9c8ee - Type: Added - Added the password visibility toggle and confirm-password behavior to `/user/settings/account`.
- 1 - I wrapped the `settings.new_password` field in `templates/user/settings/account.tmpl` with the shared `js-password-toggle-group`, reusing the existing eye-toggle logic and show/hide labels.
- 2 - I wired the `settings.retype_new_password` field into the existing confirm-password flow through dedicated selectors, so revealing the new password hides the confirm field and keeps it synchronized the same way as signup and the install-page super-admin password form.
142 - [2026-05-10 19:43:11] - v1.27.0-dev-125-g1525c9c8ee - Type: Added - Added optional custom branding uploads to the install page.
- 1 - I finalized the installer `Branding` section with optional uploads for `logo.svg`, `logo.png`, `loading.png`, `favicon.svg`, and `favicon.png`, including clear format/size guidance plus a shared-assets checkbox for using one SVG and one PNG upload for both logo and favicon.
- 2 - I implemented backend validation and persistence for all branding uploads in `routers/install/install.go` (expected type checks, 1 MB limit, square PNG with minimum 64x64) and save accepted overrides under `custom/public/assets/img/`.
- 3 - I completed the runtime behavior so uploaded branding files override built-in assets through layered serving, `logo.svg` is mirrored to `gitea.svg` for legacy lookups, post-install progress prefers a custom `loading.png`, and the shared-assets mode hides favicon fields while relabeling logo fields to `Logo & Favicon SVG/PNG`.
- 4 - I manually updated Romanian locale wording for the final branding texts and labels.
+1 -1
View File
@@ -1 +1 @@
403aa56b4fb3e6d3b5e6d2bf3368cf8598ffe82d
34a58393847d8354e1a401512d0e56138e53bcae
+22
View File
@@ -284,6 +284,28 @@
"install.log_root_path": "Log Path",
"install.log_root_path_helper": "Log files will be written to this directory.",
"install.optional_title": "Optional Settings",
"install.branding_title": "Branding",
"install.branding_desc": "Upload optional custom logo and favicon files. If you leave any field empty, Gitea will continue using the built-in asset for that file.",
"install.branding.shared_assets": "Use the same logo files for favicon assets",
"install.branding.shared_assets_helper": "When enabled, the uploaded logo SVG and logo PNG will also be reused as the favicon SVG and favicon PNG, so you only need one upload per format.",
"install.branding.logo_svg": "Logo SVG",
"install.branding.logo_and_favicon_svg": "Logo & Favicon SVG",
"install.branding.logo_svg_helper": "Optional. SVG only, up to %d KB. Recommended for scalable UI branding.",
"install.branding.logo_png": "Logo PNG",
"install.branding.logo_and_favicon_png": "Logo & Favicon PNG",
"install.branding.logo_png_helper": "Optional. PNG only, up to %d KB. Must be square and at least %dx%d pixels. 512x512 is recommended for manifest and social preview fallbacks.",
"install.branding.loading_png": "Install Progress PNG",
"install.branding.loading_png_helper": "Optional. PNG or animated PNG only, up to %d KB. Must be square and at least %dx%d pixels. Use this to replace the post-install progress graphic.",
"install.branding.favicon_svg": "Favicon SVG",
"install.branding.favicon_svg_helper": "Optional. SVG only, up to %d KB. Use a simple square icon for best browser compatibility.",
"install.branding.favicon_png": "Favicon PNG",
"install.branding.favicon_png_helper": "Optional. PNG only, up to %d KB. Must be square and at least %dx%d pixels. 512x512 is recommended; browsers will downscale as needed.",
"install.branding.upload_read_failed": "Could not read %s: %v",
"install.branding.upload_too_big": "%s exceeds the maximum size of %d KB.",
"install.branding.upload_invalid_type": "%s must be a %s file.",
"install.branding.upload_png_square": "%s must be a square PNG image.",
"install.branding.upload_png_too_small": "%s must be at least %dx%d pixels.",
"install.branding.upload_save_failed": "Could not save branding files: %v",
"install.email_title": "Email Settings",
"install.smtp_addr": "SMTP Host",
"install.smtp_port": "SMTP Port",
+38 -16
View File
@@ -224,7 +224,7 @@
"error.report_message": "Dacă crezi că aceasta este o eroare Gitea, caută probleme pe <a href=\"%s\" target=\"_blank\">GitHub</a> sau deschide o nouă problemă dacă este necesar.",
"error.not_found": "Ținta nu a putut fi găsită.",
"error.network_error": "Eroare de rețea",
"startpage.app_desc": "Un serviciu Git fără griji și auto-găzduit",
"startpage.app_desc": "Un serviciu de git auto-găzduit și fără griji",
"startpage.install": "Ușor de instalat",
"startpage.install_desc": "Pur și simplu rulează <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[1]s\"> binarul </a> pentru platforma ta, sau ca <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[2]s\">Docker</a> ori instalează-l ca <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[3]s\">pachet</a>.",
"startpage.platform": "Multiplatformă",
@@ -232,12 +232,12 @@
"startpage.lightweight": "Fără pretenții",
"startpage.lightweight_desc": "Gitea are cerințe minime scăzute și poate rula pe un Raspberry Pi ieftin. Economisește energia mașinii tale!",
"startpage.license": "Sursă Deschisă",
"startpage.license_desc": "Descarcă codul <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[3]s\">sursă</a>, alătură-te nouă și participă la a face acest proiect mai bun.",
"startpage.license_desc": "Descarcă <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%[3]s\">codul sursă</a>, alătură-te nouă și participă activ la dezvoltarea acestui proiect.",
"install.install": "Instalare",
"install.installing_desc": "Se instalează așteptați...",
"install.title": "Configurație inițială",
"install.language_balloon": "Alegeți o limbă",
"install.docker_helper": "Dacă ruli Gitea în interiorul Docker, te rugăm să citești documentația <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\"></a> înainte de a modifica orice setare.",
"install.docker_helper": "Dacă rulezi Gitea în interiorul Docker, te rugăm să citești <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">documentația</a> înainte de a modifica orice setare.",
"install.require_db_desc": "Gitea necesită MySQL, PostgreSQL, MSSQL, SQLite3 sau TiDB (protocol MySQL).",
"install.db_title": "Setări bază de date",
"install.db_type": "Tipul bazei de date",
@@ -284,6 +284,28 @@
"install.log_root_path": "Calea de jurnal",
"install.log_root_path_helper": "Fișierele jurnal vor fi scrise în acest director.",
"install.optional_title": "Setări opționale",
"install.branding_title": "Personalizare",
"install.branding_desc": "Încarcă opțional fișiere personalizate pentru logo și favicon. Dacă lași un câmp gol, Gitea va continua să folosească fișierul implicit pentru acel element.",
"install.branding.shared_assets": "Folosește aceleași fișiere de logo și pentru favicon",
"install.branding.shared_assets_helper": "Când este activată, logo-ul SVG și logo-ul PNG încărcate vor fi refolosite și ca favicon SVG, respectiv favicon PNG, astfel încât ai nevoie de un singur upload pentru fiecare format.",
"install.branding.logo_svg": "Logo SVG",
"install.branding.logo_and_favicon_svg": "Logo & Favicon SVG",
"install.branding.logo_svg_helper": "Opțional. Doar SVG, până la %d KB. Recomandat pentru afișare scalabilă în interfață.",
"install.branding.logo_png": "Logo PNG",
"install.branding.logo_and_favicon_png": "Logo & Favicon PNG",
"install.branding.logo_png_helper": "Opțional. Doar PNG, până la %d KB. Trebuie să fie pătrat și să aibă cel puțin %dx%d pixeli. Se recomandă 512x512 pentru fallback-urile din manifest și previzualizările sociale.",
"install.branding.loading_png": "PNG pentru progresul instalării",
"install.branding.loading_png_helper": "Opțional. Doar PNG sau PNG animat, până la %d KB. Trebuie să fie pătrat și să aibă cel puțin %dx%d pixeli. Folosește-l pentru a înlocui grafica din pagina de progres post-instalare.",
"install.branding.favicon_svg": "Favicon SVG",
"install.branding.favicon_svg_helper": "Opțional. Doar SVG, până la %d KB. Folosește o pictogramă simplă și pătrată pentru compatibilitate mai bună în browsere.",
"install.branding.favicon_png": "Favicon PNG",
"install.branding.favicon_png_helper": "Opțional. Doar PNG, până la %d KB. Trebuie să fie pătrat și să aibă cel puțin %dx%d pixeli. Se recomandă 512x512; browserele îl vor micșora după nevoie.",
"install.branding.upload_read_failed": "Nu s-a putut citi %s: %v",
"install.branding.upload_too_big": "%s depășește dimensiunea maximă de %d KB.",
"install.branding.upload_invalid_type": "%s trebuie să fie un fișier de tip %s.",
"install.branding.upload_png_square": "%s trebuie să fie o imagine PNG pătrată.",
"install.branding.upload_png_too_small": "%s trebuie să aibă cel puțin %dx%d pixeli.",
"install.branding.upload_save_failed": "Nu s-au putut salva fișierele de identitate vizuală: %v",
"install.email_title": "Setări de e-mail",
"install.smtp_addr": "Gazdă SMTP",
"install.smtp_port": "Port SMTP",
@@ -331,7 +353,7 @@
"install.enable_captcha": "Activare CAPTCHA la înregistrare",
"install.enable_captcha_popup": "Solicită un CAPTCHA pentru auto-înregistrarea utilizatorului.",
"install.require_sign_in_view": "Solicită autentificare pentru a vizualiza paginile",
"install.require_sign_in_view_popup": "Limitați accesul la pagină la utilizatorii conectați. Vizitatorii vor vedea numai paginile de conectare și înregistrare.",
"install.require_sign_in_view_popup": "Permite accesul la pagini doar utilizatorilor conectați. Vizitatorii vor putea accesa doar opțiunile de conectare și înregistrare.",
"install.admin_setting_desc": "Crearea unui cont de super administrator este opțională. Primul utilizator înregistrat va deveni automat super administrator.",
"install.admin_title": "Setările contului de super administrator",
"install.admin_name": "Nume Super Administrator",
@@ -358,11 +380,11 @@
"install.invalid_admin_setting": "Setarea contului de administrator nu este validă: %v",
"install.invalid_log_root_path": "Calea jurnalului nu este validă: %v",
"install.default_keep_email_private": "Ascunde adresele de e-mail în mod implicit",
"install.default_keep_email_private_popup": "Ascunde adresele de e-mail ale noilor conturi de utilizator în mod implicit.",
"install.default_keep_email_private_popup": "Ascunde în mod implicit adresele de e-mail ale utilizatorilor.",
"install.default_allow_create_organization": "Permite crearea de organizații în mod implicit",
"install.default_allow_create_organization_popup": "Permite noilor conturi de utilizator să creeze organizații în mod implicit.",
"install.default_allow_create_organization_popup": "Permite în mod implicit utilizatorilor să creeze organizații.",
"install.default_enable_timetracking": "Activare Urmărirea timpului în mod implicit",
"install.default_enable_timetracking_popup": "Activează în mod prestabilit urmărirea timpului pentru noile repozitorii.",
"install.default_enable_timetracking_popup": "Activează în mod implicit urmărirea timpului pentru noile repozitorii.",
"install.no_reply_address": "Domeniu de e-mail ascuns",
"install.no_reply_address_helper": "Nume de domeniu pentru utilizatorii cu o adresă de e-mail ascunsă. De exemplu, numele de utilizator „joe” va fi conectat în Git ca „joe@noreply.example.org” dacă domeniul de e-mail ascuns este setat la „noreply.example.org”.",
"install.password_algorithm": "Algoritmul de hash al parolei",
@@ -1612,7 +1634,7 @@
"repo.issues.filter_sort.farduedate": "Scadență îndepărtată",
"repo.issues.filter_sort.moststars": "Multe stele",
"repo.issues.filter_sort.feweststars": "Puține stele",
"repo.issues.filter_sort.mostforks": "Multe bifurcatii",
"repo.issues.filter_sort.mostforks": "Multe bifurcații",
"repo.issues.filter_sort.fewestforks": "Puține bifurcații",
"repo.issues.quick_goto": "Treci la problemă",
"repo.issues.action_open": "Deschis(e)",
@@ -2293,12 +2315,12 @@
"repo.settings.transfer_abort_success": "Transferul repozitoriului către %s a fost anulat cu succes.",
"repo.settings.transfer_desc": "Transferă acest repozitoriu către un utilizator sau către o organizație la care ai drepturi de administrator.",
"repo.settings.enter_repo_name_to_confirm": "Introdu numele repozitoriului pentru confirmare:",
"repo.settings.enter_repo_full_name_to_confirm": "Introdu numele complet al repozitoriu (proprietar/nume) pentru confirmare:",
"repo.settings.enter_repo_full_name_to_confirm": "Introdu numele complet al repozitoriului (proprietar/nume) pentru confirmare:",
"repo.settings.transfer_in_progress": "În prezent, este în curs de desfășurare un transfer. Te rugăm să îl anulezi dacă dorești să transferi acest repozitoriu către alt utilizator.",
"repo.settings.transfer_notices_1": "- Vei pierde accesul la repozitoriu dacă îl transferi unui utilizator individual.",
"repo.settings.transfer_notices_2": "- Vei păstra accesul la repozitoriu dacă îl transferi unei organizații pe care o (co)deții.",
"repo.settings.transfer_notices_3": "- Dacă repozitoriul este privat și este transferat unui utilizator individual, această acțiune se asigură că utilizatorul are cel puțin permisiunea de citire (și modifică permisiunile dacă este necesar).",
"repo.settings.transfer_notices_4": "- Dacă repozitoriu aparține unei organizații și îl transferi către o altă organizație sau persoană, vi pierde legăturile dintre problemele repozitoriului și consiliul de repozitoriu al organizației.",
"repo.settings.transfer_notices_4": "- Dacă repozitoriu aparține unei organizații și îl transferi către o altă organizație sau persoană, vei pierde legăturile dintre problemele repozitoriului și proiectul organizației.",
"repo.settings.transfer_owner": "Proprietar Nou",
"repo.settings.transfer_perform": "Execută transferul",
"repo.settings.transfer_started": "Acest repozitoriu a fost marcat pentru transfer și așteaptă confirmarea de la „%s”",
@@ -2477,10 +2499,10 @@
"repo.settings.packagist_username": "Nume de utilizator Packagist",
"repo.settings.packagist_api_token": "Token API",
"repo.settings.packagist_package_url": "Adresa URL a pachetului Packagist",
"repo.settings.deploy_keys": "Implementare chei",
"repo.settings.deploy_keys": "Chei de Implementare",
"repo.settings.add_deploy_key": "Adaugă cheia de implementare",
"repo.settings.deploy_key_desc": "Cheile de implementare au acces de extragere doar pentru citire la repozotoriu.",
"repo.settings.is_writable": "Activează accesul la scriere",
"repo.settings.deploy_key_desc": "Cheile de implementare au doar acces de tip poll la repozitoriu.",
"repo.settings.is_writable": "Activează accesul push",
"repo.settings.is_writable_info": "Permite această cheie de implementare pentru <strong>push</strong> în repozitoriu.",
"repo.settings.no_deploy_keys": "Nu există încă chei de implementare.",
"repo.settings.title": "Titlu",
@@ -2595,9 +2617,9 @@
"repo.settings.matrix.message_type": "Tip mesaj",
"repo.settings.visibility.private.button": "Fă-l privat",
"repo.settings.visibility.private.text": "Comutarea la modul privat va face repozitoriul vizibil doar pentru membrii autorizați și poate elimina relația dintre acesta și bifurcații, observatori și stelele existente.",
"repo.settings.visibility.private.bullet_title": "<strong>Comutare vizibiliții la modul privat va:</strong>",
"repo.settings.visibility.private.bullet_title": "<strong>Comutarea vizibiliții la modul privat va:</strong>",
"repo.settings.visibility.private.bullet_one": "Face repozitoriu vizibil doar pentru membrii autorizați.",
"repo.settings.visibility.private.bullet_two": "Aplică vizibilitatea bifurcatiilor sale și elimină <strong>watchers</strong> și <strong>stars</strong>.",
"repo.settings.visibility.private.bullet_two": "Aplică vizibilitatea bifurcațiilor sale și elimină <strong>urmăritorii</strong> și <strong>stelele</strong>.",
"repo.settings.visibility.private.stats_stars": "Acest repozitoriu are <strong>%d</strong> stele care se pot pierde.",
"repo.settings.visibility.private.stats_watchers": "Acest repozitoriu are <strong>%d</strong> observatori care se pot pierde.",
"repo.settings.visibility.private.stats_forks": "Acest repozitoriu are <strong>%d</strong> bifurcații care sunt asociate.",
@@ -3258,7 +3280,7 @@
"admin.systemhooks.add_webhook": "Adaugă Webhook Sistem",
"admin.systemhooks.update_webhook": "Actualizează Webhook-ul Sistem",
"admin.auths.auth_manage_panel": "Gestionare Surse Autentificare",
"admin.auths.new": "Adaugă sursa de autentificare",
"admin.auths.new": "Adaugă Sursă Nouă",
"admin.auths.name": "Nume",
"admin.auths.type": "Tip",
"admin.auths.enabled": "Activat",
+148
View File
@@ -5,7 +5,11 @@
package install
import (
"bytes"
"errors"
"image"
_ "image/png"
"io"
"net/http"
"net/mail"
"os"
@@ -27,6 +31,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/routers/common"
@@ -46,8 +51,26 @@ const (
registrationModeLocalOnly = "local_only"
registrationModeExternalOnly = "external_only"
registrationModeLocalAndExternal = "local_and_external"
installBrandingMaxFileSize = int64(1 << 20)
installBrandingMinPNGEdge = 64
)
type installBrandingAssetSpec struct {
FormField string
TargetName string
LabelKey string
MimeType string
}
var installBrandingAssetSpecs = []installBrandingAssetSpec{
{FormField: "logo_svg", TargetName: "logo.svg", LabelKey: "install.branding.logo_svg", MimeType: typesniffer.MimeTypeImageSvg},
{FormField: "logo_png", TargetName: "logo.png", LabelKey: "install.branding.logo_png", MimeType: "image/png"},
{FormField: "loading_png", TargetName: "loading.png", LabelKey: "install.branding.loading_png", MimeType: "image/png"},
{FormField: "favicon_svg", TargetName: "favicon.svg", LabelKey: "install.branding.favicon_svg", MimeType: typesniffer.MimeTypeImageSvg},
{FormField: "favicon_png", TargetName: "favicon.png", LabelKey: "install.branding.favicon_png", MimeType: "image/png"},
}
// getSupportedDbTypeNames returns a slice for supported database types and names. The slice is used to keep the order
func getSupportedDbTypeNames() (dbTypeNames []map[string]string) {
for _, t := range setting.SupportedDatabaseTypes {
@@ -155,9 +178,120 @@ func normalizeInstallRegistrationOptions(form *forms.InstallForm) {
}
}
func collectInstallBrandingAssets(ctx *context.Context, useSharedAssets bool) (map[string][]byte, error) {
uploads := make(map[string][]byte)
for _, spec := range installBrandingAssetSpecs {
file, header, err := ctx.Req.FormFile(spec.FormField)
if errors.Is(err, http.ErrMissingFile) {
continue
}
if err != nil {
return nil, errors.New(string(ctx.Tr("install.branding.upload_read_failed", ctx.Tr(spec.LabelKey), err)))
}
data, err := func() ([]byte, error) {
defer file.Close()
if header.Size > installBrandingMaxFileSize {
return nil, errors.New(string(ctx.Tr("install.branding.upload_too_big", ctx.Tr(spec.LabelKey), installBrandingMaxFileSize/1024)))
}
data, err := io.ReadAll(io.LimitReader(file, installBrandingMaxFileSize+1))
if err != nil {
return nil, errors.New(string(ctx.Tr("install.branding.upload_read_failed", ctx.Tr(spec.LabelKey), err)))
}
if int64(len(data)) > installBrandingMaxFileSize {
return nil, errors.New(string(ctx.Tr("install.branding.upload_too_big", ctx.Tr(spec.LabelKey), installBrandingMaxFileSize/1024)))
}
return data, nil
}()
if err != nil {
return nil, err
}
sniffedType := typesniffer.DetectContentType(data)
switch spec.MimeType {
case typesniffer.MimeTypeImageSvg:
if !sniffedType.IsSvgImage() {
return nil, errors.New(string(ctx.Tr("install.branding.upload_invalid_type", ctx.Tr(spec.LabelKey), "SVG")))
}
case "image/png":
if sniffedType.GetMimeType() != "image/png" {
return nil, errors.New(string(ctx.Tr("install.branding.upload_invalid_type", ctx.Tr(spec.LabelKey), "PNG")))
}
cfg, format, err := image.DecodeConfig(bytes.NewReader(data))
if err != nil || format != "png" {
return nil, errors.New(string(ctx.Tr("install.branding.upload_invalid_type", ctx.Tr(spec.LabelKey), "PNG")))
}
if cfg.Width != cfg.Height {
return nil, errors.New(string(ctx.Tr("install.branding.upload_png_square", ctx.Tr(spec.LabelKey))))
}
if cfg.Width < installBrandingMinPNGEdge || cfg.Height < installBrandingMinPNGEdge {
return nil, errors.New(string(ctx.Tr("install.branding.upload_png_too_small", ctx.Tr(spec.LabelKey), installBrandingMinPNGEdge, installBrandingMinPNGEdge)))
}
}
uploads[spec.TargetName] = data
}
if useSharedAssets {
if logoSVG, ok := uploads["logo.svg"]; ok {
uploads["favicon.svg"] = logoSVG
}
if logoPNG, ok := uploads["logo.png"]; ok {
uploads["favicon.png"] = logoPNG
}
}
return uploads, nil
}
func saveInstallBrandingAssets(uploads map[string][]byte) error {
if len(uploads) == 0 {
return nil
}
customImgPath := filepath.Join(setting.CustomPath, "public", "assets", "img")
if err := os.MkdirAll(customImgPath, os.ModePerm); err != nil {
return err
}
for targetName, data := range uploads {
if err := os.WriteFile(filepath.Join(customImgPath, targetName), data, 0o644); err != nil {
return err
}
}
if logoSVG, ok := uploads["logo.svg"]; ok {
if err := os.WriteFile(filepath.Join(customImgPath, "gitea.svg"), logoSVG, 0o644); err != nil {
return err
}
}
return nil
}
func getInstallProgressLogo() string {
customImgPath := filepath.Join(setting.CustomPath, "public", "assets", "img")
if _, err := os.Stat(filepath.Join(customImgPath, "loading.png")); err == nil {
return "loading.png"
}
if _, err := os.Stat(filepath.Join(customImgPath, "logo.svg")); err == nil {
return "logo.svg"
}
if _, err := os.Stat(filepath.Join(customImgPath, "logo.png")); err == nil {
return "logo.png"
}
return "loading.png"
}
func installContexter() func(next http.Handler) http.Handler {
return context.ContexterInstallPage(map[string]any{
"DbTypeNames": getSupportedDbTypeNames(),
"BrandingMaxFileSizeKB": installBrandingMaxFileSize / 1024,
"BrandingMinPNGEdge": installBrandingMinPNGEdge,
"EnvConfigKeys": setting.CollectEnvConfigKeys(),
"CustomConfFile": setting.CustomConf,
"PasswordHashAlgorithms": hash.RecommendedHashAlgorithms,
@@ -535,6 +669,13 @@ func SubmitInstall(ctx *context.Context) {
}
}
brandingUploads, err := collectInstallBrandingAssets(ctx, form.BrandingUseSharedAssets)
if err != nil {
ctx.Data["Err_Branding"] = true
ctx.RenderWithErrDeprecated(err.Error(), tplInstall, form)
return
}
// Init the engine with migration
if err = db.InitEngineWithMigration(ctx, versioned_migration.Migrate); err != nil {
db.UnsetDefaultEngine()
@@ -694,6 +835,12 @@ func SubmitInstall(ctx *context.Context) {
return
}
if err = saveInstallBrandingAssets(brandingUploads); err != nil {
ctx.Data["Err_Branding"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("install.branding.upload_save_failed", err), tplInstall, &form)
return
}
// unset default engine before reload database setting
db.UnsetDefaultEngine()
@@ -782,5 +929,6 @@ func SubmitInstall(ctx *context.Context) {
func InstallDone(ctx *context.Context) { //nolint:revive // export stutter
hasUsers, _ := user_model.HasUsers(ctx)
ctx.Data["IsAccountCreated"] = hasUsers.HasAnyUser
ctx.Data["InstallProgressLogo"] = getInstallProgressLogo()
ctx.HTML(http.StatusOK, tplPostInstall)
}
+9 -8
View File
@@ -38,14 +38,15 @@ type InstallForm struct {
AppURL string `binding:"Required"`
LogRootPath string `binding:"Required"`
SMTPAddr string
SMTPPort string
SMTPFrom string
SMTPUser string `binding:"OmitEmpty;MaxSize(254)" locale:"install.mailer_user"`
SMTPPasswd string
RegisterConfirm bool
RegisterManualConfirm bool
MailNotify bool
SMTPAddr string
SMTPPort string
SMTPFrom string
SMTPUser string `binding:"OmitEmpty;MaxSize(254)" locale:"install.mailer_user"`
SMTPPasswd string
RegisterConfirm bool
RegisterManualConfirm bool
MailNotify bool
BrandingUseSharedAssets bool
RegistrationMode string `binding:"In(,admin_only,local_only,external_only,local_and_external)" locale:"install.registration_mode"`
AdminCreatedAccountMode string `binding:"In(,local,invite)" locale:"install.admin_created_account_mode"`
+41 -1
View File
@@ -10,7 +10,7 @@
<p>{{ctx.Locale.Tr "install.docker_helper" "https://docs.gitea.com/installation/install-with-docker"}}</p>
<form class="ui form js-install-form" action="{{AppSubUrl}}/" method="post">
<form class="ui form js-install-form" action="{{AppSubUrl}}/" method="post" enctype="multipart/form-data">
<!-- Database Settings -->
<h4 class="ui dividing header">{{ctx.Locale.Tr "install.db_title"}}</h4>
<p>{{ctx.Locale.Tr "install.require_db_desc"}}</p>
@@ -174,6 +174,46 @@
<!-- Optional Settings -->
<h4 class="ui dividing header">{{ctx.Locale.Tr "install.optional_title"}}</h4>
<div>
<!-- Branding -->
<details class="optional field"{{if .Err_Branding}} open{{end}}>
<summary class="right-content tw-py-2{{if .Err_Branding}} tw-text-red{{end}}">
{{ctx.Locale.Tr "install.branding_title"}}
</summary>
<p class="center">{{ctx.Locale.Tr "install.branding_desc"}}</p>
<div class="inline field">
<div class="ui checkbox">
<label for="branding_use_shared_assets">{{ctx.Locale.Tr "install.branding.shared_assets"}}</label>
<input id="branding_use_shared_assets" name="branding_use_shared_assets" type="checkbox" {{if .branding_use_shared_assets}}checked{{end}}>
</div>
<span class="help">{{ctx.Locale.Tr "install.branding.shared_assets_helper"}}</span>
</div>
<div class="inline field">
<label for="logo_svg" id="branding_logo_svg_label" data-default-label="{{ctx.Locale.Tr "install.branding.logo_svg"}}" data-shared-label="{{ctx.Locale.Tr "install.branding.logo_and_favicon_svg"}}">{{ctx.Locale.Tr "install.branding.logo_svg"}}</label>
<input id="logo_svg" name="logo_svg" type="file" accept=".svg,image/svg+xml">
<span class="help">{{ctx.Locale.Tr "install.branding.logo_svg_helper" .BrandingMaxFileSizeKB}}</span>
</div>
<div class="inline field">
<label for="logo_png" id="branding_logo_png_label" data-default-label="{{ctx.Locale.Tr "install.branding.logo_png"}}" data-shared-label="{{ctx.Locale.Tr "install.branding.logo_and_favicon_png"}}">{{ctx.Locale.Tr "install.branding.logo_png"}}</label>
<input id="logo_png" name="logo_png" type="file" accept=".png,image/png">
<span class="help">{{ctx.Locale.Tr "install.branding.logo_png_helper" .BrandingMaxFileSizeKB .BrandingMinPNGEdge .BrandingMinPNGEdge}}</span>
</div>
<div class="inline field">
<label for="loading_png">{{ctx.Locale.Tr "install.branding.loading_png"}}</label>
<input id="loading_png" name="loading_png" type="file" accept=".png,image/png">
<span class="help">{{ctx.Locale.Tr "install.branding.loading_png_helper" .BrandingMaxFileSizeKB .BrandingMinPNGEdge .BrandingMinPNGEdge}}</span>
</div>
<div class="inline field js-install-branding-favicon-field">
<label for="favicon_svg">{{ctx.Locale.Tr "install.branding.favicon_svg"}}</label>
<input id="favicon_svg" name="favicon_svg" type="file" accept=".svg,image/svg+xml">
<span class="help">{{ctx.Locale.Tr "install.branding.favicon_svg_helper" .BrandingMaxFileSizeKB}}</span>
</div>
<div class="inline field js-install-branding-favicon-field">
<label for="favicon_png">{{ctx.Locale.Tr "install.branding.favicon_png"}}</label>
<input id="favicon_png" name="favicon_png" type="file" accept=".png,image/png">
<span class="help">{{ctx.Locale.Tr "install.branding.favicon_png_helper" .BrandingMaxFileSizeKB .BrandingMinPNGEdge .BrandingMinPNGEdge}}</span>
</div>
</details>
<!-- Email -->
<details class="optional field">
<summary class="right-content tw-py-2{{if .Err_SMTP}} tw-text-red{{end}}">
+1 -2
View File
@@ -1,8 +1,7 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content install post-install tw-h-full">
<div class="home tw-text-center tw-h-full tw-flex tw-flex-col tw-justify-center"><!-- the "home" class makes the links green -->
<!-- the "cup" has a handler, so move it a little leftward to make it visually in the center -->
<div class="tw-ml-[-30px]"><img width="160" src="{{AssetUrlPrefix}}/img/loading.png" alt aria-hidden="true"></div>
<div{{if eq .InstallProgressLogo "loading.png"}} class="tw-ml-[-30px]"{{end}}><img width="160" src="{{AssetUrlPrefix}}/img/{{.InstallProgressLogo}}" alt aria-hidden="true"></div>
<div class="tw-my-[2em] tw-text-[18px]">
<a id="goto-after-install" href="{{AppSubUrl}}{{Iif .IsAccountCreated "/user/login" "/user/sign_up"}}">{{ctx.Locale.Tr "install.installing_desc"}}</a>
</div>
+31
View File
@@ -129,6 +129,37 @@ function initPreInstall() {
syncRegistrationManagement();
}
const brandingUseSharedAssets = document.querySelector<HTMLInputElement>('#branding_use_shared_assets');
const brandingFaviconFields = document.querySelectorAll<HTMLElement>('.js-install-branding-favicon-field');
const brandingLogoSvgLabel = document.querySelector<HTMLElement>('#branding_logo_svg_label');
const brandingLogoPngLabel = document.querySelector<HTMLElement>('#branding_logo_png_label');
if (brandingUseSharedAssets && brandingFaviconFields.length > 0) {
const syncBrandingLabels = () => {
if (brandingLogoSvgLabel) {
brandingLogoSvgLabel.textContent = brandingUseSharedAssets.checked ?
brandingLogoSvgLabel.getAttribute('data-shared-label') :
brandingLogoSvgLabel.getAttribute('data-default-label');
}
if (brandingLogoPngLabel) {
brandingLogoPngLabel.textContent = brandingUseSharedAssets.checked ?
brandingLogoPngLabel.getAttribute('data-shared-label') :
brandingLogoPngLabel.getAttribute('data-default-label');
}
};
const syncBrandingFields = () => {
if (brandingUseSharedAssets.checked) {
for (const field of brandingFaviconFields) hideElem(field);
} else {
for (const field of brandingFaviconFields) showElem(field);
}
syncBrandingLabels();
};
brandingUseSharedAssets.addEventListener('change', syncBrandingFields);
syncBrandingFields();
}
}
function initPostInstall() {