Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d541a07263 | |||
| 4ef4aeb04a | |||
| 4ec61ec260 | |||
| b5defc2a4a | |||
| d77713dd77 | |||
| a45be34139 | |||
| d97955394f | |||
| 592a71968f | |||
| d55b79a9ff | |||
| 64e1e37e20 | |||
| a847129f9c | |||
| 30e16cccc1 | |||
| b74cf800ef | |||
| 90f612f211 |
@@ -8,7 +8,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version tag (e.g. v1.26.1-moko.04.00.00)'
|
||||
description: 'Version tag (e.g. v1.26.1-moko.05.01.00)'
|
||||
required: true
|
||||
default: 'latest'
|
||||
environment:
|
||||
@@ -30,6 +30,7 @@ env:
|
||||
DEPLOY_HOST: git.mokoconsulting.tech
|
||||
DEPLOY_PORT: 2918
|
||||
DEPLOY_USER: mokoconsulting
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
@@ -47,15 +48,30 @@ jobs:
|
||||
echo "source_dir=/opt/gitea/source" >> $GITHUB_OUTPUT
|
||||
echo "branch=main" >> $GITHUB_OUTPUT
|
||||
echo "tag=${VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "instance_url=https://git.mokoconsulting.tech" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "compose_dir=/opt/gitea-dev" >> $GITHUB_OUTPUT
|
||||
echo "container=mokogitea-dev" >> $GITHUB_OUTPUT
|
||||
echo "source_dir=/opt/gitea-dev/source" >> $GITHUB_OUTPUT
|
||||
echo "branch=dev" >> $GITHUB_OUTPUT
|
||||
echo "tag=${VERSION}-dev" >> $GITHUB_OUTPUT
|
||||
echo "instance_url=https://git.dev.mokoconsulting.tech" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Build, push, and deploy via SSH
|
||||
- name: Enable maintenance mode
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
|
||||
run: |
|
||||
echo "Enabling maintenance mode on ${INSTANCE_URL}..."
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
"${INSTANCE_URL}/-/admin/config" \
|
||||
-d 'key=instance.maintenance_mode&value={"AdminWebAccessOnly":true}' \
|
||||
|| echo "WARNING: Could not enable maintenance mode (instance may be down)"
|
||||
|
||||
- name: Build and deploy via SSH
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
TAG: ${{ steps.config.outputs.tag }}
|
||||
@@ -124,6 +140,92 @@ jobs:
|
||||
exit 1
|
||||
"
|
||||
|
||||
- name: Update updates.xml
|
||||
if: success()
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||
TAG: ${{ steps.config.outputs.tag }}
|
||||
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
|
||||
DEPLOY_ENV: ${{ github.event.inputs.environment }}
|
||||
run: |
|
||||
# Only update updates.xml for production stable releases
|
||||
if [ "$DEPLOY_ENV" != "production" ]; then
|
||||
echo "Skipping updates.xml — dev deployments don't update stable channel"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Extract moko version from tag (e.g. v1.26.1-moko.05.01.01 -> 05.01.01)
|
||||
MOKO_VER=$(echo "$TAG" | sed -n 's/.*-moko\.\(.*\)/\1/p')
|
||||
if [ -z "$MOKO_VER" ]; then
|
||||
echo "Could not extract moko version from tag: $TAG"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
RELEASE_URL="https://${REGISTRY}/MokoConsulting/MokoGitea/releases/tag/${TAG}"
|
||||
DOCKER_IMG="${REGISTRY}/${IMAGE}:${TAG}"
|
||||
|
||||
python3 << PYEOF
|
||||
import json, os, re, base64, urllib.request
|
||||
|
||||
token = os.environ["GITEA_TOKEN"]
|
||||
registry = os.environ["REGISTRY"]
|
||||
tag = os.environ["TAG"]
|
||||
moko_ver = os.environ["MOKO_VER"]
|
||||
release_url = os.environ["RELEASE_URL"]
|
||||
docker_img = os.environ["DOCKER_IMG"]
|
||||
api = f"https://{registry}/api/v1/repos/MokoConsulting/MokoGitea"
|
||||
|
||||
# Fetch current updates.xml
|
||||
req = urllib.request.Request(f"{api}/contents/updates.xml?ref=main",
|
||||
headers={"Authorization": f"token {token}"})
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
data = json.loads(resp.read())
|
||||
sha = data["sha"]
|
||||
content = base64.b64decode(data["content"]).decode("utf-8")
|
||||
|
||||
# Update stable channel version, infourl, and docker tag
|
||||
content = re.sub(
|
||||
r"(<tags><tag>stable</tag></tags>[\s\S]*?<version>)[^<]*(</version>)",
|
||||
rf"\g<1>{moko_ver}\2", content)
|
||||
content = re.sub(
|
||||
r"(<tags><tag>stable</tag></tags>[\s\S]*?<infourl[^>]*>)[^<]*(</infourl>)",
|
||||
rf"\g<1>{release_url}\2", content)
|
||||
content = re.sub(
|
||||
r"(<tags><tag>stable</tag></tags>[\s\S]*?<downloadurl[^>]*>)[^<]*(</downloadurl>)",
|
||||
rf"\g<1>{docker_img}\2", content)
|
||||
|
||||
# Also update VERSION comment at top
|
||||
content = re.sub(r"VERSION: [^\n]*", f"VERSION: {moko_ver}", content)
|
||||
|
||||
# Push updated file
|
||||
encoded = base64.b64encode(content.encode()).decode()
|
||||
payload = json.dumps({
|
||||
"message": f"chore(ci): update updates.xml to {moko_ver}",
|
||||
"content": encoded,
|
||||
"sha": sha,
|
||||
"branch": "main",
|
||||
}).encode()
|
||||
req = urllib.request.Request(f"{api}/contents/updates.xml",
|
||||
data=payload, method="PUT",
|
||||
headers={"Authorization": f"token {token}", "Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
print(f"updates.xml updated to {moko_ver}")
|
||||
PYEOF
|
||||
|
||||
- name: Disable maintenance mode
|
||||
if: always()
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
|
||||
run: |
|
||||
echo "Disabling maintenance mode on ${INSTANCE_URL}..."
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
"${INSTANCE_URL}/-/admin/config" \
|
||||
-d 'key=instance.maintenance_mode&value={"AdminWebAccessOnly":false}' \
|
||||
|| echo "WARNING: Could not disable maintenance mode"
|
||||
|
||||
- name: Verify
|
||||
run: |
|
||||
sleep 5
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -18,6 +18,7 @@ import (
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context/upload"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/convert"
|
||||
release_service "git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/release"
|
||||
)
|
||||
|
||||
func checkReleaseMatchRepo(ctx *context.APIContext, releaseID int64) bool {
|
||||
@@ -263,6 +264,14 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
// Regenerate checksums after new attachment
|
||||
rel, relErr := repo_model.GetReleaseByID(ctx, releaseID)
|
||||
if relErr == nil {
|
||||
if checksumErr := release_service.GenerateReleaseChecksums(ctx, rel); checksumErr != nil {
|
||||
log.Error("GenerateReleaseChecksums after upload: %v", checksumErr)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attach))
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
)
|
||||
|
||||
const tplBranding templates.TplName = "admin/branding"
|
||||
|
||||
// brandingImageDir returns the path to the custom branding images directory.
|
||||
func brandingImageDir() string {
|
||||
return filepath.Join(setting.CustomPath, "public", "assets", "img")
|
||||
}
|
||||
|
||||
// Branding shows the admin branding page.
|
||||
func Branding(ctx *context.Context) {
|
||||
ctx.Data["Title"] = "Branding"
|
||||
ctx.Data["PageIsAdminBranding"] = true
|
||||
|
||||
imgDir := brandingImageDir()
|
||||
ctx.Data["HasNavIcon"] = fileExists(filepath.Join(imgDir, "logo-small.png"))
|
||||
ctx.Data["HasLogo"] = fileExists(filepath.Join(imgDir, "logo.png"))
|
||||
ctx.Data["HasFavicon"] = fileExists(filepath.Join(imgDir, "favicon.png"))
|
||||
|
||||
ctx.HTML(http.StatusOK, tplBranding)
|
||||
}
|
||||
|
||||
// BrandingUpload handles branding image uploads.
|
||||
func BrandingUpload(ctx *context.Context) {
|
||||
imageType := ctx.FormString("type")
|
||||
if imageType == "" {
|
||||
ctx.Flash.Error("No image type specified")
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||
return
|
||||
}
|
||||
|
||||
var filename string
|
||||
switch imageType {
|
||||
case "nav-icon":
|
||||
filename = "logo-small.png"
|
||||
case "logo":
|
||||
filename = "logo.png"
|
||||
case "favicon":
|
||||
filename = "favicon.png"
|
||||
default:
|
||||
ctx.Flash.Error("Invalid image type: " + imageType)
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||
return
|
||||
}
|
||||
|
||||
file, header, err := ctx.Req.FormFile("file")
|
||||
if err != nil {
|
||||
ctx.Flash.Error("Upload failed: " + err.Error())
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||
return
|
||||
}
|
||||
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")
|
||||
log.Error("MkdirAll %s: %v", imgDir, err)
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||
return
|
||||
}
|
||||
|
||||
// Write the file
|
||||
destPath := filepath.Join(imgDir, filename)
|
||||
dest, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
ctx.Flash.Error("Failed to save image")
|
||||
log.Error("Create %s: %v", destPath, err)
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||
return
|
||||
}
|
||||
defer dest.Close()
|
||||
|
||||
if _, err := io.Copy(dest, file); err != nil {
|
||||
ctx.Flash.Error("Failed to write image")
|
||||
log.Error("Copy to %s: %v", destPath, err)
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||
return
|
||||
}
|
||||
|
||||
// Also remove SVG override if present (PNG should take priority)
|
||||
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)
|
||||
log.Info("Branding image uploaded: %s (%d bytes)", filename, header.Size)
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
@@ -93,7 +93,7 @@ func home(ctx *context.Context, viewRepositories bool) {
|
||||
ListOptions: db.ListOptions{Page: 1, PageSize: 25},
|
||||
}
|
||||
|
||||
members, _, err := organization.FindOrgMembers(ctx, opts)
|
||||
members, membersIsPublic, err := organization.FindOrgMembers(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("FindOrgMembers", err)
|
||||
return
|
||||
@@ -102,6 +102,14 @@ func home(ctx *context.Context, viewRepositories bool) {
|
||||
const orgOverviewTeamsLimit = 5
|
||||
ctx.Data["OrgOverviewMembers"] = members
|
||||
ctx.Data["OrgOverviewTeams"] = ctx.Org.Teams[:min(len(ctx.Org.Teams), orgOverviewTeamsLimit)]
|
||||
ctx.Data["Members"] = members
|
||||
ctx.Data["NumMembers"] = len(members)
|
||||
ctx.Data["Teams"] = ctx.Org.Teams
|
||||
ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember
|
||||
ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner
|
||||
ctx.Data["IsPublicMember"] = func(uid int64) bool {
|
||||
return membersIsPublic[uid]
|
||||
}
|
||||
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
|
||||
ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0
|
||||
|
||||
|
||||
@@ -765,6 +765,11 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Get("/settings", admin.ConfigSettings)
|
||||
})
|
||||
|
||||
m.Group("/branding", func() {
|
||||
m.Get("", admin.Branding)
|
||||
m.Post("/upload", admin.BrandingUpload)
|
||||
})
|
||||
|
||||
m.Group("/monitor", func() {
|
||||
m.Get("/stats", admin.MonitorStats)
|
||||
m.Get("/cron", admin.CronTasks)
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package release
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
repo_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/storage"
|
||||
attachment_service "git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/attachment"
|
||||
)
|
||||
|
||||
// GenerateReleaseChecksums computes SHA256 checksums for all attachments
|
||||
// on a release and adds a checksums.sha256 manifest file as an attachment.
|
||||
func GenerateReleaseChecksums(ctx context.Context, rel *repo_model.Release) error {
|
||||
// Load attachments into rel.Attachments
|
||||
if err := repo_model.GetReleaseAttachments(ctx, rel); err != nil {
|
||||
return fmt.Errorf("GetReleaseAttachments: %w", err)
|
||||
}
|
||||
|
||||
if len(rel.Attachments) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove existing checksums file if present
|
||||
for _, a := range rel.Attachments {
|
||||
if a.Name == "checksums.sha256" {
|
||||
if err := repo_model.DeleteAttachment(ctx, a, true); err != nil {
|
||||
log.Warn("Failed to delete old checksums.sha256: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Compute SHA256 for each attachment
|
||||
var manifest bytes.Buffer
|
||||
for _, a := range rel.Attachments {
|
||||
if a.Name == "checksums.sha256" {
|
||||
continue
|
||||
}
|
||||
|
||||
fr, err := storage.Attachments.Open(a.RelativePath())
|
||||
if err != nil {
|
||||
log.Warn("Cannot open attachment %s for checksumming: %v", a.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, fr); err != nil {
|
||||
fr.Close()
|
||||
log.Warn("Cannot read attachment %s for checksumming: %v", a.Name, err)
|
||||
continue
|
||||
}
|
||||
fr.Close()
|
||||
|
||||
fmt.Fprintf(&manifest, "%x %s\n", h.Sum(nil), a.Name)
|
||||
}
|
||||
|
||||
if manifest.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create the checksums.sha256 attachment
|
||||
checksumAttach := &repo_model.Attachment{
|
||||
RepoID: rel.RepoID,
|
||||
ReleaseID: rel.ID,
|
||||
Name: "checksums.sha256",
|
||||
}
|
||||
|
||||
if _, err := attachment_service.NewAttachment(ctx, checksumAttach, &manifest, int64(manifest.Len())); err != nil {
|
||||
return fmt.Errorf("create checksums.sha256 attachment: %w", err)
|
||||
}
|
||||
|
||||
log.Info("Generated checksums.sha256 for release %s (repo %d)", rel.TagName, rel.RepoID)
|
||||
return nil
|
||||
}
|
||||
@@ -190,6 +190,13 @@ func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, attachmentU
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate SHA256 checksums for all release attachments
|
||||
if len(attachmentUUIDs) > 0 {
|
||||
if err := GenerateReleaseChecksums(gitRepo.Ctx, rel); err != nil {
|
||||
log.Error("GenerateReleaseChecksums for %s: %v", rel.TagName, err)
|
||||
}
|
||||
}
|
||||
|
||||
if !rel.IsDraft {
|
||||
notify_service.NewRelease(gitRepo.Ctx, rel)
|
||||
}
|
||||
@@ -344,6 +351,13 @@ func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repo
|
||||
}
|
||||
}
|
||||
|
||||
// Regenerate checksums when attachments change
|
||||
if len(addAttachmentUUIDs) > 0 || len(delAttachmentUUIDs) > 0 {
|
||||
if err := GenerateReleaseChecksums(ctx, rel); err != nil {
|
||||
log.Error("GenerateReleaseChecksums for %s: %v", rel.TagName, err)
|
||||
}
|
||||
}
|
||||
|
||||
if !rel.IsDraft {
|
||||
if !isTagCreated && !isConvertedFromTag {
|
||||
notify_service.UpdateRelease(gitRepo.Ctx, doer, rel)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
{{template "admin/layout_head" (dict "pageClass" "admin branding")}}
|
||||
<div class="admin-setting-content">
|
||||
<h4 class="ui top attached header">
|
||||
{{svg "octicon-paintbrush" 16}} Branding
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<p>Upload custom branding images. Changes take effect immediately — no restart required.</p>
|
||||
|
||||
<div class="ui three stackable cards">
|
||||
<!-- Nav Icon -->
|
||||
<div class="card">
|
||||
<div class="content">
|
||||
<div class="header">Nav Icon</div>
|
||||
<div class="meta">Top-left corner (30x30px recommended)</div>
|
||||
</div>
|
||||
<div class="image tw-p-4 tw-text-center" style="background: var(--color-body);">
|
||||
<img src="{{AssetUrlPrefix}}/img/logo-small.png?v={{ctx.CspScriptNonce}}" style="max-height: 64px;" onerror="this.src='{{AssetUrlPrefix}}/img/logo.png'">
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<form method="post" action="{{AppSubUrl}}/-/admin/branding/upload" enctype="multipart/form-data">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="type" value="nav-icon">
|
||||
<div class="ui action input tw-w-full">
|
||||
<input type="file" name="file" accept="image/png,image/svg+xml" required>
|
||||
<button type="submit" class="ui primary button">{{svg "octicon-upload" 14}} Upload</button>
|
||||
</div>
|
||||
</form>
|
||||
{{if .HasNavIcon}}<span class="ui green label tw-mt-2">Custom</span>{{else}}<span class="ui grey label tw-mt-2">Default</span>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Login Logo -->
|
||||
<div class="card">
|
||||
<div class="content">
|
||||
<div class="header">Login Logo</div>
|
||||
<div class="meta">Login page and homepage (wide format recommended)</div>
|
||||
</div>
|
||||
<div class="image tw-p-4 tw-text-center" style="background: var(--color-body);">
|
||||
<img src="{{AssetUrlPrefix}}/img/logo.png?v={{ctx.CspScriptNonce}}" style="max-height: 64px;">
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<form method="post" action="{{AppSubUrl}}/-/admin/branding/upload" enctype="multipart/form-data">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="type" value="logo">
|
||||
<div class="ui action input tw-w-full">
|
||||
<input type="file" name="file" accept="image/png,image/svg+xml" required>
|
||||
<button type="submit" class="ui primary button">{{svg "octicon-upload" 14}} Upload</button>
|
||||
</div>
|
||||
</form>
|
||||
{{if .HasLogo}}<span class="ui green label tw-mt-2">Custom</span>{{else}}<span class="ui grey label tw-mt-2">Default</span>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Favicon -->
|
||||
<div class="card">
|
||||
<div class="content">
|
||||
<div class="header">Favicon</div>
|
||||
<div class="meta">Browser tab icon (256x256px recommended)</div>
|
||||
</div>
|
||||
<div class="image tw-p-4 tw-text-center" style="background: var(--color-body);">
|
||||
<img src="{{AssetUrlPrefix}}/img/favicon.png?v={{ctx.CspScriptNonce}}" style="max-height: 64px;">
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<form method="post" action="{{AppSubUrl}}/-/admin/branding/upload" enctype="multipart/form-data">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="type" value="favicon">
|
||||
<div class="ui action input tw-w-full">
|
||||
<input type="file" name="file" accept="image/png,image/svg+xml,image/x-icon" required>
|
||||
<button type="submit" class="ui primary button">{{svg "octicon-upload" 14}} Upload</button>
|
||||
</div>
|
||||
</form>
|
||||
{{if .HasFavicon}}<span class="ui green label tw-mt-2">Custom</span>{{else}}<span class="ui grey label tw-mt-2">Default</span>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "admin/layout_tail" .}}
|
||||
@@ -84,6 +84,9 @@
|
||||
</div>
|
||||
</details>
|
||||
{{end}}
|
||||
<a class="{{if .PageIsAdminBranding}}active {{end}}item" href="{{AppSubUrl}}/-/admin/branding">
|
||||
{{svg "octicon-paintbrush" 16}} Branding
|
||||
</a>
|
||||
<details class="item toggleable-item" {{if or .PageIsAdminConfig}}open{{end}}>
|
||||
<summary>{{ctx.Locale.Tr "admin.config"}}</summary>
|
||||
<div class="menu">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="navbar-left">
|
||||
<!-- the logo -->
|
||||
<a class="item" id="navbar-logo" href="{{AppSubUrl}}/" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home_title"}}{{end}}">
|
||||
<img width="30" height="30" src="https://mokoconsulting.tech/images/branding/logo.png" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
|
||||
<img width="30" height="30" src="{{AssetUrlPrefix}}/img/logo-small.png" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true" onerror="this.src='{{AssetUrlPrefix}}/img/logo.png'">
|
||||
</a>
|
||||
|
||||
<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column -->
|
||||
|
||||
+1
-1
@@ -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="https://mokoconsulting.tech/images/branding/logo.png" alt="{{ctx.Locale.Tr "logo"}}">
|
||||
<img class="logo" width="220" height="220" src="{{AssetUrlPrefix}}/img/logo.png" alt="{{ctx.Locale.Tr "logo"}}">
|
||||
<div class="hero">
|
||||
<h1 class="ui icon header title tw-text-balance">
|
||||
{{AppName}}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<div class="ui container tw-flex">
|
||||
<div class="item tw-flex-1">
|
||||
<a href="{{AppSubUrl}}/" aria-label="{{ctx.Locale.Tr "home_title"}}">
|
||||
<img width="30" height="30" src="https://mokoconsulting.tech/images/branding/logo.png" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
|
||||
<img width="30" height="30" src="{{AssetUrlPrefix}}/img/logo-small.png" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
|
||||
</a>
|
||||
</div>
|
||||
<div class="item">
|
||||
|
||||
@@ -9,8 +9,8 @@ details.toggleable-item .menu {
|
||||
|
||||
details.toggleable-item summary {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
padding: 0.92857143em 1.14285714em;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ details.toggleable-item summary::-webkit-details-marker /* Safari */ {
|
||||
}
|
||||
|
||||
details.toggleable-item summary::after {
|
||||
margin-left: auto;
|
||||
transition: transform 0.25s ease;
|
||||
content: "";
|
||||
width: 14px;
|
||||
@@ -35,3 +36,8 @@ details.toggleable-item summary::after {
|
||||
details.toggleable-item[open] summary::after {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.flex-container-nav .ui.menu .item {
|
||||
text-align: left !important;
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user