This commit is contained in:
Krzysztof Kotlarek
2025-02-05 10:48:44 +11:00
parent 43e8172ebc
commit b024054eac
15 changed files with 512 additions and 35 deletions

View File

@@ -0,0 +1,220 @@
import Component from "@glimmer/component";
import { cached } from "@glimmer/tracking";
import { tracked } from "@glimmer/tracking";
import { hash } from "@ember/helper";
import { fn } from "@ember/helper";
import { action } from "@ember/object";
import { service } from "@ember/service";
import BackButton from "discourse/components/back-button";
import DButton from "discourse/components/d-button";
import Form from "discourse/components/form";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { bind } from "discourse/lib/decorators";
import getURL from "discourse/lib/get-url";
import { i18n } from "discourse-i18n";
import AdminConfigAreaCardSection from "admin/components/admin-config-area-card-section";
import SimpleList from "admin/components/simple-list";
export default class AdminBrandingLogoForm extends Component {
@service router;
@service site;
@action
handleUpload(type, upload, { set }) {
if (upload) {
set(type, getURL(upload.url));
} else {
set(type, undefined);
}
}
<template>
<@form.Field
@name="logo"
@title={{i18n "admin.config.branding.logo.form.logo.title"}}
@description={{i18n "admin.config.branding.logo.form.logo.description"}}
@helpText={{i18n "admin.config.branding.logo.form.logo.help_text"}}
@onSet={{(fn this.handleUpload "logo")}}
as |field|
>
<field.Image @type="branding" />
</@form.Field>
<@form.Field
@name="logo_dark_required"
@title={{i18n "admin.config.branding.logo.form.logo_dark.required"}}
@format="full"
as |field|
>
<field.Toggle />
</@form.Field>
{{#if @transientData.logo_dark_required}}
<@form.Field
@name="logo_dark"
@title={{i18n "admin.config.branding.logo.form.logo_dark.title"}}
@helpText={{i18n "admin.config.branding.logo.form.logo_dark.help_text"}}
@onSet={{(fn this.handleUpload "logo_dark")}}
as |field|
>
<field.Image @type="branding" />
</@form.Field>
{{/if}}
<@form.Field
@name="large_icon"
@title={{i18n "admin.config.branding.logo.form.large_icon.title"}}
@description={{i18n
"admin.config.branding.logo.form.large_icon.description"
}}
@helpText={{i18n "admin.config.branding.logo.form.large_icon.help_text"}}
@onSet={{(fn this.handleUpload "large_icon")}}
as |field|
>
<field.Image @type="branding" />
</@form.Field>
<@form.Field
@name="favicon"
@title={{i18n "admin.config.branding.logo.form.favicon.title"}}
@description={{i18n
"admin.config.branding.logo.form.favicon.description"
}}
@onSet={{(fn this.handleUpload "square_icon_light")}}
as |field|
>
<field.Image @type="branding" />
</@form.Field>
<@form.Field
@name="logo_small"
@title={{i18n "admin.config.branding.logo.form.logo_small.title"}}
@description={{i18n
"admin.config.branding.logo.form.logo_small.description"
}}
@helpText={{i18n "admin.config.branding.logo.form.logo_small.help_text"}}
@onSet={{(fn this.handleUpload "logo_small")}}
as |field|
>
<field.Image @type="branding" />
</@form.Field>
<@form.Field
@name="logo_small_dark_required"
@title={{i18n "admin.config.branding.logo.form.logo_small_dark.required"}}
@format="full"
as |field|
>
<field.Toggle />
</@form.Field>
{{#if @transientData.logo_small_dark_required}}
<@form.Field
@name="logo_small_dark"
@title={{i18n "admin.config.branding.logo.form.logo_small_dark.title"}}
@helpText={{i18n
"admin.config.branding.logo.form.logo_small_dark.help_text"
}}
@onSet={{(fn this.handleUpload "logo_small_dark")}}
as |field|
>
<field.Image @type="branding" />
</@form.Field>
{{/if}}
<AdminConfigAreaCardSection
@heading="admin.config.branding.logo.form.mobile"
@collapsable={{true}}
@collapsed={{true}}
>
<:content>
<@form.Field
@name="mobile_logo"
@title={{i18n "admin.config.branding.logo.form.mobile_logo.title"}}
@description={{i18n
"admin.config.branding.logo.form.mobile_logo.description"
}}
@helpText={{i18n
"admin.config.branding.logo.form.mobile_logo.help_text"
}}
@onSet={{(fn this.handleUpload "mobile_logo")}}
as |field|
>
<field.Image @type="branding" />
</@form.Field>
<@form.Field
@name="mobile_logo_dark_required"
@title={{i18n
"admin.config.branding.logo.form.mobile_logo_dark.required"
}}
@format="full"
as |field|
>
<field.Toggle />
</@form.Field>
{{#if @transientData.mobile_logo_dark_required}}
<@form.Field
@name="mobile_logo_dark"
@title={{i18n
"admin.config.branding.logo.form.mobile_logo_dark.title"
}}
@helpText={{i18n
"admin.config.branding.logo.form.mobile_logo_dark.help_text"
}}
@onSet={{(fn this.handleUpload "mobile_logo_dark")}}
as |field|
>
<field.Image @type="branding" />
</@form.Field>
{{/if}}
<@form.Field
@name="manifest_icon"
@title={{i18n "admin.config.branding.logo.form.manifest_icon.title"}}
@description={{i18n
"admin.config.branding.logo.form.manifest_icon.description"
}}
@helpText={{i18n
"admin.config.branding.logo.form.manifest_icon.help_text"
}}
@onSet={{(fn this.handleUpload "manifest_icon")}}
as |field|
>
<field.Image @type="branding" />
</@form.Field>
<@form.Field
@name="manifest_screenshots"
@title={{i18n
"admin.config.branding.logo.form.manifest_screenshots.title"
}}
@description={{i18n
"admin.config.branding.logo.form.manifest_screenshots.description"
}}
@format="full"
as |field|
>
<field.Custom>
<SimpleList
@id={{field.id}}
@onChange={{field.set}}
@inputDelimiter="|"
@values={{field.value}}
@allowAny={{true}}
/>
</field.Custom>
</@form.Field>
</:content>
</AdminConfigAreaCardSection>
<AdminConfigAreaCardSection
@heading="admin.config.branding.logo.form.email"
@collapsable={{true}}
@collapsed={{true}}
>
<:content>
fsdfgsdfsdf ds fsdf sdfsd
</:content>
</AdminConfigAreaCardSection>
<AdminConfigAreaCardSection
@heading="admin.config.branding.logo.form.social_media"
@collapsable={{true}}
@collapsed={{true}}
>
<:content>
fsdfgsdfsdf ds fsdf sdfsd
</:content>
</AdminConfigAreaCardSection>
</template>
}

View File

@@ -0,0 +1,48 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import DButton from "discourse/components/d-button";
import icon from "discourse/helpers/d-icon";
import { i18n } from "discourse-i18n";
export default class AdminConfigAreaCardSection extends Component {
@tracked collapsed = this.args.collapsed;
get computedHeading() {
if (this.args.heading) {
return i18n(this.args.heading);
}
return this.args.translatedHeading;
}
get headerCaretIcon() {
return this.collapsed ? "plus" : "minus";
}
@action
toggleSectionDisplay() {
this.collapsed = !this.collapsed;
}
<template>
<section class="admin-config-area-card-section" ...attributes>
<div class="admin-config-area-card-section__header-wrapper">
<h4 class="admin-config-area-card__title">{{this.computedHeading}}</h4>
{{#if @collapsable}}
<DButton
@title="sidebar.toggle_section"
@action={{this.toggleSectionDisplay}}
class="admin-config-area-card-section__toggle-button btn-transparent"
>
{{icon this.headerCaretIcon}}
</DButton>
{{/if}}
</div>
{{#unless this.collapsed}}
<div class="admin-config-area-card-section__content">
{{yield to="content"}}
</div>
{{/unless}}
</section>
</template>
}

View File

@@ -0,0 +1,44 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { cached } from "@glimmer/tracking";
import { action } from "@ember/object";
import { service } from "@ember/service";
import Form from "discourse/components/form";
export default class AdminConfigAreasBrandingForm extends Component {
@service siteSettings;
@action
submit(args) {
console.log("args", args);
}
@cached
get formData() {
return {
logo: this.siteSettings.logo,
logo_dark_required: !!this.siteSettings.logo_dark,
logo_dark: this.siteSettings.logo_dark,
large_icon: this.siteSettings.large_icon,
favicon: this.siteSettings.favicon,
logo_small: this.siteSettings.logo_small,
logo_small_dark_required: !!this.siteSettings.logo_small_dark,
logo_small_dark: this.siteSettings.logo_small_dark,
mobile_logo: this.siteSettings.mobile_logo,
mobile_logo_dark_required: !!this.siteSettings.mobile_logo_dark,
mobile_logo_dark: this.siteSettings.mobile_logo_dark,
manifest_icon: this.siteSettings.manifest_icon,
manifest_screenshots: this.siteSettings.manifest_screenshots,
};
}
<template>
<Form
@onSubmit={{this.submit}}
@data={{this.formData}}
as |form transientData|
>
{{yield form transientData to="content"}}
<form.Submit />
</Form>
</template>
}

View File

@@ -316,6 +316,7 @@ export default function () {
this.route("logo", function () {
this.route("settings", { path: "/" });
});
this.route("branding");
this.route("navigation", function () {
this.route("settings", { path: "/" });
});

View File

@@ -0,0 +1,42 @@
import RouteTemplate from "ember-route-template";
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
import DPageHeader from "discourse/components/d-page-header";
import { i18n } from "discourse-i18n";
import AdminBrandingLogoForm from "admin/components/admin-branding-logo-form";
import AdminConfigAreaCard from "admin/components/admin-config-area-card";
import AdminConfigAreasBrandingForm from "admin/components/admin-config-areas/branding-form";
export default RouteTemplate(
<template>
<DPageHeader
@hideTabs={{true}}
@titleLabel={{i18n "admin.config.branding.title"}}
>
<:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/config/branding"
@label={{i18n "admin.config.branding.title"}}
/>
</:breadcrumbs>
</DPageHeader>
<div class="admin-config-area">
<div class="admin-config-area__primary-content">
<AdminConfigAreasBrandingForm>
<:content as |form transientData|>
<AdminConfigAreaCard
@heading="admin.config.branding.logo.title"
@collapsable={{true}}
class="admin-config-area-branding__logo"
>
<:content>
<AdminBrandingLogoForm @form={{form}} @transientData={{transientData}} />
</:content>
</AdminConfigAreaCard>
</:content>
</AdminConfigAreasBrandingForm>
</div>
</div>
</template>
);

View File

@@ -114,6 +114,12 @@ export const ADMIN_NAV_MAP = [
label: "admin.appearance.sidebar_link.font_style",
icon: "italic",
},
{
name: "admin_branding",
route: "adminConfig.branding",
label: "admin.config.branding.title",
icon: "fab-discourse",
},
{
name: "admin_site_logo",
route: "adminConfig.logo.settings",

View File

@@ -1206,6 +1206,7 @@ a.inline-editable-field {
}
// Styles for subtabs in admin
@import "common/admin/branding";
@import "common/admin/dashboard";
@import "common/admin/sidebar";
@import "common/admin/settings";

View File

@@ -122,3 +122,30 @@
margin-top: 1em;
}
}
.admin-config-area-card-section {
display: flex;
flex-wrap: nowrap;
gap: var(--space-4);
flex-direction: column;
width: 100%;
@media (max-width: $mobile-breakpoint) {
flex-direction: column;
}
&__header-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
}
&__title {
margin-bottom: 0;
}
&__content {
margin-top: 0.5rem;
padding-right: 15px;
}
}

View File

@@ -0,0 +1,28 @@
.admin-config-area-branding {
&__logo {
width: 100%;
.admin-config-area-card__content {
display: flex;
flex-direction: column;
gap: 1.5em;
align-items: flex-start;
}
// #control-dark_logo_required {
// width: 100%;
//
// .form-kit__container-title {
// display: flex;
// flex-direction: column;
// }
// }
}
}
.admin-config-area-card-section__content {
display: flex;
flex-direction: column;
gap: 1.5em;
align-items: flex-start;
}

View File

@@ -5,6 +5,10 @@
width: var(--form-kit-medium-input);
}
&.--large {
width: var(--form-kit-large-input);
}
&.--full {
width: 100%;
}

View File

@@ -386,41 +386,42 @@ class Upload < ActiveRecord::Base
color = ""
end
color ||=
begin
data =
Discourse::Utils.execute_command(
"nice",
"-n",
"10",
"convert",
local_path,
"-depth",
"8",
"-resize",
"1x1",
"-define",
"histogram:unique-colors=true",
"-format",
"%c",
"histogram:info:",
timeout: DOMINANT_COLOR_COMMAND_TIMEOUT_SECONDS,
)
# Output format:
# 1: (110.873,116.226,93.8821) #6F745E srgb(43.4798%,45.5789%,36.8165%)
color = data[/#([0-9A-F]{6})/, 1]
raise "Calculated dominant color but unable to parse output:\n#{data}" if color.nil?
color
rescue Discourse::Utils::CommandError => e
# Timeout or unable to parse image
# This can happen due to bad user input - ignore and save
# an empty string to prevent re-evaluation
""
end
# color ||=
# begin
# data =
# Discourse::Utils.execute_command(
# "nice",
# "-n",
# "10",
# "convert",
# local_path,
# "-depth",
# "8",
# "-resize",
# "1x1",
# "-define",
# "histogram:unique-colors=true",
# "-format",
# "%c",
# "histogram:info:",
# timeout: DOMINANT_COLOR_COMMAND_TIMEOUT_SECONDS,
# )
#
# # Output format:
# # 1: (110.873,116.226,93.8821) #6F745E srgb(43.4798%,45.5789%,36.8165%)
#
# color = data[/#([0-9A-F]{6})/, 1]
#
# raise "Calculated dominant color but unable to parse output:\n#{data}" if color.nil?
#
# color
# rescue Discourse::Utils::CommandError => e
# # Timeout or unable to parse image
# # This can happen due to bad user input - ignore and save
# # an empty string to prevent re-evaluation
# ""
# end
"#000000"
end
if persisted?

View File

@@ -5184,6 +5184,56 @@ en:
logo:
title: "Site logo"
header_description: "Customize the variations of your site logo"
branding:
title: "Branding"
logo:
title: "Logo"
form:
logo:
title: "Primary logo"
description: "Appears on the site's top navigation, as well as the top of the site's Email Notifications."
help_text: "Recommended size is 600 x 80 pixels"
logo_dark:
required: "Use a different logo for dark mode?"
title: "Primary logo dark"
help_text: "Recommended size is 600 x 80 pixels"
large_icon:
title: "Square icon"
description: "A squared version of the logo image appears at the top of the administration and is also the mobile home screen app logo."
help_text: "Recommended size is 512 x 512 pixels"
square_icon_dark:
required: "Use a different square icon for dark mode?"
title: "Square icon dark"
help_text: "Recommended size is 512 x 512 pixels"
favicon:
title: "Favicon"
description: "The logo will appear as the icon in the browser tab and the browser favorites/bookmarks."
logo_small:
title: "Small logo"
description: "The small logo image at the top left of your site, seen when scrolling down. If left blank, a home glyph will be shown."
help_text: "Recommended size is 120 x 120 pixels"
logo_small_dark:
required: "Use a different small logo for dark mode?"
title: "Small logo dark"
help_text: "Recommended size is 120 x 120 pixels"
mobile: "Mobile"
email: "Email"
social_media: "Social media"
mobile_logo:
title: "Mobile logo"
description: "The logo used on mobile version of your site. If left blank, the image from the `logo` setting will be used."
help_text: "Use a wide rectangular image with a height of 120 and an aspect ratio greater than 3:1."
mobile_logo_dark:
required: "Use a different mobile logo for dark mode?"
title: "Mobile logo dark"
help_text: "Use a wide rectangular image with a height of 120 and an aspect ratio greater than 3:1."
manifest_icon:
title: "Manifest icon"
description: "Image used as logo/splash image on Android. If left blank, large_icon will be used."
help_text: "Will be automatically resized to 512 × 512."
manifest_screenshots:
title: "Manifest screenshots"
description: "Screenshots that showcase your instance features and functionality on its install prompt page. All images should be local uploads and of the same dimensions."
navigation:
title: "Navigation"
header_description: "Configure the navigation links and menu items for your site. This includes the location and behavior of the primary navigation menu, the quick links at the top of the homepage, as well as the admin sidebar"

View File

@@ -409,6 +409,9 @@ Discourse::Application.routes.draw do
get "trust-levels" => "site_settings#index"
get "group-permissions" => "site_settings#index"
#TODO
get "branding" => "site_settings#index"
resources :flags, only: %i[index new create update destroy] do
put "toggle"
put "reorder/:direction" => "flags#reorder"

View File

@@ -125,10 +125,12 @@ branding:
type: upload
manifest_icon:
default: ""
client: true
type: upload
manifest_screenshots:
type: list
list_type: simple
client: true
default: ""
favicon:
default: ""