mirror of
https://github.com/discourse/discourse.git
synced 2025-02-07 20:08:26 +00:00
FEATURE: Add site setting and wizard step to set base font (#10250)
Co-authored-by: Neil Lalonde <neillalonde@gmail.com>
This commit is contained in:
parent
7290ca1f4a
commit
f2e14a3946
2
.gitignore
vendored
2
.gitignore
vendored
@ -87,6 +87,7 @@ config/multisite.yml
|
||||
config/multisite1.yml
|
||||
config/fog_credentials.yml
|
||||
|
||||
/public/fonts
|
||||
/public/uploads
|
||||
/public/backups
|
||||
/public/stylesheet-cache/*
|
||||
@ -131,6 +132,7 @@ node_modules
|
||||
|
||||
# ignore auto-generated plugin js assets
|
||||
/app/assets/javascripts/plugins/*
|
||||
/app/assets/stylesheets/common/fonts.scss
|
||||
|
||||
# ignore generated api documentation files
|
||||
openapi/*
|
||||
|
1
Gemfile
1
Gemfile
@ -69,6 +69,7 @@ gem 'http_accept_language', require: false
|
||||
gem 'ember-rails', '0.18.5'
|
||||
gem 'discourse-ember-source', '~> 3.12.2'
|
||||
gem 'ember-handlebars-template', '0.8.0'
|
||||
gem 'discourse-fonts'
|
||||
|
||||
gem 'barber'
|
||||
|
||||
|
@ -99,6 +99,7 @@ GEM
|
||||
diff-lcs (1.4.4)
|
||||
diffy (3.4.0)
|
||||
discourse-ember-source (3.12.2.2)
|
||||
discourse-fonts (0.0.2)
|
||||
discourse_image_optim (0.26.2)
|
||||
exifr (~> 1.2, >= 1.2.2)
|
||||
fspath (~> 3.0)
|
||||
@ -461,6 +462,7 @@ DEPENDENCIES
|
||||
css_parser
|
||||
diffy
|
||||
discourse-ember-source (~> 3.12.2)
|
||||
discourse-fonts
|
||||
discourse_image_optim
|
||||
email_reply_trimmer
|
||||
ember-handlebars-template (= 0.8.0)
|
||||
|
125
app/assets/javascripts/wizard/components/font-preview.js
Normal file
125
app/assets/javascripts/wizard/components/font-preview.js
Normal file
@ -0,0 +1,125 @@
|
||||
import I18n from "I18n";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { observes } from "discourse-common/utils/decorators";
|
||||
import {
|
||||
createPreviewComponent,
|
||||
darkLightDiff,
|
||||
chooseDarker,
|
||||
LOREM
|
||||
} from "wizard/lib/preview";
|
||||
|
||||
export default createPreviewComponent(305, 165, {
|
||||
logo: null,
|
||||
avatar: null,
|
||||
|
||||
classNameBindings: ["isSelected"],
|
||||
|
||||
@discourseComputed("selectedId", "fontId")
|
||||
isSelected(selectedId, fontId) {
|
||||
return selectedId === fontId;
|
||||
},
|
||||
|
||||
click() {
|
||||
this.onChange(this.fontId);
|
||||
},
|
||||
|
||||
@observes("step.fieldsById.base_scheme_id.value")
|
||||
themeChanged() {
|
||||
this.triggerRepaint();
|
||||
},
|
||||
|
||||
images() {
|
||||
return {
|
||||
logo: this.wizard.getLogoUrl(),
|
||||
avatar: "/images/wizard/trout.png"
|
||||
};
|
||||
},
|
||||
|
||||
paint(ctx, colors, font, width, height) {
|
||||
const headerHeight = height * 0.3;
|
||||
|
||||
this.drawFullHeader(colors, font);
|
||||
|
||||
const margin = width * 0.04;
|
||||
const avatarSize = height * 0.2;
|
||||
const lineHeight = height / 9.5;
|
||||
|
||||
// Draw a fake topic
|
||||
this.scaleImage(
|
||||
this.avatar,
|
||||
margin,
|
||||
headerHeight + height * 0.085,
|
||||
avatarSize,
|
||||
avatarSize
|
||||
);
|
||||
|
||||
const titleFontSize = headerHeight / 44;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = colors.primary;
|
||||
ctx.font = `bold ${titleFontSize}em '${font}'`;
|
||||
ctx.fillText(I18n.t("wizard.previews.topic_title"), margin, height * 0.3);
|
||||
|
||||
const bodyFontSize = height / 220.0;
|
||||
ctx.font = `${bodyFontSize}em '${font}'`;
|
||||
|
||||
let line = 0;
|
||||
const lines = LOREM.split("\n");
|
||||
for (let i = 0; i < 4; i++) {
|
||||
line = height * 0.35 + i * lineHeight;
|
||||
ctx.fillText(lines[i], margin + avatarSize + margin, line);
|
||||
}
|
||||
|
||||
// Share Button
|
||||
ctx.beginPath();
|
||||
ctx.rect(margin, line + lineHeight, width * 0.14, height * 0.14);
|
||||
ctx.fillStyle = darkLightDiff(colors.primary, colors.secondary, 90, 65);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = chooseDarker(colors.primary, colors.secondary);
|
||||
ctx.font = `${bodyFontSize}em '${font}'`;
|
||||
ctx.fillText(
|
||||
I18n.t("wizard.previews.share_button"),
|
||||
margin + width / 55,
|
||||
line + lineHeight * 1.85
|
||||
);
|
||||
|
||||
// Reply Button
|
||||
ctx.beginPath();
|
||||
ctx.rect(
|
||||
margin * 2 + width * 0.14,
|
||||
line + lineHeight,
|
||||
width * 0.14,
|
||||
height * 0.14
|
||||
);
|
||||
ctx.fillStyle = colors.tertiary;
|
||||
ctx.fill();
|
||||
ctx.fillStyle = colors.secondary;
|
||||
ctx.font = `${bodyFontSize}em '${font}'`;
|
||||
ctx.fillText(
|
||||
I18n.t("wizard.previews.reply_button"),
|
||||
margin * 2 + width * 0.14 + width / 55,
|
||||
line + lineHeight * 1.85
|
||||
);
|
||||
|
||||
// Draw Timeline
|
||||
const timelineX = width * 0.8;
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = colors.tertiary;
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.moveTo(timelineX, height * 0.3);
|
||||
ctx.lineTo(timelineX, height * 0.7);
|
||||
ctx.stroke();
|
||||
|
||||
// Timeline
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = colors.tertiary;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.moveTo(timelineX, height * 0.3);
|
||||
ctx.lineTo(timelineX, height * 0.4);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.font = `Bold ${bodyFontSize}em ${font}`;
|
||||
ctx.fillStyle = colors.primary;
|
||||
ctx.fillText("1 / 20", timelineX + margin, height * 0.3 + margin * 1.5);
|
||||
}
|
||||
});
|
@ -0,0 +1,8 @@
|
||||
import Component from "@ember/component";
|
||||
export default Component.extend({
|
||||
actions: {
|
||||
changed(value) {
|
||||
this.set("field.value", value);
|
||||
}
|
||||
}
|
||||
});
|
@ -21,36 +21,36 @@ export default createPreviewComponent(659, 320, {
|
||||
};
|
||||
},
|
||||
|
||||
paint(ctx, colors, width, height) {
|
||||
this.drawFullHeader(colors);
|
||||
paint(ctx, colors, font, width, height) {
|
||||
this.drawFullHeader(colors, font);
|
||||
|
||||
if (this.get("step.fieldsById.homepage_style.value") === "latest") {
|
||||
this.drawPills(colors, height * 0.15);
|
||||
this.renderLatest(ctx, colors, width, height);
|
||||
this.drawPills(colors, font, height * 0.15);
|
||||
this.renderLatest(ctx, colors, font, width, height);
|
||||
} else if (
|
||||
["categories_only", "categories_with_featured_topics"].includes(
|
||||
this.get("step.fieldsById.homepage_style.value")
|
||||
)
|
||||
) {
|
||||
this.drawPills(colors, height * 0.15, { categories: true });
|
||||
this.renderCategories(ctx, colors, width, height);
|
||||
this.drawPills(colors, font, height * 0.15, { categories: true });
|
||||
this.renderCategories(ctx, colors, font, width, height);
|
||||
} else if (
|
||||
["categories_boxes", "categories_boxes_with_topics"].includes(
|
||||
this.get("step.fieldsById.homepage_style.value")
|
||||
)
|
||||
) {
|
||||
this.drawPills(colors, height * 0.15, { categories: true });
|
||||
this.drawPills(colors, font, height * 0.15, { categories: true });
|
||||
const topics =
|
||||
this.get("step.fieldsById.homepage_style.value") ===
|
||||
"categories_boxes_with_topics";
|
||||
this.renderCategoriesBoxes(ctx, colors, width, height, { topics });
|
||||
this.renderCategoriesBoxes(ctx, colors, font, width, height, { topics });
|
||||
} else {
|
||||
this.drawPills(colors, height * 0.15, { categories: true });
|
||||
this.renderCategoriesWithTopics(ctx, colors, width, height);
|
||||
this.drawPills(colors, font, height * 0.15, { categories: true });
|
||||
this.renderCategoriesWithTopics(ctx, colors, font, width, height);
|
||||
}
|
||||
},
|
||||
|
||||
renderCategoriesBoxes(ctx, colors, width, height, opts) {
|
||||
renderCategoriesBoxes(ctx, colors, font, width, height, opts) {
|
||||
opts = opts || {};
|
||||
|
||||
const borderColor = darkLightDiff(
|
||||
@ -83,7 +83,7 @@ export default createPreviewComponent(659, 320, {
|
||||
]
|
||||
);
|
||||
|
||||
ctx.font = `Bold ${bodyFontSize * 1.3}em 'Arial'`;
|
||||
ctx.font = `Bold ${bodyFontSize * 1.3}em '${font}'`;
|
||||
ctx.fillStyle = colors.primary;
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText(category.name, boxStartX + boxWidth / 2, boxStartY + 25);
|
||||
@ -92,7 +92,7 @@ export default createPreviewComponent(659, 320, {
|
||||
if (opts.topics) {
|
||||
let startY = boxStartY + 60;
|
||||
this.getTitles().forEach(title => {
|
||||
ctx.font = `${bodyFontSize * 1}em 'Arial'`;
|
||||
ctx.font = `${bodyFontSize * 1}em '${font}'`;
|
||||
ctx.fillStyle = colors.tertiary;
|
||||
startY +=
|
||||
this.fillTextMultiLine(
|
||||
@ -105,7 +105,7 @@ export default createPreviewComponent(659, 320, {
|
||||
) + 8;
|
||||
});
|
||||
} else {
|
||||
ctx.font = `${bodyFontSize * 1}em 'Arial'`;
|
||||
ctx.font = `${bodyFontSize * 1}em '${font}'`;
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.textAlign = "center";
|
||||
this.fillTextMultiLine(
|
||||
@ -121,7 +121,7 @@ export default createPreviewComponent(659, 320, {
|
||||
});
|
||||
},
|
||||
|
||||
renderCategories(ctx, colors, width, height) {
|
||||
renderCategories(ctx, colors, font, width, height) {
|
||||
const textColor = darkLightDiff(colors.primary, colors.secondary, 50, 50);
|
||||
const margin = height * 0.03;
|
||||
const bodyFontSize = height / 440.0;
|
||||
@ -144,7 +144,7 @@ export default createPreviewComponent(659, 320, {
|
||||
const cols = [0.025, 0.45, 0.53, 0.58, 0.94, 0.96].map(c => c * width);
|
||||
|
||||
const headingY = height * 0.33;
|
||||
ctx.font = `${bodyFontSize * 0.9}em 'Arial'`;
|
||||
ctx.font = `${bodyFontSize * 0.9}em '${font}'`;
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.fillText("Category", cols[0], headingY);
|
||||
if (
|
||||
@ -165,11 +165,11 @@ export default createPreviewComponent(659, 320, {
|
||||
// Categories
|
||||
this.categories().forEach(category => {
|
||||
const textPos = y + categoryHeight * 0.35;
|
||||
ctx.font = `Bold ${bodyFontSize * 1.1}em 'Arial'`;
|
||||
ctx.font = `Bold ${bodyFontSize * 1.1}em '${font}'`;
|
||||
ctx.fillStyle = colors.primary;
|
||||
ctx.fillText(category.name, cols[0], textPos);
|
||||
|
||||
ctx.font = `${bodyFontSize * 0.8}em 'Arial'`;
|
||||
ctx.font = `${bodyFontSize * 0.8}em '${font}'`;
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.fillText(
|
||||
titles[0],
|
||||
@ -188,14 +188,14 @@ export default createPreviewComponent(659, 320, {
|
||||
this.get("step.fieldsById.homepage_style.value") ===
|
||||
"categories_with_featured_topics"
|
||||
) {
|
||||
ctx.font = `${bodyFontSize}em 'Arial'`;
|
||||
ctx.font = `${bodyFontSize}em '${font}'`;
|
||||
ctx.fillText(
|
||||
Math.floor(Math.random() * 90) + 10,
|
||||
cols[1] + 15,
|
||||
textPos
|
||||
);
|
||||
} else {
|
||||
ctx.font = `${bodyFontSize}em 'Arial'`;
|
||||
ctx.font = `${bodyFontSize}em '${font}'`;
|
||||
ctx.fillText(Math.floor(Math.random() * 90) + 10, cols[5], textPos);
|
||||
}
|
||||
|
||||
@ -216,7 +216,7 @@ export default createPreviewComponent(659, 320, {
|
||||
ctx.fillStyle = colors.tertiary;
|
||||
|
||||
titles.forEach(title => {
|
||||
ctx.font = `${bodyFontSize}em 'Arial'`;
|
||||
ctx.font = `${bodyFontSize}em '${font}'`;
|
||||
const textPos = y + topicHeight * 0.35;
|
||||
ctx.fillStyle = colors.tertiary;
|
||||
ctx.fillText(`${title}`, cols[2], textPos);
|
||||
@ -225,7 +225,7 @@ export default createPreviewComponent(659, 320, {
|
||||
}
|
||||
},
|
||||
|
||||
renderCategoriesWithTopics(ctx, colors, width, height) {
|
||||
renderCategoriesWithTopics(ctx, colors, font, width, height) {
|
||||
const textColor = darkLightDiff(colors.primary, colors.secondary, 50, 50);
|
||||
const margin = height * 0.03;
|
||||
const bodyFontSize = height / 440.0;
|
||||
@ -246,7 +246,7 @@ export default createPreviewComponent(659, 320, {
|
||||
const cols = [0.025, 0.42, 0.53, 0.58, 0.94].map(c => c * width);
|
||||
|
||||
const headingY = height * 0.33;
|
||||
ctx.font = `${bodyFontSize * 0.9}em 'Arial'`;
|
||||
ctx.font = `${bodyFontSize * 0.9}em '${font}'`;
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.fillText("Category", cols[0], headingY);
|
||||
ctx.fillText("Topics", cols[1], headingY);
|
||||
@ -270,11 +270,11 @@ export default createPreviewComponent(659, 320, {
|
||||
// Categories
|
||||
this.categories().forEach(category => {
|
||||
const textPos = y + categoryHeight * 0.35;
|
||||
ctx.font = `Bold ${bodyFontSize * 1.1}em 'Arial'`;
|
||||
ctx.font = `Bold ${bodyFontSize * 1.1}em '${font}'`;
|
||||
ctx.fillStyle = colors.primary;
|
||||
ctx.fillText(category.name, cols[0], textPos);
|
||||
|
||||
ctx.font = `${bodyFontSize * 0.8}em 'Arial'`;
|
||||
ctx.font = `${bodyFontSize * 0.8}em '${font}'`;
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.fillText(
|
||||
titles[0],
|
||||
@ -289,7 +289,7 @@ export default createPreviewComponent(659, 320, {
|
||||
ctx.lineTo(margin, y + categoryHeight);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.font = `${bodyFontSize}em 'Arial'`;
|
||||
ctx.font = `${bodyFontSize}em '${font}'`;
|
||||
ctx.fillText(Math.floor(Math.random() * 90) + 10, cols[1] + 15, textPos);
|
||||
|
||||
y += categoryHeight;
|
||||
@ -306,7 +306,7 @@ export default createPreviewComponent(659, 320, {
|
||||
|
||||
titles.forEach(title => {
|
||||
const category = this.categories()[0];
|
||||
ctx.font = `${bodyFontSize}em 'Arial'`;
|
||||
ctx.font = `${bodyFontSize}em '${font}'`;
|
||||
const textPos = y + topicHeight * 0.45;
|
||||
ctx.fillStyle = textColor;
|
||||
this.scaleImage(
|
||||
@ -318,15 +318,15 @@ export default createPreviewComponent(659, 320, {
|
||||
);
|
||||
ctx.fillText(title, cols[3], textPos);
|
||||
|
||||
ctx.font = `Bold ${bodyFontSize}em 'Arial'`;
|
||||
ctx.font = `Bold ${bodyFontSize}em '${font}'`;
|
||||
ctx.fillText(Math.floor(Math.random() * 90) + 10, cols[4], textPos);
|
||||
ctx.font = `${bodyFontSize}em 'Arial'`;
|
||||
ctx.font = `${bodyFontSize}em '${font}'`;
|
||||
ctx.fillText(`1h`, cols[4], textPos + topicHeight * 0.4);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = category.color;
|
||||
const badgeSize = topicHeight * 0.1;
|
||||
ctx.font = `Bold ${bodyFontSize * 0.5}em 'Arial'`;
|
||||
ctx.font = `Bold ${bodyFontSize * 0.5}em '${font}'`;
|
||||
ctx.rect(
|
||||
cols[3] + margin * 0.5,
|
||||
y + topicHeight * 0.65,
|
||||
@ -357,12 +357,12 @@ export default createPreviewComponent(659, 320, {
|
||||
return LOREM.split(".");
|
||||
},
|
||||
|
||||
renderLatest(ctx, colors, width, height) {
|
||||
renderLatest(ctx, colors, font, width, height) {
|
||||
const rowHeight = height / 6.6;
|
||||
const textColor = darkLightDiff(colors.primary, colors.secondary, 50, 50);
|
||||
const bodyFontSize = height / 440.0;
|
||||
|
||||
ctx.font = `${bodyFontSize}em 'Arial'`;
|
||||
ctx.font = `${bodyFontSize}em '${font}'`;
|
||||
|
||||
const margin = height * 0.03;
|
||||
|
||||
@ -385,7 +385,7 @@ export default createPreviewComponent(659, 320, {
|
||||
const headingY = height * 0.33;
|
||||
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.font = `${bodyFontSize * 0.9}em 'Arial'`;
|
||||
ctx.font = `${bodyFontSize * 0.9}em '${font}'`;
|
||||
ctx.fillText("Topic", cols[0], headingY);
|
||||
ctx.fillText("Replies", cols[2], headingY);
|
||||
ctx.fillText("Views", cols[3], headingY);
|
||||
@ -396,7 +396,7 @@ export default createPreviewComponent(659, 320, {
|
||||
ctx.lineWidth = 2;
|
||||
drawLine(y);
|
||||
|
||||
ctx.font = `${bodyFontSize}em 'Arial'`;
|
||||
ctx.font = `${bodyFontSize}em '${font}'`;
|
||||
ctx.lineWidth = 1;
|
||||
this.getTitles().forEach(title => {
|
||||
const textPos = y + rowHeight * 0.4;
|
||||
@ -407,7 +407,7 @@ export default createPreviewComponent(659, 320, {
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = category.color;
|
||||
const badgeSize = rowHeight * 0.15;
|
||||
ctx.font = `Bold ${bodyFontSize * 0.75}em 'Arial'`;
|
||||
ctx.font = `Bold ${bodyFontSize * 0.75}em '${font}'`;
|
||||
ctx.rect(cols[0] + 4, y + rowHeight * 0.6, badgeSize, badgeSize);
|
||||
ctx.fill();
|
||||
|
||||
@ -426,7 +426,7 @@ export default createPreviewComponent(659, 320, {
|
||||
);
|
||||
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.font = `${bodyFontSize}em 'Arial'`;
|
||||
ctx.font = `${bodyFontSize}em '${font}'`;
|
||||
for (let j = 2; j <= 4; j++) {
|
||||
ctx.fillText(
|
||||
j === 4 ? "1h" : Math.floor(Math.random() * 90) + 10,
|
||||
|
@ -14,7 +14,7 @@ export default createPreviewComponent(371, 124, {
|
||||
return { tab: "/images/wizard/tab.png", image: this.get("field.value") };
|
||||
},
|
||||
|
||||
paint(ctx, colors, width, height) {
|
||||
paint(ctx, colors, font, width, height) {
|
||||
this.scaleImage(this.tab, 0, 0, width, height);
|
||||
this.scaleImage(this.image, 40, 25, 30, 30);
|
||||
|
||||
|
@ -17,7 +17,7 @@ export default createPreviewComponent(325, 125, {
|
||||
};
|
||||
},
|
||||
|
||||
paint(ctx, colors, width, height) {
|
||||
paint(ctx, colors, font, width, height) {
|
||||
this.scaleImage(this.image, 10, 8, 87, 87);
|
||||
this.scaleImage(this.ios, 0, 0, width, height);
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ export default createPreviewComponent(375, 100, {
|
||||
return { image: this.get("field.value") };
|
||||
},
|
||||
|
||||
paint(ctx, colors, width, height) {
|
||||
paint(ctx, colors, font, width, height) {
|
||||
const headerHeight = height / 2;
|
||||
|
||||
drawHeader(ctx, colors, width, headerHeight);
|
||||
@ -39,7 +39,7 @@ export default createPreviewComponent(375, 100, {
|
||||
|
||||
const afterLogo = headerMargin * 1.7 + imageWidth;
|
||||
const fontSize = Math.round(headerHeight * 0.4);
|
||||
ctx.font = `Bold ${fontSize}px 'Arial'`;
|
||||
ctx.font = `Bold ${fontSize}px '${font}'`;
|
||||
ctx.fillStyle = colors.primary;
|
||||
const title = LOREM.substring(0, 27);
|
||||
ctx.fillText(
|
||||
@ -55,7 +55,7 @@ export default createPreviewComponent(375, 100, {
|
||||
ctx.rect(afterLogo, headerHeight * 0.7, badgeSize, badgeSize);
|
||||
ctx.fill();
|
||||
|
||||
ctx.font = `Bold ${badgeSize * 1.2}px 'Arial'`;
|
||||
ctx.font = `Bold ${badgeSize * 1.2}px '${font}'`;
|
||||
ctx.fillStyle = colors.primary;
|
||||
ctx.fillText(
|
||||
category.name,
|
||||
@ -64,7 +64,7 @@ export default createPreviewComponent(375, 100, {
|
||||
);
|
||||
|
||||
const LINE_HEIGHT = 12;
|
||||
ctx.font = `${LINE_HEIGHT}px 'Arial'`;
|
||||
ctx.font = `${LINE_HEIGHT}px '${font}'`;
|
||||
const lines = LOREM.split("\n");
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const line = height * 0.55 + i * (LINE_HEIGHT * 1.5);
|
||||
|
@ -13,7 +13,7 @@ export default createPreviewComponent(400, 100, {
|
||||
return { image: this.get("field.value") };
|
||||
},
|
||||
|
||||
paint(ctx, colors, width, height) {
|
||||
paint(ctx, colors, font, width, height) {
|
||||
const headerHeight = height / 2;
|
||||
|
||||
drawHeader(ctx, colors, width, headerHeight);
|
||||
@ -31,6 +31,6 @@ export default createPreviewComponent(400, 100, {
|
||||
imageHeight
|
||||
);
|
||||
|
||||
this.drawPills(colors, height / 2);
|
||||
this.drawPills(colors, font, height / 2);
|
||||
}
|
||||
});
|
||||
|
@ -35,10 +35,10 @@ export default createPreviewComponent(305, 165, {
|
||||
};
|
||||
},
|
||||
|
||||
paint(ctx, colors, width, height) {
|
||||
paint(ctx, colors, font, width, height) {
|
||||
const headerHeight = height * 0.3;
|
||||
|
||||
this.drawFullHeader(colors);
|
||||
this.drawFullHeader(colors, font);
|
||||
|
||||
const margin = width * 0.04;
|
||||
const avatarSize = height * 0.2;
|
||||
@ -57,11 +57,11 @@ export default createPreviewComponent(305, 165, {
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = colors.primary;
|
||||
ctx.font = `bold ${titleFontSize}em 'Arial'`;
|
||||
ctx.font = `bold ${titleFontSize}em '${font}'`;
|
||||
ctx.fillText(I18n.t("wizard.previews.topic_title"), margin, height * 0.3);
|
||||
|
||||
const bodyFontSize = height / 220.0;
|
||||
ctx.font = `${bodyFontSize}em 'Arial'`;
|
||||
ctx.font = `${bodyFontSize}em '${font}'`;
|
||||
|
||||
let line = 0;
|
||||
const lines = LOREM.split("\n");
|
||||
@ -76,7 +76,7 @@ export default createPreviewComponent(305, 165, {
|
||||
ctx.fillStyle = darkLightDiff(colors.primary, colors.secondary, 90, 65);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = chooseDarker(colors.primary, colors.secondary);
|
||||
ctx.font = `${bodyFontSize}em 'Arial'`;
|
||||
ctx.font = `${bodyFontSize}em '${font}'`;
|
||||
ctx.fillText(
|
||||
I18n.t("wizard.previews.share_button"),
|
||||
margin + width / 55,
|
||||
@ -94,7 +94,7 @@ export default createPreviewComponent(305, 165, {
|
||||
ctx.fillStyle = colors.tertiary;
|
||||
ctx.fill();
|
||||
ctx.fillStyle = colors.secondary;
|
||||
ctx.font = `${bodyFontSize}em 'Arial'`;
|
||||
ctx.font = `${bodyFontSize}em '${font}'`;
|
||||
ctx.fillText(
|
||||
I18n.t("wizard.previews.reply_button"),
|
||||
margin * 2 + width * 0.14 + width / 55,
|
||||
@ -118,7 +118,7 @@ export default createPreviewComponent(305, 165, {
|
||||
ctx.lineTo(timelineX, height * 0.4);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.font = `Bold ${bodyFontSize}em Arial`;
|
||||
ctx.font = `Bold ${bodyFontSize}em ${font}`;
|
||||
ctx.fillStyle = colors.primary;
|
||||
ctx.fillText("1 / 20", timelineX + margin, height * 0.3 + margin * 1.5);
|
||||
}
|
||||
|
@ -7,5 +7,16 @@ export default Controller.extend({
|
||||
@discourseComputed("currentStepId")
|
||||
showCanvas(currentStepId) {
|
||||
return currentStepId === "finished";
|
||||
},
|
||||
|
||||
@discourseComputed("model")
|
||||
fontClasses(model) {
|
||||
const fontsStep = model.steps.findBy("id", "fonts");
|
||||
if (!fontsStep) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const fontField = fontsStep.get("fieldsById.font_previews");
|
||||
return fontField.choices.map(choice => `font-${choice.data.class}`);
|
||||
}
|
||||
});
|
||||
|
@ -55,6 +55,10 @@ export function createPreviewComponent(width, height, obj) {
|
||||
|
||||
images() {},
|
||||
|
||||
loadFonts() {
|
||||
return document.fonts.ready;
|
||||
},
|
||||
|
||||
loadImages() {
|
||||
const images = this.images();
|
||||
if (images) {
|
||||
@ -68,7 +72,7 @@ export function createPreviewComponent(width, height, obj) {
|
||||
},
|
||||
|
||||
reload() {
|
||||
this.loadImages().then(() => {
|
||||
Promise.all([this.loadFonts(), this.loadImages()]).then(() => {
|
||||
this.loaded = true;
|
||||
this.triggerRepaint();
|
||||
});
|
||||
@ -88,12 +92,17 @@ export function createPreviewComponent(width, height, obj) {
|
||||
return;
|
||||
}
|
||||
|
||||
const font = this.wizard.getCurrentFont(this.fontId);
|
||||
if (!font) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { ctx } = this;
|
||||
|
||||
ctx.fillStyle = colors.secondary;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
this.paint(ctx, colors, this.width, this.height);
|
||||
this.paint(ctx, colors, font, this.width, this.height);
|
||||
|
||||
// draw border
|
||||
ctx.beginPath();
|
||||
@ -133,7 +142,7 @@ export function createPreviewComponent(width, height, obj) {
|
||||
ctx.drawImage(scaled[key], x, y, w, h);
|
||||
},
|
||||
|
||||
drawFullHeader(colors) {
|
||||
drawFullHeader(colors, font) {
|
||||
const { ctx } = this;
|
||||
|
||||
const headerHeight = height * 0.15;
|
||||
@ -147,7 +156,7 @@ export function createPreviewComponent(width, height, obj) {
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = colors.header_primary;
|
||||
ctx.font = `bold ${logoHeight}px 'Arial'`;
|
||||
ctx.font = `bold ${logoHeight}px '${font}'`;
|
||||
ctx.fillText("Discourse", headerMargin, headerHeight - headerMargin);
|
||||
|
||||
// Top right menu
|
||||
@ -188,7 +197,7 @@ export function createPreviewComponent(width, height, obj) {
|
||||
ctx.restore();
|
||||
},
|
||||
|
||||
drawPills(colors, headerHeight, opts) {
|
||||
drawPills(colors, font, headerHeight, opts) {
|
||||
opts = opts || {};
|
||||
|
||||
const { ctx } = this;
|
||||
@ -210,7 +219,7 @@ export function createPreviewComponent(width, height, obj) {
|
||||
|
||||
const fontSize = Math.round(badgeHeight * 0.5);
|
||||
|
||||
ctx.font = `${fontSize}px 'Arial'`;
|
||||
ctx.font = `${fontSize}px '${font}'`;
|
||||
ctx.fillStyle = colors.primary;
|
||||
ctx.fillText(
|
||||
"all categories",
|
||||
@ -246,7 +255,7 @@ export function createPreviewComponent(width, height, obj) {
|
||||
);
|
||||
ctx.fill();
|
||||
|
||||
ctx.font = `${fontSize}px 'Arial'`;
|
||||
ctx.font = `${fontSize}px '${font}'`;
|
||||
ctx.fillStyle = colors.secondary;
|
||||
let x = headerMargin * 3.0 + categoriesSize;
|
||||
ctx.fillText(
|
||||
|
@ -52,6 +52,35 @@ const Wizard = EmberObject.extend({
|
||||
}
|
||||
|
||||
return option.data.colors;
|
||||
},
|
||||
|
||||
getCurrentFont(fontId) {
|
||||
const fontsStep = this.steps.findBy("id", "fonts");
|
||||
if (!fontsStep) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fontChoice = fontsStep.get("fieldsById.font_previews");
|
||||
if (!fontChoice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const choiceId = fontId ? fontId : fontChoice.get("value");
|
||||
if (!choiceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const choices = fontChoice.get("choices");
|
||||
if (!choices) {
|
||||
return;
|
||||
}
|
||||
|
||||
const option = choices.findBy("id", choiceId);
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
|
||||
return option.data.font_stack.split(",")[0];
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -5,6 +5,13 @@
|
||||
<div class="wizard-column">
|
||||
<div class="wizard-column-contents">
|
||||
{{outlet}}
|
||||
|
||||
{{!-- Load all font styles --}}
|
||||
<div class="preloaded-font-styles">
|
||||
{{#each fontClasses as |fontClass|}}
|
||||
<span class={{fontClass}}> </span>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="wizard-footer">
|
||||
<div class="discourse-logo"></div>
|
||||
|
@ -0,0 +1,8 @@
|
||||
<div class="preview-area">
|
||||
<canvas
|
||||
width={{elementWidth}}
|
||||
height={{elementHeight}}
|
||||
style={{canvasStyle}}
|
||||
>
|
||||
</canvas>
|
||||
</div>
|
@ -0,0 +1,14 @@
|
||||
<ul class="grid">
|
||||
{{#each field.choices as |choice|}}
|
||||
<li>
|
||||
{{font-preview wizard=wizard
|
||||
fontId=choice.id
|
||||
selectedId=field.value
|
||||
onChange=(action "changed")}}
|
||||
{{radio-button radioValue=choice.id
|
||||
label=choice.label
|
||||
value=field.value
|
||||
onChange=(action "changed")}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
@ -13,3 +13,4 @@
|
||||
@import "common/base/*";
|
||||
@import "common/d-editor";
|
||||
@import "common/topic-timeline";
|
||||
@import "common/fonts";
|
||||
|
@ -46,7 +46,7 @@ $base-font-size-smaller: 0.875em !default; // eq. to 14px
|
||||
$base-font-size: 0.938em !default; // eq. to 15px
|
||||
$base-font-size-larger: 1.063em !default; // eq. to 17px
|
||||
$base-font-size-largest: 1.118em !default; // eq. to 19px
|
||||
$base-font-family: Helvetica, Arial, sans-serif !default;
|
||||
$base-font-family: var(--font-family) !default;
|
||||
|
||||
// Font-size defintions, multiplier ^ (step / interval)
|
||||
$font-up-6: 2.296em;
|
||||
|
@ -7,6 +7,7 @@
|
||||
@import "common/foundation/mixins";
|
||||
@import "common/select-kit/*";
|
||||
@import "common/components/svg";
|
||||
@import "common/fonts";
|
||||
|
||||
body.wizard {
|
||||
background-color: #fff;
|
||||
@ -131,7 +132,8 @@ body.wizard {
|
||||
}
|
||||
}
|
||||
|
||||
.wizard-step-colors {
|
||||
.wizard-step-colors,
|
||||
.wizard-step-fonts {
|
||||
max-height: 465px;
|
||||
overflow-y: auto;
|
||||
.grid {
|
||||
@ -189,6 +191,10 @@ body.wizard {
|
||||
width: 100%;
|
||||
border: 1px solid #ccc;
|
||||
|
||||
.preloaded-font-styles {
|
||||
font-size: 1px;
|
||||
}
|
||||
|
||||
.wizard-step-contents {
|
||||
height: 550px;
|
||||
margin-bottom: 2em;
|
||||
|
@ -119,6 +119,7 @@ module ApplicationHelper
|
||||
list << 'ios-device' if ios_device?
|
||||
list << 'rtl' if rtl?
|
||||
list << text_size_class
|
||||
list << font_class
|
||||
list << 'anon' unless current_user
|
||||
list.join(' ')
|
||||
end
|
||||
@ -151,6 +152,10 @@ module ApplicationHelper
|
||||
"text-size-#{size}"
|
||||
end
|
||||
|
||||
def font_class
|
||||
"font-#{SiteSetting.base_font.tr("_", "-")}"
|
||||
end
|
||||
|
||||
def escape_unicode(javascript)
|
||||
if javascript
|
||||
javascript = javascript.scrub
|
||||
|
19
app/models/base_font_setting.rb
Normal file
19
app/models/base_font_setting.rb
Normal file
@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "enum_site_setting"
|
||||
|
||||
class BaseFontSetting < EnumSiteSetting
|
||||
def self.valid_value?(val)
|
||||
values.any? { |v| v[:value].to_s == val.to_s }
|
||||
end
|
||||
|
||||
def self.values
|
||||
@values ||= DiscourseFonts.fonts.map do |font|
|
||||
{ name: "base_font_setting.#{font[:key]}", value: font[:key] }
|
||||
end
|
||||
end
|
||||
|
||||
def self.translate_names?
|
||||
true
|
||||
end
|
||||
end
|
@ -53,6 +53,8 @@ end
|
||||
|
||||
require 'pry-rails' if Rails.env.development?
|
||||
|
||||
require 'discourse_fonts'
|
||||
|
||||
if defined?(Bundler)
|
||||
bundler_groups = [:default]
|
||||
|
||||
@ -307,6 +309,32 @@ module Discourse
|
||||
config.assets.precompile << "#{file}.js"
|
||||
end
|
||||
|
||||
# Use discourse-fonts gem to symlink fonts and generate .scss file
|
||||
fonts_path = File.join(config.root, 'public/fonts')
|
||||
Discourse::Utils.atomic_ln_s(DiscourseFonts.path_for_fonts, fonts_path)
|
||||
File.open(File.join(config.root, 'app/assets/stylesheets/common/fonts.scss'), 'w') do |file|
|
||||
DiscourseFonts.fonts.each do |font|
|
||||
file.write <<~EOF
|
||||
.font-#{font[:key].tr("_", "-")} {
|
||||
--font-family: #{font[:name]};
|
||||
font-family: #{font[:name]};
|
||||
}
|
||||
EOF
|
||||
|
||||
if font[:variants].present?
|
||||
font[:variants].each do |variant|
|
||||
file.write <<~EOF
|
||||
@font-face {
|
||||
font-family: #{font[:name]};
|
||||
src: asset-url("/fonts/#{variant[:filename]}") format("#{variant[:format]}");
|
||||
font-weight: #{variant[:weight]};
|
||||
}
|
||||
EOF
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
require_dependency 'stylesheet/manager'
|
||||
require_dependency 'svg_sprite/svg_sprite'
|
||||
|
||||
|
@ -1733,6 +1733,31 @@ en:
|
||||
categories_boxes: "Boxes with Subcategories"
|
||||
categories_boxes_with_topics: "Boxes with Featured Topics"
|
||||
|
||||
base_font_setting:
|
||||
helvetica: "Helvetica/Arial"
|
||||
open_sans: "Open Sans"
|
||||
oxanium: "Oxanium"
|
||||
roboto: "Roboto"
|
||||
lato: "Lato"
|
||||
noto_sans_jp: "NotoSansJP"
|
||||
montserrat: "Montserrat"
|
||||
roboto_condensed: "RobotoCondensed"
|
||||
source_sans_pro: "SourceSansPro"
|
||||
oswald: "Oswald"
|
||||
raleway: "Raleway"
|
||||
roboto_mono: "RobotoMono"
|
||||
poppins: "Poppins"
|
||||
noto_sans: "NotoSans"
|
||||
roboto_slab: "RobotoSlab"
|
||||
merriweather: "Merriweather"
|
||||
ubuntu: "Ubuntu"
|
||||
pt_sans: "PTSans"
|
||||
playfair_display: "PlayfairDisplay"
|
||||
nunito: "Nunito"
|
||||
lora: "Lora"
|
||||
mukta: "Mukta"
|
||||
|
||||
|
||||
shortcut_modifier_key:
|
||||
shift: "Shift"
|
||||
ctrl: "Ctrl"
|
||||
|
@ -2222,6 +2222,8 @@ en:
|
||||
push_notifications_prompt: "Display user consent prompt."
|
||||
push_notifications_icon: "The badge icon that appears in the notification corner. A 96×96 monochromatic PNG with transparency is recommended."
|
||||
|
||||
base_font: "Font to use in most places on the site. Themes can override."
|
||||
|
||||
short_title: "The short title will be used on the user's home screen, launcher, or other places where space may be limited. It should be limited to 12 characters."
|
||||
|
||||
dashboard_hidden_reports: "Allow to hide the specified reports from the dashboard."
|
||||
@ -4680,6 +4682,9 @@ en:
|
||||
</ul>
|
||||
Popular theme components (for more, browse <a href='https://meta.discourse.org/c/theme/l/latest' target='_blank'>#theme</a>)"
|
||||
|
||||
fonts:
|
||||
title: "Fonts"
|
||||
|
||||
logos:
|
||||
title: "Logos"
|
||||
fields:
|
||||
|
@ -323,6 +323,10 @@ basic:
|
||||
vapid_base_url:
|
||||
default: ""
|
||||
hidden: true
|
||||
base_font:
|
||||
default: "helvetica"
|
||||
enum: "BaseFontSetting"
|
||||
refresh: true
|
||||
|
||||
login:
|
||||
invite_only:
|
||||
|
@ -209,6 +209,22 @@ class Wizard
|
||||
step.add_field(id: 'popular-themes', type: 'component')
|
||||
end
|
||||
|
||||
@wizard.append_step('fonts') do |step|
|
||||
field = step.add_field(
|
||||
id: 'font_previews',
|
||||
type: 'component',
|
||||
value: SiteSetting.base_font
|
||||
)
|
||||
|
||||
DiscourseFonts.fonts.each do |font|
|
||||
field.add_choice(font[:key], data: { class: font[:key].tr("_", "-"), font_stack: font[:stack] })
|
||||
end
|
||||
|
||||
step.on_update do |updater|
|
||||
updater.update_setting(:base_font, updater.fields[:font_previews])
|
||||
end
|
||||
end
|
||||
|
||||
@wizard.append_step('logos') do |step|
|
||||
step.add_field(id: 'logo', type: 'image', value: SiteSetting.site_logo_url)
|
||||
step.add_field(id: 'logo_small', type: 'image', value: SiteSetting.site_logo_small_url)
|
||||
|
@ -167,6 +167,16 @@ describe Wizard::StepUpdater do
|
||||
end
|
||||
end
|
||||
|
||||
context "fonts step" do
|
||||
it "updates the font" do
|
||||
updater = wizard.create_updater('fonts', font_previews: 'open_sans')
|
||||
updater.update
|
||||
expect(updater.success?).to eq(true)
|
||||
expect(wizard.completed_steps?('fonts')).to eq(true)
|
||||
expect(SiteSetting.base_font).to eq('open_sans')
|
||||
end
|
||||
end
|
||||
|
||||
context "colors step" do
|
||||
context "with an existing color scheme" do
|
||||
fab!(:color_scheme) { Fabricate(:color_scheme, name: 'existing', via_wizard: true) }
|
||||
|
@ -41,6 +41,15 @@ describe Wizard::Builder do
|
||||
expect(invites_step.disabled).to be_truthy
|
||||
end
|
||||
|
||||
context 'fonts step' do
|
||||
let(:fonts_step) { wizard.steps.find { |s| s.id == 'fonts' } }
|
||||
let(:field) { fonts_step.fields.first }
|
||||
|
||||
it 'should set the right font' do
|
||||
expect(field.choices.size).to eq(DiscourseFonts.fonts.size)
|
||||
end
|
||||
end
|
||||
|
||||
context 'logos step' do
|
||||
let(:logos_step) { wizard.steps.find { |s| s.id == 'logos' } }
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user