Compare commits

...

1 Commits

Author SHA1 Message Date
Jonathan Miller deb002ccdc fix: login logo defaults to none, separate from nav icon
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 18s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 22:25:50 -05:00
3 changed files with 200 additions and 67 deletions
+95 -8
View File
@@ -29,12 +29,69 @@ func Branding(ctx *context.Context) {
imgDir := brandingImageDir()
ctx.Data["HasNavIcon"] = fileExists(filepath.Join(imgDir, "logo-small.png"))
ctx.Data["HasLogo"] = fileExists(filepath.Join(imgDir, "logo.png"))
ctx.Data["HasLoginLogo"] = fileExists(filepath.Join(imgDir, "login-logo.png"))
ctx.Data["HasFavicon"] = fileExists(filepath.Join(imgDir, "favicon.png"))
ctx.Data["MetaDescription"] = setting.UI.Meta.Description
ctx.Data["MetaAuthor"] = setting.UI.Meta.Author
ctx.Data["HelpURL"] = setting.HelpURL
ctx.Data["SupportURL"] = setting.SupportURL
ctx.HTML(http.StatusOK, tplBranding)
}
// BrandingSettings handles the text branding form submission.
func BrandingSettings(ctx *context.Context) {
appName := ctx.FormString("app_name")
description := ctx.FormString("description")
helpURL := ctx.FormString("help_url")
supportURL := ctx.FormString("support_url")
author := ctx.FormString("author")
// Update in-memory settings
if appName != "" {
setting.AppName = appName
}
if description != "" {
setting.UI.Meta.Description = description
}
setting.HelpURL = helpURL
setting.SupportURL = supportURL
if author != "" {
setting.UI.Meta.Author = author
}
// Persist to app.ini
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
ctx.Flash.Error("Failed to load config: " + err.Error())
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
return
}
if appName != "" {
cfg.Section("").Key("APP_NAME").SetValue(appName)
}
if description != "" {
cfg.Section("ui.meta").Key("DESCRIPTION").SetValue(description)
}
cfg.Section("").Key("HELP_URL").SetValue(helpURL)
cfg.Section("").Key("SUPPORT_URL").SetValue(supportURL)
if author != "" {
cfg.Section("ui.meta").Key("AUTHOR").SetValue(author)
}
if err := cfg.SaveTo(setting.CustomConf); err != nil {
ctx.Flash.Error("Failed to save config: " + err.Error())
log.Error("SaveTo %s: %v", setting.CustomConf, err)
} else {
ctx.Flash.Success("Branding settings saved")
log.Info("Branding settings updated: AppName=%s, Author=%s", appName, author)
}
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
}
// BrandingUpload handles branding image uploads.
func BrandingUpload(ctx *context.Context) {
imageType := ctx.FormString("type")
@@ -48,8 +105,8 @@ func BrandingUpload(ctx *context.Context) {
switch imageType {
case "nav-icon":
filename = "logo-small.png"
case "logo":
filename = "logo.png"
case "login-logo":
filename = "login-logo.png"
case "favicon":
filename = "favicon.png"
default:
@@ -66,14 +123,12 @@ func BrandingUpload(ctx *context.Context) {
}
defer file.Close()
// Validate file size (max 2MB)
if header.Size > 2*1024*1024 {
ctx.Flash.Error("File too large (max 2MB)")
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
return
}
// Ensure the custom image directory exists
imgDir := brandingImageDir()
if err := os.MkdirAll(imgDir, 0o755); err != nil {
ctx.Flash.Error("Failed to create image directory")
@@ -82,7 +137,6 @@ func BrandingUpload(ctx *context.Context) {
return
}
// Write the file
destPath := filepath.Join(imgDir, filename)
dest, err := os.Create(destPath)
if err != nil {
@@ -100,11 +154,10 @@ func BrandingUpload(ctx *context.Context) {
return
}
// Also remove SVG override if present (PNG should take priority)
// Remove SVG override if present
svgPath := filepath.Join(imgDir, filename[:len(filename)-4]+".svg")
if fileExists(svgPath) {
os.Remove(svgPath)
log.Info("Removed SVG override: %s", svgPath)
}
ctx.Flash.Success("Branding image updated: " + imageType)
@@ -112,6 +165,40 @@ func BrandingUpload(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
}
// BrandingReset removes a custom branding image, reverting to the built-in default.
func BrandingReset(ctx *context.Context) {
imageType := ctx.FormString("type")
var filename string
switch imageType {
case "nav-icon":
filename = "logo-small.png"
case "login-logo":
filename = "login-logo.png"
case "favicon":
filename = "favicon.png"
default:
ctx.Flash.Error("Invalid image type")
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
return
}
path := filepath.Join(brandingImageDir(), filename)
if fileExists(path) {
if err := os.Remove(path); err != nil {
ctx.Flash.Error("Failed to remove custom image")
log.Error("Remove %s: %v", path, err)
} else {
ctx.Flash.Success("Reset to default: " + imageType)
log.Info("Branding reset to default: %s", filename)
}
} else {
ctx.Flash.Info("Already using default: " + imageType)
}
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
}
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
+104 -58
View File
@@ -4,67 +4,113 @@
{{svg "octicon-paintbrush" 16}} Branding
</h4>
<div class="ui attached segment">
<p>Upload custom branding images. Changes take effect immediately.</p>
<!-- Nav Icon -->
<div class="ui segment">
<div class="tw-flex tw-items-center tw-gap-4 tw-mb-4">
<img src="{{AssetUrlPrefix}}/img/logo-small.png?v={{ctx.CspScriptNonce}}" style="width: 48px; height: 48px; object-fit: contain;" onerror="this.src='{{AssetUrlPrefix}}/img/logo.png'">
<div>
<strong>Nav Icon</strong>
<div class="tw-text-text-light">Top-left corner, 30x30px recommended</div>
</div>
{{if .HasNavIcon}}<span class="ui green label">Custom</span>{{else}}<span class="ui grey label">Default</span>{{end}}
</div>
<form method="post" action="{{AppSubUrl}}/-/admin/branding/upload" enctype="multipart/form-data">
{{.CsrfTokenHtml}}
<input type="hidden" name="type" value="nav-icon">
<div class="tw-flex tw-gap-2">
<input type="file" name="file" accept="image/png,image/svg+xml" required>
<button type="submit" class="ui primary small button">{{svg "octicon-upload" 14}} Upload</button>
</div>
</form>
</div>
<!-- Text Branding -->
<h5>Identity</h5>
<form method="post" action="{{AppSubUrl}}/-/admin/branding/settings">
{{.CsrfTokenHtml}}
<table class="ui very basic table">
<tbody>
<tr>
<td style="width: 30%;"><strong>Application Name</strong><div class="tw-text-text-light tw-text-sm">Shown in page titles, emails, and footer</div></td>
<td><input type="text" name="app_name" value="{{AppName}}" class="tw-w-full" placeholder="MokoGitea"></td>
</tr>
<tr>
<td><strong>Description</strong><div class="tw-text-text-light tw-text-sm">Meta description for SEO and social sharing</div></td>
<td><input type="text" name="description" value="{{.MetaDescription}}" class="tw-w-full" placeholder="Self-hosted Git service"></td>
</tr>
<tr>
<td><strong>Help URL</strong><div class="tw-text-text-light tw-text-sm">Knowledge base or documentation link shown in help menus</div></td>
<td><input type="text" name="help_url" value="{{.HelpURL}}" class="tw-w-full" placeholder="https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/wiki"></td>
</tr>
<tr>
<td><strong>Support URL</strong><div class="tw-text-text-light tw-text-sm">Ticket system, email, or contact page for user support requests</div></td>
<td><input type="text" name="support_url" value="{{.SupportURL}}" class="tw-w-full" placeholder="https://mokoconsulting.tech/support"></td>
</tr>
<tr>
<td><strong>Author</strong><div class="tw-text-text-light tw-text-sm">Meta author tag</div></td>
<td><input type="text" name="author" value="{{.MetaAuthor}}" class="tw-w-full" placeholder="Moko Consulting"></td>
</tr>
</tbody>
</table>
<button type="submit" class="ui primary small button tw-mt-2">{{svg "octicon-check" 14}} Save Settings</button>
</form>
<!-- Login Logo -->
<div class="ui segment">
<div class="tw-flex tw-items-center tw-gap-4 tw-mb-4">
<img src="{{AssetUrlPrefix}}/img/logo.png?v={{ctx.CspScriptNonce}}" style="max-width: 120px; max-height: 48px; object-fit: contain;">
<div>
<strong>Login Logo</strong>
<div class="tw-text-text-light">Login page and homepage, wide format recommended</div>
</div>
{{if .HasLogo}}<span class="ui green label">Custom</span>{{else}}<span class="ui grey label">Default</span>{{end}}
</div>
<form method="post" action="{{AppSubUrl}}/-/admin/branding/upload" enctype="multipart/form-data">
{{.CsrfTokenHtml}}
<input type="hidden" name="type" value="logo">
<div class="tw-flex tw-gap-2">
<input type="file" name="file" accept="image/png,image/svg+xml" required>
<button type="submit" class="ui primary small button">{{svg "octicon-upload" 14}} Upload</button>
</div>
</form>
</div>
<div class="ui divider"></div>
<!-- Favicon -->
<div class="ui segment">
<div class="tw-flex tw-items-center tw-gap-4 tw-mb-4">
<img src="{{AssetUrlPrefix}}/img/favicon.png?v={{ctx.CspScriptNonce}}" style="width: 48px; height: 48px; object-fit: contain;">
<div>
<strong>Favicon</strong>
<div class="tw-text-text-light">Browser tab icon, 256x256px recommended</div>
</div>
{{if .HasFavicon}}<span class="ui green label">Custom</span>{{else}}<span class="ui grey label">Default</span>{{end}}
</div>
<form method="post" action="{{AppSubUrl}}/-/admin/branding/upload" enctype="multipart/form-data">
{{.CsrfTokenHtml}}
<input type="hidden" name="type" value="favicon">
<div class="tw-flex tw-gap-2">
<input type="file" name="file" accept="image/png,image/svg+xml,image/x-icon" required>
<button type="submit" class="ui primary small button">{{svg "octicon-upload" 14}} Upload</button>
</div>
</form>
</div>
<!-- Image Branding -->
<h5>Images</h5>
<p class="tw-text-text-light tw-text-sm">Changes take effect immediately.</p>
<table class="ui very basic table">
<thead>
<tr>
<th style="width: 30%;">Setting</th>
<th style="width: 40%;">Upload</th>
<th style="width: 30%;">Preview</th>
</tr>
</thead>
<tbody>
<!-- Nav Icon -->
<tr>
<td>
<strong>Nav Icon</strong> {{if .HasNavIcon}}<span class="ui mini green label">Custom</span>{{else}}<span class="ui mini grey label">Default</span>{{end}}
<div class="tw-text-text-light tw-text-sm tw-mt-1">Top-left corner across all pages. Square, 30x30px.</div>
</td>
<td>
<form method="post" action="{{AppSubUrl}}/-/admin/branding/upload" enctype="multipart/form-data">
{{.CsrfTokenHtml}}
<input type="hidden" name="type" value="nav-icon">
<input type="file" name="file" accept="image/png,image/svg+xml" required class="tw-mb-2" style="max-width: 100%;">
<br><button type="submit" class="ui primary mini button">{{svg "octicon-upload" 12}} Upload</button>
{{if .HasNavIcon}}<a href="{{AppSubUrl}}/-/admin/branding/reset?type=nav-icon" class="ui mini button tw-ml-2">{{svg "octicon-sync" 12}} Reset</a>{{end}}
</form>
</td>
<td class="tw-text-center" style="background: var(--color-secondary); border-radius: var(--border-radius);">
<img src="{{AssetUrlPrefix}}/img/logo-small.png?v={{ctx.CspScriptNonce}}" style="max-height: 48px; max-width: 48px; object-fit: contain;" onerror="this.src='{{AssetUrlPrefix}}/img/logo.png'">
</td>
</tr>
<!-- Login Logo -->
<tr>
<td>
<strong>Login Logo</strong> {{if .HasLoginLogo}}<span class="ui mini green label">Custom</span>{{else}}<span class="ui mini grey label">None</span>{{end}}
<div class="tw-text-text-light tw-text-sm tw-mt-1">Login page and homepage. Wide format, max 220px. Hidden when not set.</div>
</td>
<td>
<form method="post" action="{{AppSubUrl}}/-/admin/branding/upload" enctype="multipart/form-data">
{{.CsrfTokenHtml}}
<input type="hidden" name="type" value="login-logo">
<input type="file" name="file" accept="image/png,image/svg+xml" required class="tw-mb-2" style="max-width: 100%;">
<br><button type="submit" class="ui primary mini button">{{svg "octicon-upload" 12}} Upload</button>
{{if .HasLoginLogo}}<a href="{{AppSubUrl}}/-/admin/branding/reset?type=login-logo" class="ui mini button tw-ml-2">{{svg "octicon-sync" 12}} Reset</a>{{end}}
</form>
</td>
<td class="tw-text-center" style="background: var(--color-secondary); border-radius: var(--border-radius);">
{{if .HasLoginLogo}}<img src="{{AssetUrlPrefix}}/img/login-logo.png?v={{ctx.CspScriptNonce}}" style="max-height: 48px; max-width: 140px; object-fit: contain;">{{else}}<span class="tw-text-text-light">Not set</span>{{end}}
</td>
</tr>
<!-- Favicon -->
<tr>
<td>
<strong>Favicon</strong> {{if .HasFavicon}}<span class="ui mini green label">Custom</span>{{else}}<span class="ui mini grey label">Default</span>{{end}}
<div class="tw-text-text-light tw-text-sm tw-mt-1">Browser tab and PWA app icon. Square, 256x256px.</div>
</td>
<td>
<form method="post" action="{{AppSubUrl}}/-/admin/branding/upload" enctype="multipart/form-data">
{{.CsrfTokenHtml}}
<input type="hidden" name="type" value="favicon">
<input type="file" name="file" accept="image/png,image/svg+xml,image/x-icon" required class="tw-mb-2" style="max-width: 100%;">
<br><button type="submit" class="ui primary mini button">{{svg "octicon-upload" 12}} Upload</button>
{{if .HasFavicon}}<a href="{{AppSubUrl}}/-/admin/branding/reset?type=favicon" class="ui mini button tw-ml-2">{{svg "octicon-sync" 12}} Reset</a>{{end}}
</form>
</td>
<td class="tw-text-center" style="background: var(--color-secondary); border-radius: var(--border-radius);">
<img src="{{AssetUrlPrefix}}/img/favicon.png?v={{ctx.CspScriptNonce}}" style="max-height: 48px; max-width: 48px; object-fit: contain;">
</td>
</tr>
</tbody>
</table>
</div>
</div>
{{template "admin/layout_footer" .}}
+1 -1
View File
@@ -2,7 +2,7 @@
<div role="main" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home_title"}}{{end}}" class="page-content home">
<div class="tw-mb-8 tw-px-8">
<div class="center">
<img class="logo" width="220" height="220" src="{{AssetUrlPrefix}}/img/logo.png" alt="{{ctx.Locale.Tr "logo"}}">
<img class="logo" width="220" height="220" src="{{AssetUrlPrefix}}/img/login-logo.png" alt="{{ctx.Locale.Tr "logo"}}" onerror="this.style.display='none'">
<div class="hero">
<h1 class="ui icon header title tw-text-balance">
{{AppName}}