mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
FEATURE: User selectable color schemes (#10544)
This commit is contained in:
@@ -72,6 +72,10 @@ export default Controller.extend({
|
||||
this.model.save();
|
||||
},
|
||||
|
||||
applyUserSelectable() {
|
||||
this.model.updateUserSelectable(this.get("model.user_selectable"));
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
const model = this.model;
|
||||
return bootbox.confirm(
|
||||
|
||||
@@ -107,6 +107,17 @@ const ColorScheme = EmberObject.extend({
|
||||
});
|
||||
},
|
||||
|
||||
updateUserSelectable(value) {
|
||||
if (!this.id) return;
|
||||
|
||||
return ajax(`/admin/color_schemes/${this.id}.json`, {
|
||||
data: JSON.stringify({ color_scheme: { user_selectable: value } }),
|
||||
type: "PUT",
|
||||
dataType: "json",
|
||||
contentType: "application/json"
|
||||
});
|
||||
},
|
||||
|
||||
destroy() {
|
||||
if (this.id) {
|
||||
return ajax(`/admin/color_schemes/${this.id}`, { type: "DELETE" });
|
||||
@@ -129,6 +140,7 @@ ColorScheme.reopenClass({
|
||||
theme_id: colorScheme.theme_id,
|
||||
theme_name: colorScheme.theme_name,
|
||||
base_scheme_id: colorScheme.base_scheme_id,
|
||||
user_selectable: colorScheme.user_selectable,
|
||||
colors: colorScheme.colors.map(c => {
|
||||
return ColorSchemeColor.create({
|
||||
name: c.name,
|
||||
|
||||
@@ -22,9 +22,12 @@
|
||||
icon="far-clipboard"
|
||||
label="admin.customize.copy_to_clipboard"
|
||||
}}
|
||||
<span class="saving {{unless model.savingStatus "hidden"}}">{{model.savingStatus}}</span>
|
||||
{{#if model.theme_id}}
|
||||
{{i18n "admin.customize.theme_owner"}}
|
||||
{{#link-to "adminCustomizeThemes.show" model.theme_id}}{{model.theme_name}}{{/link-to}}
|
||||
<span class="not-editable">
|
||||
{{i18n "admin.customize.theme_owner"}}
|
||||
{{#link-to "adminCustomizeThemes.show" model.theme_id}}{{model.theme_name}}{{/link-to}}
|
||||
</span>
|
||||
{{else}}
|
||||
{{d-button
|
||||
action=(action "destroy")
|
||||
@@ -33,27 +36,29 @@
|
||||
label="admin.customize.delete"
|
||||
}}
|
||||
{{/if}}
|
||||
<span class="saving {{unless model.savingStatus "hidden"}}">{{model.savingStatus}}</span>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="admin-controls">
|
||||
<div class="search controls">
|
||||
<label>
|
||||
{{input type="checkbox" checked=onlyOverridden}}
|
||||
{{i18n "admin.settings.show_overriden"}}
|
||||
</label>
|
||||
</div>
|
||||
{{inline-edit-checkbox action=(action "applyUserSelectable") labelKey="admin.customize.theme.color_scheme_user_selectable" checked=model.user_selectable}}
|
||||
</div>
|
||||
|
||||
{{#if colors.length}}
|
||||
<table class="table colors">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
</th>
|
||||
<th class="hex">{{i18n "admin.customize.color"}}</th>
|
||||
<th></th>
|
||||
<th class="overriden">
|
||||
{{#unless model.theme_id}}
|
||||
<label>
|
||||
{{input type="checkbox" checked=onlyOverridden}}
|
||||
{{i18n "admin.settings.show_overriden"}}
|
||||
</label>
|
||||
{{/unless}}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -4,6 +4,11 @@ import Controller from "@ember/controller";
|
||||
import { setDefaultHomepage } from "discourse/lib/utilities";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { listThemes, setLocalTheme } from "discourse/lib/theme-selector";
|
||||
import {
|
||||
listColorSchemes,
|
||||
loadColorSchemeStylesheet,
|
||||
updateColorSchemeCookie
|
||||
} from "discourse/lib/color-scheme-picker";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { reload } from "discourse/helpers/page-reloader";
|
||||
import {
|
||||
@@ -12,6 +17,7 @@ import {
|
||||
iOSWithVisualViewport
|
||||
} from "discourse/lib/utilities";
|
||||
import { computed } from "@ember/object";
|
||||
import { reads } from "@ember/object/computed";
|
||||
|
||||
const USER_HOMES = {
|
||||
1: "latest",
|
||||
@@ -26,14 +32,25 @@ const TITLE_COUNT_MODES = ["notifications", "contextual"];
|
||||
|
||||
export default Controller.extend({
|
||||
currentThemeId: -1,
|
||||
previewingColorScheme: false,
|
||||
selectedColorSchemeId: null,
|
||||
selectedDarkColorSchemeId: null,
|
||||
preferencesController: inject("preferences"),
|
||||
|
||||
@discourseComputed("makeThemeDefault")
|
||||
saveAttrNames(makeDefault) {
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.setProperties({
|
||||
selectedColorSchemeId: this.session.userColorSchemeId,
|
||||
selectedDarkColorSchemeId: this.session.userDarkSchemeId
|
||||
});
|
||||
},
|
||||
|
||||
@discourseComputed("makeThemeDefault", "makeColorSchemeDefault")
|
||||
saveAttrNames(makeThemeDefault, makeColorSchemeDefault) {
|
||||
let attrs = [
|
||||
"locale",
|
||||
"external_links_in_new_tab",
|
||||
"dark_scheme_id",
|
||||
"dynamic_favicon",
|
||||
"enable_quoting",
|
||||
"enable_defer",
|
||||
@@ -47,10 +64,14 @@ export default Controller.extend({
|
||||
"skip_new_user_tips"
|
||||
];
|
||||
|
||||
if (makeDefault) {
|
||||
if (makeThemeDefault) {
|
||||
attrs.push("theme_ids");
|
||||
}
|
||||
|
||||
if (makeColorSchemeDefault) {
|
||||
attrs.push("color_scheme_id");
|
||||
attrs.push("dark_scheme_id");
|
||||
}
|
||||
return attrs;
|
||||
},
|
||||
|
||||
@@ -71,6 +92,11 @@ export default Controller.extend({
|
||||
return JSON.parse(this.siteSettings.available_locales);
|
||||
},
|
||||
|
||||
@discourseComputed
|
||||
defaultDarkSchemeId() {
|
||||
return this.siteSettings.default_dark_mode_color_scheme_id;
|
||||
},
|
||||
|
||||
@discourseComputed
|
||||
textSizes() {
|
||||
return TEXT_SIZES.map(value => {
|
||||
@@ -116,6 +142,16 @@ export default Controller.extend({
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed
|
||||
userSelectableColorSchemes() {
|
||||
return listColorSchemes(this.site);
|
||||
},
|
||||
|
||||
showColorSchemeSelector: reads("userSelectableColorSchemes.length"),
|
||||
selectedColorSchemeNoneLabel: I18n.t(
|
||||
"user.color_schemes.default_description"
|
||||
),
|
||||
|
||||
@discourseComputed("model.user_option.theme_ids", "themeId")
|
||||
showThemeSetDefault(userOptionThemes, selectedTheme) {
|
||||
return !userOptionThemes || userOptionThemes[0] !== selectedTheme;
|
||||
@@ -153,7 +189,23 @@ export default Controller.extend({
|
||||
|
||||
@discourseComputed
|
||||
showDarkModeToggle() {
|
||||
return this.siteSettings.default_dark_mode_color_scheme_id > 0;
|
||||
return this.defaultDarkSchemeId > 0 && !this.showDarkColorSchemeSelector;
|
||||
},
|
||||
|
||||
@discourseComputed
|
||||
userSelectableDarkColorSchemes() {
|
||||
return listColorSchemes(this.site, {
|
||||
darkOnly: true
|
||||
});
|
||||
},
|
||||
|
||||
@discourseComputed("userSelectableDarkColorSchemes")
|
||||
showDarkColorSchemeSelector(darkSchemes) {
|
||||
// when a default dark scheme is set
|
||||
// dropdown has two items (disable / use site default)
|
||||
// but we show a checkbox in that case
|
||||
const minToShow = this.defaultDarkSchemeId > 0 ? 2 : 1;
|
||||
return darkSchemes && darkSchemes.length > minToShow;
|
||||
},
|
||||
|
||||
enableDarkMode: computed({
|
||||
@@ -178,10 +230,32 @@ export default Controller.extend({
|
||||
this.set("model.user_option.text_size", this.textSize);
|
||||
}
|
||||
|
||||
this.set(
|
||||
"model.user_option.dark_scheme_id",
|
||||
this.enableDarkMode ? null : -1
|
||||
);
|
||||
if (this.makeColorSchemeDefault) {
|
||||
this.set(
|
||||
"model.user_option.color_scheme_id",
|
||||
this.selectedColorSchemeId
|
||||
);
|
||||
}
|
||||
|
||||
if (this.showDarkModeToggle) {
|
||||
this.set(
|
||||
"model.user_option.dark_scheme_id",
|
||||
this.enableDarkMode ? null : -1
|
||||
);
|
||||
} else {
|
||||
// if chosen dark scheme matches site dark scheme, no need to store
|
||||
if (
|
||||
this.defaultDarkSchemeId > 0 &&
|
||||
this.selectedDarkColorSchemeId === this.defaultDarkSchemeId
|
||||
) {
|
||||
this.set("model.user_option.dark_scheme_id", null);
|
||||
} else {
|
||||
this.set(
|
||||
"model.user_option.dark_scheme_id",
|
||||
this.selectedDarkColorSchemeId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.model
|
||||
.save(this.saveAttrNames)
|
||||
@@ -202,6 +276,24 @@ export default Controller.extend({
|
||||
this.model.updateTextSizeCookie(this.textSize);
|
||||
}
|
||||
|
||||
if (this.makeColorSchemeDefault) {
|
||||
updateColorSchemeCookie(null);
|
||||
updateColorSchemeCookie(null, { dark: true });
|
||||
} else {
|
||||
updateColorSchemeCookie(this.selectedColorSchemeId);
|
||||
|
||||
if (
|
||||
this.defaultDarkSchemeId > 0 &&
|
||||
this.selectedDarkColorSchemeId === this.defaultDarkSchemeId
|
||||
) {
|
||||
updateColorSchemeCookie(null, { dark: true });
|
||||
} else {
|
||||
updateColorSchemeCookie(this.selectedDarkColorSchemeId, {
|
||||
dark: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.homeChanged();
|
||||
|
||||
if (this.isiPad) {
|
||||
@@ -236,6 +328,60 @@ export default Controller.extend({
|
||||
// Force refresh when leaving this screen
|
||||
this.session.requiresRefresh = true;
|
||||
this.set("textSize", newSize);
|
||||
},
|
||||
|
||||
loadColorScheme(colorSchemeId) {
|
||||
this.setProperties({
|
||||
selectedColorSchemeId: colorSchemeId,
|
||||
previewingColorScheme: true
|
||||
});
|
||||
|
||||
if (colorSchemeId < 0) {
|
||||
const defaultTheme = this.userSelectableThemes.findBy(
|
||||
"id",
|
||||
this.themeId
|
||||
);
|
||||
|
||||
if (defaultTheme && defaultTheme.color_scheme_id) {
|
||||
colorSchemeId = defaultTheme.color_scheme_id;
|
||||
}
|
||||
}
|
||||
loadColorSchemeStylesheet(colorSchemeId, this.themeId);
|
||||
if (this.selectedDarkColorSchemeId === -1) {
|
||||
// set this same scheme for dark mode preview when dark scheme is disabled
|
||||
loadColorSchemeStylesheet(colorSchemeId, this.themeId, true);
|
||||
}
|
||||
},
|
||||
|
||||
loadDarkColorScheme(colorSchemeId) {
|
||||
this.setProperties({
|
||||
selectedDarkColorSchemeId: colorSchemeId,
|
||||
previewingColorScheme: true
|
||||
});
|
||||
|
||||
if (colorSchemeId === -1) {
|
||||
// load preview of regular scheme when dark scheme is disabled
|
||||
loadColorSchemeStylesheet(
|
||||
this.selectedColorSchemeId,
|
||||
this.themeId,
|
||||
true
|
||||
);
|
||||
} else {
|
||||
loadColorSchemeStylesheet(colorSchemeId, this.themeId, true);
|
||||
}
|
||||
},
|
||||
|
||||
undoColorSchemePreview() {
|
||||
this.setProperties({
|
||||
selectedColorSchemeId: this.session.userColorSchemeId,
|
||||
selectedDarkColorSchemeId: this.session.userDarkSchemeId,
|
||||
previewingColorScheme: false
|
||||
});
|
||||
const darkStylesheet = document.querySelector("link#cs-preview-dark"),
|
||||
lightStylesheet = document.querySelector("link#cs-preview-light");
|
||||
if (darkStylesheet) darkStylesheet.remove();
|
||||
|
||||
if (lightStylesheet) lightStylesheet.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import I18n from "I18n";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import cookie, { removeCookie } from "discourse/lib/cookie";
|
||||
|
||||
export function listColorSchemes(site, options = {}) {
|
||||
let schemes = site.get("user_color_schemes");
|
||||
|
||||
if (!schemes || !Array.isArray(schemes)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let results = [];
|
||||
|
||||
if (!options.darkOnly) {
|
||||
schemes = schemes.sort((a, b) => Number(a.is_dark) - Number(b.is_dark));
|
||||
}
|
||||
schemes.forEach(s => {
|
||||
if ((options.darkOnly && s.is_dark) || !options.darkOnly) {
|
||||
results.push({
|
||||
name: s.name,
|
||||
id: s.id
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (options.darkOnly) {
|
||||
const defaultDarkColorScheme = site.get("default_dark_color_scheme");
|
||||
if (defaultDarkColorScheme) {
|
||||
const existing = schemes.findBy("id", defaultDarkColorScheme.id);
|
||||
if (!existing) {
|
||||
results.unshift({
|
||||
id: defaultDarkColorScheme.id,
|
||||
name: `${defaultDarkColorScheme.name} ${I18n.t(
|
||||
"user.color_schemes.default_dark_scheme"
|
||||
)}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
results.unshift({
|
||||
id: -1,
|
||||
name: I18n.t("user.color_schemes.disable_dark_scheme")
|
||||
});
|
||||
}
|
||||
|
||||
return results.length === 0 ? null : results;
|
||||
}
|
||||
|
||||
export function loadColorSchemeStylesheet(
|
||||
colorSchemeId,
|
||||
theme_id,
|
||||
dark = false
|
||||
) {
|
||||
const themeId = theme_id ? `/${theme_id}` : "";
|
||||
ajax(`/color-scheme-stylesheet/${colorSchemeId}${themeId}.json`).then(
|
||||
result => {
|
||||
if (result && result.new_href) {
|
||||
const elementId = dark ? "cs-preview-dark" : "cs-preview-light";
|
||||
const existingElement = document.querySelector(`link#${elementId}`);
|
||||
if (existingElement) {
|
||||
existingElement.href = result.new_href;
|
||||
} else {
|
||||
let link = document.createElement("link");
|
||||
link.href = result.new_href;
|
||||
link.media = dark
|
||||
? "(prefers-color-scheme: dark)"
|
||||
: "(prefers-color-scheme: light)";
|
||||
link.rel = "stylesheet";
|
||||
link.id = elementId;
|
||||
|
||||
document.body.appendChild(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function updateColorSchemeCookie(id, options = {}) {
|
||||
const cookieName = options.dark ? "dark_scheme_id" : "color_scheme_id";
|
||||
if (id) {
|
||||
cookie(cookieName, id, {
|
||||
path: "/",
|
||||
expires: 9999
|
||||
});
|
||||
} else {
|
||||
removeCookie(cookieName, { path: "/", expires: 1 });
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,11 @@ export function listThemes(site) {
|
||||
}
|
||||
|
||||
themes.forEach(t => {
|
||||
results.push({ name: t.name, id: t.theme_id });
|
||||
results.push({
|
||||
name: t.name,
|
||||
id: t.theme_id,
|
||||
color_scheme_id: t.color_scheme_id
|
||||
});
|
||||
});
|
||||
|
||||
return results.length === 0 ? null : results;
|
||||
|
||||
@@ -302,6 +302,7 @@ const User = RestModel.extend({
|
||||
"email_messages_level",
|
||||
"email_level",
|
||||
"email_previous_replies",
|
||||
"color_scheme_id",
|
||||
"dark_scheme_id",
|
||||
"dynamic_favicon",
|
||||
"enable_quoting",
|
||||
|
||||
@@ -90,6 +90,9 @@ export default {
|
||||
|
||||
session.highlightJsPath = setupData.highlightJsPath;
|
||||
session.svgSpritePath = setupData.svgSpritePath;
|
||||
session.userColorSchemeId =
|
||||
parseInt(setupData.userColorSchemeId, 10) || null;
|
||||
session.userDarkSchemeId = parseInt(setupData.userDarkSchemeId, 10) || -1;
|
||||
|
||||
if (isDevelopment()) {
|
||||
setIconList(setupData.svgIconList);
|
||||
|
||||
@@ -12,6 +12,7 @@ export default RestrictedUserRoute.extend({
|
||||
makeThemeDefault:
|
||||
!user.get("user_option.theme_ids") ||
|
||||
currentThemeId() === user.get("user_option.theme_ids")[0],
|
||||
makeColorSchemeDefault: !user.get("user_option.color_scheme_id"),
|
||||
makeTextSizeDefault:
|
||||
user.get("currentTextSize") === user.get("user_option.text_size")
|
||||
});
|
||||
|
||||
@@ -20,6 +20,53 @@
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if showColorSchemeSelector}}
|
||||
<div class="control-group color-scheme">
|
||||
<label class="control-label">{{i18n "user.color_scheme"}}</label>
|
||||
<div class="control-subgroup light-color-scheme">
|
||||
{{#if showDarkColorSchemeSelector}}
|
||||
<div class="instructions">{{i18n "user.color_schemes.regular" }}</div>
|
||||
{{/if}}
|
||||
<div class="controls">
|
||||
{{combo-box
|
||||
content=userSelectableColorSchemes
|
||||
value=selectedColorSchemeId
|
||||
onChange=(action "loadColorScheme")
|
||||
options=(hash
|
||||
translatedNone=selectedColorSchemeNoneLabel
|
||||
)
|
||||
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
{{#if showDarkColorSchemeSelector}}
|
||||
<div class="control-subgroup dark-color-scheme">
|
||||
<div class="instructions">{{i18n "user.color_schemes.dark" }}</div>
|
||||
<div class="controls">
|
||||
{{combo-box
|
||||
content=userSelectableDarkColorSchemes
|
||||
value=selectedDarkColorSchemeId
|
||||
onChange=(action "loadDarkColorScheme")}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="instructions">
|
||||
{{i18n "user.color_schemes.dark_instructions" }}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if previewingColorScheme}}
|
||||
{{#if previewingColorScheme}}
|
||||
{{d-button action=(action "undoColorSchemePreview") label="user.color_schemes.undo" icon="undo" class="btn-default btn-small undo-preview"}}
|
||||
{{/if}}
|
||||
|
||||
<div class="controls color-scheme-checkbox">
|
||||
{{preference-checkbox labelKey="user.color_scheme_default_on_all_devices" checked=makeColorSchemeDefault}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if showDarkModeToggle}}
|
||||
<div class="control-group dark-mode">
|
||||
<label class="control-label">{{i18n "user.dark_mode"}}</label>
|
||||
|
||||
Reference in New Issue
Block a user