diff --git a/app/assets/javascripts/admin/controllers/admin-customize-colors-show.js b/app/assets/javascripts/admin/controllers/admin-customize-colors-show.js index 9ca23f186c7..68d7f65cc41 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-colors-show.js +++ b/app/assets/javascripts/admin/controllers/admin-customize-colors-show.js @@ -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( diff --git a/app/assets/javascripts/admin/models/color-scheme.js b/app/assets/javascripts/admin/models/color-scheme.js index 5fe6337f0d8..28457d9d287 100644 --- a/app/assets/javascripts/admin/models/color-scheme.js +++ b/app/assets/javascripts/admin/models/color-scheme.js @@ -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, diff --git a/app/assets/javascripts/admin/templates/customize-colors-show.hbs b/app/assets/javascripts/admin/templates/customize-colors-show.hbs index b360a1c26bd..86f5c595558 100644 --- a/app/assets/javascripts/admin/templates/customize-colors-show.hbs +++ b/app/assets/javascripts/admin/templates/customize-colors-show.hbs @@ -22,9 +22,12 @@ icon="far-clipboard" label="admin.customize.copy_to_clipboard" }} + {{model.savingStatus}} {{#if model.theme_id}} - {{i18n "admin.customize.theme_owner"}} - {{#link-to "adminCustomizeThemes.show" model.theme_id}}{{model.theme_name}}{{/link-to}} + + {{i18n "admin.customize.theme_owner"}} + {{#link-to "adminCustomizeThemes.show" model.theme_id}}{{model.theme_name}}{{/link-to}} + {{else}} {{d-button action=(action "destroy") @@ -33,27 +36,29 @@ label="admin.customize.delete" }} {{/if}} - {{model.savingStatus}}
- + {{inline-edit-checkbox action=(action "applyUserSelectable") labelKey="admin.customize.theme.color_scheme_user_selectable" checked=model.user_selectable}}
{{#if colors.length}} - + - + diff --git a/app/assets/javascripts/discourse/app/controllers/preferences/interface.js b/app/assets/javascripts/discourse/app/controllers/preferences/interface.js index dd80b409d29..1ec0dd64337 100644 --- a/app/assets/javascripts/discourse/app/controllers/preferences/interface.js +++ b/app/assets/javascripts/discourse/app/controllers/preferences/interface.js @@ -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(); } } }); diff --git a/app/assets/javascripts/discourse/app/lib/color-scheme-picker.js b/app/assets/javascripts/discourse/app/lib/color-scheme-picker.js new file mode 100644 index 00000000000..f1bca621789 --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/color-scheme-picker.js @@ -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 }); + } +} diff --git a/app/assets/javascripts/discourse/app/lib/theme-selector.js b/app/assets/javascripts/discourse/app/lib/theme-selector.js index a5b46c8ac1e..82d838fd766 100644 --- a/app/assets/javascripts/discourse/app/lib/theme-selector.js +++ b/app/assets/javascripts/discourse/app/lib/theme-selector.js @@ -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; diff --git a/app/assets/javascripts/discourse/app/models/user.js b/app/assets/javascripts/discourse/app/models/user.js index 940c5678194..15b7315e2d4 100644 --- a/app/assets/javascripts/discourse/app/models/user.js +++ b/app/assets/javascripts/discourse/app/models/user.js @@ -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", diff --git a/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js b/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js index 20c10a81e8d..15b9ef82922 100644 --- a/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js +++ b/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js @@ -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); diff --git a/app/assets/javascripts/discourse/app/routes/preferences-interface.js b/app/assets/javascripts/discourse/app/routes/preferences-interface.js index 34976bbd825..bf483fc9871 100644 --- a/app/assets/javascripts/discourse/app/routes/preferences-interface.js +++ b/app/assets/javascripts/discourse/app/routes/preferences-interface.js @@ -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") }); diff --git a/app/assets/javascripts/discourse/app/templates/preferences/interface.hbs b/app/assets/javascripts/discourse/app/templates/preferences/interface.hbs index d4cd222b96f..e53cd172c2c 100644 --- a/app/assets/javascripts/discourse/app/templates/preferences/interface.hbs +++ b/app/assets/javascripts/discourse/app/templates/preferences/interface.hbs @@ -20,6 +20,53 @@ {{/if}} +{{#if showColorSchemeSelector}} +
+ +
+ {{#if showDarkColorSchemeSelector}} +
{{i18n "user.color_schemes.regular" }}
+ {{/if}} +
+ {{combo-box + content=userSelectableColorSchemes + value=selectedColorSchemeId + onChange=(action "loadColorScheme") + options=(hash + translatedNone=selectedColorSchemeNoneLabel + ) + + }} +
+
+ {{#if showDarkColorSchemeSelector}} +
+
{{i18n "user.color_schemes.dark" }}
+
+ {{combo-box + content=userSelectableDarkColorSchemes + value=selectedDarkColorSchemeId + onChange=(action "loadDarkColorScheme")}} +
+
+ +
+ {{i18n "user.color_schemes.dark_instructions" }} +
+ {{/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}} + +
+ {{preference-checkbox labelKey="user.color_scheme_default_on_all_devices" checked=makeColorSchemeDefault}} +
+ {{/if}} +
+{{/if}} + {{#if showDarkModeToggle}}
diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss index e5617aa0124..0cc24a534be 100644 --- a/app/assets/stylesheets/common/admin/customize.scss +++ b/app/assets/stylesheets/common/admin/customize.scss @@ -179,6 +179,10 @@ margin: 0 0.5em 0.5em 0; } } + + &.color-scheme .admin-controls { + padding: 0.5em; + } } .add-component-button { vertical-align: middle; @@ -486,10 +490,16 @@ } .color-scheme { .controls { + display: flex; + align-items: center; button, a { margin-right: 10px; } + + button.btn-danger { + margin-left: auto; + } } } .colors { @@ -507,8 +517,8 @@ margin-bottom: 0; margin-right: 6px; } - .hex { - text-align: center; + th.overriden { + text-align: right; } .color-input { display: flex; diff --git a/app/assets/stylesheets/common/base/user.scss b/app/assets/stylesheets/common/base/user.scss index 4aabed9c24c..275444ab7ea 100644 --- a/app/assets/stylesheets/common/base/user.scss +++ b/app/assets/stylesheets/common/base/user.scss @@ -656,6 +656,46 @@ .save-theme-alert { font-size: $font-down-1; } + + .control-subgroup { + float: left; + + .controls { + clear: both; + padding-top: 1em; + } + } + + .light-color-scheme { + margin-right: 1em; + } + + .instructions { + clear: both; + display: inline-block; + margin-top: 4px; + } + + @mixin inactiveMode() { + color: var(--primary-medium); + .select-kit.combo-box .select-kit-header { + border-color: var(--primary-medium); + } + } + + @media (prefers-color-scheme: dark) { + .light-color-scheme { + @include inactiveMode; + } + } + @media (prefers-color-scheme: light) { + .dark-color-scheme { + @include inactiveMode; + } + } + + .undo-preview { + margin-bottom: 1em; + } } .paginated-topics-list { diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 9432a82ff98..aed20e9962e 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -247,11 +247,6 @@ .user-chooser { width: 100%; } - - .instructions { - display: inline-block; - margin-top: 4px; - } } .user-crawler { diff --git a/app/controllers/admin/color_schemes_controller.rb b/app/controllers/admin/color_schemes_controller.rb index 79292350908..4b6407158ab 100644 --- a/app/controllers/admin/color_schemes_controller.rb +++ b/app/controllers/admin/color_schemes_controller.rb @@ -38,6 +38,6 @@ class Admin::ColorSchemesController < Admin::AdminController end def color_scheme_params - params.permit(color_scheme: [:base_scheme_id, :name, colors: [:name, :hex]])[:color_scheme] + params.permit(color_scheme: [:base_scheme_id, :name, :user_selectable, colors: [:name, :hex]])[:color_scheme] end end diff --git a/app/controllers/stylesheets_controller.rb b/app/controllers/stylesheets_controller.rb index 3391456cda7..163ed9e430f 100644 --- a/app/controllers/stylesheets_controller.rb +++ b/app/controllers/stylesheets_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class StylesheetsController < ApplicationController - skip_before_action :preload_json, :redirect_to_login_if_required, :check_xhr, :verify_authenticity_token, only: [:show, :show_source_map] + skip_before_action :preload_json, :redirect_to_login_if_required, :check_xhr, :verify_authenticity_token, only: [:show, :show_source_map, :color_scheme] def show_source_map show_resource(source_map: true) @@ -13,6 +13,13 @@ class StylesheetsController < ApplicationController show_resource end + def color_scheme + params.require("id") + params.permit("theme_id") + + stylesheet = Stylesheet::Manager.color_scheme_stylesheet_details(params[:id], 'all', params[:theme_id]) + render json: stylesheet + end protected def show_resource(source_map: false) @@ -28,7 +35,7 @@ class StylesheetsController < ApplicationController if !Rails.env.production? # TODO add theme # calling this method ensures we have a cache for said target - # we hold of re-compilation till someone asks for asset + # we hold off re-compilation till someone asks for asset if target.include?("color_definitions") split_target, color_scheme_id = target.split(/_(-?[0-9]+)/) Stylesheet::Manager.color_scheme_stylesheet_link_tag(color_scheme_id) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index d4fe2de92b2..c0dec694b17 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -402,6 +402,11 @@ module ApplicationHelper end def scheme_id + custom_user_scheme_id = cookies[:color_scheme_id] || current_user&.user_option&.color_scheme_id + if custom_user_scheme_id && ColorScheme.find_by_id(custom_user_scheme_id) + return custom_user_scheme_id + end + return if theme_ids.blank? Theme .where(id: theme_ids.first) @@ -409,6 +414,10 @@ module ApplicationHelper .first end + def dark_scheme_id + cookies[:dark_scheme_id] || current_user&.user_option&.dark_scheme_id || SiteSetting.default_dark_mode_color_scheme_id + end + def current_homepage current_user&.user_option&.homepage || SiteSetting.anonymous_homepage end @@ -454,9 +463,6 @@ module ApplicationHelper result = +"" result << Stylesheet::Manager.color_scheme_stylesheet_link_tag(scheme_id, 'all', theme_ids) - user_dark_scheme_id = current_user&.user_option&.dark_scheme_id - dark_scheme_id = user_dark_scheme_id || SiteSetting.default_dark_mode_color_scheme_id - if dark_scheme_id != -1 result << Stylesheet::Manager.color_scheme_stylesheet_link_tag(dark_scheme_id, '(prefers-color-scheme: dark)', theme_ids) end @@ -489,7 +495,9 @@ module ApplicationHelper highlight_js_path: HighlightJs.path, svg_sprite_path: SvgSprite.path(theme_ids), enable_js_error_reporting: GlobalSetting.enable_js_error_reporting, - color_scheme_is_dark: dark_color_scheme? + color_scheme_is_dark: dark_color_scheme?, + user_color_scheme_id: scheme_id, + user_dark_scheme_id: dark_scheme_id } if Rails.env.development? diff --git a/app/models/color_scheme.rb b/app/models/color_scheme.rb index 4dbdde5061b..ab82c117a92 100644 --- a/app/models/color_scheme.rb +++ b/app/models/color_scheme.rb @@ -131,8 +131,8 @@ class ColorScheme < ActiveRecord::Base before_save :bump_version after_save :publish_discourse_stylesheet - after_save :dump_hex_cache - after_destroy :dump_hex_cache + after_save :dump_caches + after_destroy :dump_caches belongs_to :theme validates_associated :color_scheme_colors @@ -286,8 +286,9 @@ class ColorScheme < ActiveRecord::Base end end - def dump_hex_cache + def dump_caches self.class.hex_cache.clear + ApplicationSerializer.expire_cache_fragment!("user_color_schemes") end def bump_version @@ -297,6 +298,8 @@ class ColorScheme < ActiveRecord::Base end def is_dark? + return if colors.empty? + primary_b = brightness(colors_by_name["primary"].hex) secondary_b = brightness(colors_by_name["secondary"].hex) @@ -314,12 +317,13 @@ end # # Table name: color_schemes # -# id :integer not null, primary key -# name :string not null -# version :integer default(1), not null -# created_at :datetime not null -# updated_at :datetime not null -# via_wizard :boolean default(FALSE), not null -# base_scheme_id :string -# theme_id :integer +# id :integer not null, primary key +# name :string not null +# version :integer default(1), not null +# created_at :datetime not null +# updated_at :datetime not null +# via_wizard :boolean default(FALSE), not null +# base_scheme_id :string +# theme_id :integer +# user_selectable :boolean default(FALSE), not null # diff --git a/app/serializers/color_scheme_selectable_serializer.rb b/app/serializers/color_scheme_selectable_serializer.rb new file mode 100644 index 00000000000..10ad373e6aa --- /dev/null +++ b/app/serializers/color_scheme_selectable_serializer.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class ColorSchemeSelectableSerializer < ApplicationSerializer + attributes :id, :name, :is_dark + + def is_dark + object.is_dark? + end +end diff --git a/app/serializers/color_scheme_serializer.rb b/app/serializers/color_scheme_serializer.rb index 7c5237f97fa..f8ae507a86c 100644 --- a/app/serializers/color_scheme_serializer.rb +++ b/app/serializers/color_scheme_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class ColorSchemeSerializer < ApplicationSerializer - attributes :id, :name, :is_base, :base_scheme_id, :theme_id, :theme_name + attributes :id, :name, :is_base, :base_scheme_id, :theme_id, :theme_name, :user_selectable has_many :colors, serializer: ColorSchemeColorSerializer, embed: :objects def theme_name diff --git a/app/serializers/site_serializer.rb b/app/serializers/site_serializer.rb index 96833f398e7..f7915050aa9 100644 --- a/app/serializers/site_serializer.rb +++ b/app/serializers/site_serializer.rb @@ -24,6 +24,8 @@ class SiteSerializer < ApplicationSerializer :wizard_required, :topic_featured_link_allowed_category_ids, :user_themes, + :user_color_schemes, + :default_dark_color_scheme, :censored_regexp, :shared_drafts_category_id, :custom_emoji_translation @@ -40,12 +42,23 @@ class SiteSerializer < ApplicationSerializer Theme.where('id = :default OR user_selectable', default: SiteSetting.default_theme_id) .order(:name) - .pluck(:id, :name) - .map { |id, n| { theme_id: id, name: n, default: id == SiteSetting.default_theme_id } } + .pluck(:id, :name, :color_scheme_id) + .map { |id, n, cs| { theme_id: id, name: n, default: id == SiteSetting.default_theme_id, color_scheme_id: cs } } .as_json end end + def user_color_schemes + cache_fragment("user_color_schemes") do + schemes = ColorScheme.where('user_selectable').order(:name) + ActiveModel::ArraySerializer.new(schemes, each_serializer: ColorSchemeSelectableSerializer).as_json + end + end + + def default_dark_color_scheme + ColorScheme.find_by_id(SiteSetting.default_dark_mode_color_scheme_id).as_json + end + def groups cache_anon_fragment("group_names") do object.groups.order(:name).pluck(:id, :name).map { |id, name| { id: id, name: name } }.as_json diff --git a/app/serializers/user_option_serializer.rb b/app/serializers/user_option_serializer.rb index fb6091abd9d..ed47d0e6dbe 100644 --- a/app/serializers/user_option_serializer.rb +++ b/app/serializers/user_option_serializer.rb @@ -8,6 +8,7 @@ class UserOptionSerializer < ApplicationSerializer :email_level, :email_messages_level, :external_links_in_new_tab, + :color_scheme_id, :dark_scheme_id, :dynamic_favicon, :enable_quoting, diff --git a/app/services/color_scheme_revisor.rb b/app/services/color_scheme_revisor.rb index e76f3d6f36b..7515d513d3a 100644 --- a/app/services/color_scheme_revisor.rb +++ b/app/services/color_scheme_revisor.rb @@ -13,8 +13,8 @@ class ColorSchemeRevisor def revise ColorScheme.transaction do - - @color_scheme.name = @params[:name] if @params.has_key?(:name) + @color_scheme.name = @params[:name] if @params.has_key?(:name) + @color_scheme.user_selectable = @params[:user_selectable] if @params.has_key?(:user_selectable) @color_scheme.base_scheme_id = @params[:base_scheme_id] if @params.has_key?(:base_scheme_id) has_colors = @params[:colors] @@ -31,6 +31,7 @@ class ColorSchemeRevisor if has_colors || @color_scheme.saved_change_to_name? || + @color_scheme.will_save_change_to_user_selectable? || @color_scheme.saved_change_to_base_scheme_id? @color_scheme.save diff --git a/app/services/user_updater.rb b/app/services/user_updater.rb index 913e5ee7cd0..713fc2b9999 100644 --- a/app/services/user_updater.rb +++ b/app/services/user_updater.rb @@ -26,6 +26,7 @@ class UserUpdater :external_links_in_new_tab, :enable_quoting, :enable_defer, + :color_scheme_id, :dark_scheme_id, :dynamic_favicon, :automatically_unpin_topics, diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 072638439e5..c8d2915cdba 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -935,6 +935,16 @@ en: not_first_time: "Not your first time?" skip_link: "Skip these tips" theme_default_on_all_devices: "Make this the default theme on all my devices" + color_scheme_default_on_all_devices: "Set default color scheme(s) on all my devices" + color_scheme: "Color Scheme" + color_schemes: + default_description: "Default" + disable_dark_scheme: "Same as regular" + dark_instructions: "You can preview the dark mode color scheme by toggling your device's dark mode." + undo: "Reset" + regular: "Regular" + dark: "Dark mode" + default_dark_scheme: "(site default)" dark_mode: "Dark Mode" dark_mode_enable: "Enable automatic dark mode color scheme" text_size_default_on_all_devices: "Make this the default text size on all my devices" @@ -3886,6 +3896,7 @@ en: hide_unused_fields: "Hide unused fields" is_default: "Theme is enabled by default" user_selectable: "Theme can be selected by users" + color_scheme_user_selectable: "Color scheme can be selected by users" color_scheme: "Color Palette" default_light_scheme: "Light (default)" color_scheme_select: "Select colors to be used by theme" diff --git a/config/routes.rb b/config/routes.rb index bdf42bc0c30..47bc641eb5f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -518,6 +518,7 @@ Discourse::Application.routes.draw do get "stylesheets/:name.css.map" => "stylesheets#show_source_map", constraints: { name: /[-a-z0-9_]+/ } get "stylesheets/:name.css" => "stylesheets#show", constraints: { name: /[-a-z0-9_]+/ } + get "color-scheme-stylesheet/:id(/:theme_id)" => "stylesheets#color_scheme", constraints: { format: :json } get "theme-javascripts/:digest.js" => "theme_javascripts#show", constraints: { digest: /\h{40}/ } post "uploads/lookup-metadata" => "uploads#metadata" diff --git a/db/migrate/20200819021210_add_user_selectable_column_to_color_schemes.rb b/db/migrate/20200819021210_add_user_selectable_column_to_color_schemes.rb new file mode 100644 index 00000000000..e9add3e262c --- /dev/null +++ b/db/migrate/20200819021210_add_user_selectable_column_to_color_schemes.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddUserSelectableColumnToColorSchemes < ActiveRecord::Migration[6.0] + def change + add_column :color_schemes, :user_selectable, :bool, null: false, default: false + end +end diff --git a/db/migrate/20200819203846_add_color_scheme_id_to_user_options.rb b/db/migrate/20200819203846_add_color_scheme_id_to_user_options.rb new file mode 100644 index 00000000000..fc700e5aa69 --- /dev/null +++ b/db/migrate/20200819203846_add_color_scheme_id_to_user_options.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddColorSchemeIdToUserOptions < ActiveRecord::Migration[6.0] + def change + add_column :user_options, :color_scheme_id, :integer + end +end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 257acb53c37..ad3a7a3239c 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -374,11 +374,44 @@ describe ApplicationHelper do expect(cs_stylesheets).not_to include("(prefers-color-scheme: dark)") end - context "with a user option" do + context "custom light scheme" do + before do + @new_cs = Fabricate(:color_scheme, name: 'Flamboyant') + user.user_option.color_scheme_id = @new_cs.id + user.user_option.save! + helper.request.env[Auth::DefaultCurrentUserProvider::CURRENT_USER_KEY] = user + end + + it "returns color scheme from user option value" do + color_stylesheets = helper.discourse_color_scheme_stylesheets + expect(color_stylesheets).to include("color_definitions_flamboyant") + end + + it "returns color scheme from cookie value" do + cs = ColorScheme.where(name: "Dark").first + helper.request.cookies["color_scheme_id"] = cs.id + + color_stylesheets = helper.discourse_color_scheme_stylesheets + + expect(color_stylesheets).to include("color_definitions_dark") + expect(color_stylesheets).not_to include("color_definitions_flamboyant") + end + + it "falls back to base scheme with invalid cookie value" do + helper.request.cookies["color_scheme_id"] = -50 + + color_stylesheets = helper.discourse_color_scheme_stylesheets + expect(color_stylesheets).not_to include("color_definitions_flamboyant") + expect(color_stylesheets).to include("color_definitions_base") + end + end + + context "dark scheme with user option and/or cookies" do before do user.user_option.dark_scheme_id = -1 user.user_option.save! helper.request.env[Auth::DefaultCurrentUserProvider::CURRENT_USER_KEY] = user + @new_cs = Fabricate(:color_scheme, name: 'Custom Color Scheme') SiteSetting.default_dark_mode_color_scheme_id = ColorScheme.where(name: "Dark").pluck(:id).first end @@ -391,14 +424,28 @@ describe ApplicationHelper do end it "returns user-selected dark color scheme stylesheet" do - new_cs = Fabricate(:color_scheme, name: 'Custom Color Scheme') - user.user_option.update!(dark_scheme_id: new_cs.id) + user.user_option.update!(dark_scheme_id: @new_cs.id) color_stylesheets = helper.discourse_color_scheme_stylesheets expect(color_stylesheets).to include("(prefers-color-scheme: dark)") expect(color_stylesheets).to include("custom-color-scheme") end + it "respects cookie value over user option for dark color scheme" do + helper.request.cookies["dark_scheme_id"] = @new_cs.id + + color_stylesheets = helper.discourse_color_scheme_stylesheets + expect(color_stylesheets).to include("(prefers-color-scheme: dark)") + expect(color_stylesheets).to include("custom-color-scheme") + end + + it "returns no dark scheme with invalid cookie value" do + helper.request.cookies["dark_scheme_id"] = -10 + + color_stylesheets = helper.discourse_color_scheme_stylesheets + expect(color_stylesheets).not_to include("(prefers-color-scheme: dark)") + end + end end diff --git a/spec/models/color_scheme_spec.rb b/spec/models/color_scheme_spec.rb index 7035f06b44c..1add1fa77ad 100644 --- a/spec/models/color_scheme_spec.rb +++ b/spec/models/color_scheme_spec.rb @@ -96,5 +96,10 @@ describe ColorScheme do ColorSchemeRevisor.revise(scheme, colors: [{ name: 'primary', hex: 'F8F8F8' }, { name: 'secondary', hex: '232323' }]) expect(scheme.is_dark?).to eq(true) end + + it "does not break in scheme without colors" do + scheme = ColorScheme.create(name: "No Bueno") + expect(scheme.is_dark?).to eq(nil) + end end end diff --git a/spec/models/site_spec.rb b/spec/models/site_spec.rb index c97aafe10b6..cf8a8ba9cc5 100644 --- a/spec/models/site_spec.rb +++ b/spec/models/site_spec.rb @@ -11,8 +11,8 @@ describe Site do expected = Theme.where('id = :default OR user_selectable', default: SiteSetting.default_theme_id) .order(:name) - .pluck(:id, :name) - .map { |id, n| { "theme_id" => id, "name" => n, "default" => id == SiteSetting.default_theme_id } } + .pluck(:id, :name, :color_scheme_id) + .map { |id, n, cs| { "theme_id" => id, "name" => n, "default" => id == SiteSetting.default_theme_id, "color_scheme_id" => cs } } expect(parsed["user_themes"]).to eq(expected) end diff --git a/spec/requests/stylesheets_controller_spec.rb b/spec/requests/stylesheets_controller_spec.rb index 38f06152877..04926798acf 100644 --- a/spec/requests/stylesheets_controller_spec.rb +++ b/spec/requests/stylesheets_controller_spec.rb @@ -57,4 +57,26 @@ describe StylesheetsController do expect(response.status).to eq(200) end + + context "#color_scheme" do + it 'works as expected' do + scheme = ColorScheme.last + get "/color-scheme-stylesheet/#{scheme.id}.json" + + expect(response.status).to eq(200) + json = JSON.parse(response.body) + expect(json["color_scheme_id"]).to eq(scheme.id) + end + + it 'works with a theme parameter' do + scheme = ColorScheme.last + theme = Theme.last + get "/color-scheme-stylesheet/#{scheme.id}/#{theme.id}.json" + + expect(response.status).to eq(200) + json = JSON.parse(response.body) + expect(json["color_scheme_id"]).to eq(scheme.id) + end + + end end diff --git a/spec/serializers/site_serializer_spec.rb b/spec/serializers/site_serializer_spec.rb index 22a55ced834..bc9787a267f 100644 --- a/spec/serializers/site_serializer_spec.rb +++ b/spec/serializers/site_serializer_spec.rb @@ -28,4 +28,33 @@ describe SiteSerializer do expect(categories[0][:notification_level]).to eq(0) expect(categories[-1][:notification_level]).to eq(1) end + + it "includes user-selectable color schemes" do + scheme = ColorScheme.create_from_base(name: "Neutral", base_scheme_id: "Neutral") + scheme.user_selectable = true + scheme.save! + + serialized = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json + expect(serialized[:user_color_schemes].count).to eq (1) + + dark_scheme = ColorScheme.create_from_base(name: "ADarkScheme", base_scheme_id: "Dark") + dark_scheme.user_selectable = true + dark_scheme.save! + + serialized = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json + expect(serialized[:user_color_schemes].count).to eq(2) + expect(serialized[:user_color_schemes][0][:is_dark]).to eq(true) + end + + it "includes default dark mode scheme" do + scheme = ColorScheme.last + SiteSetting.default_dark_mode_color_scheme_id = scheme.id + serialized = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json + default_dark_scheme = + expect(serialized[:default_dark_color_scheme]["name"]).to eq(scheme.name) + + SiteSetting.default_dark_mode_color_scheme_id = -1 + serialized = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json + expect(serialized[:default_dark_color_scheme]).to eq(nil) + end end diff --git a/spec/services/color_scheme_revisor_spec.rb b/spec/services/color_scheme_revisor_spec.rb index 2c7d8590d75..c078481ce7e 100644 --- a/spec/services/color_scheme_revisor_spec.rb +++ b/spec/services/color_scheme_revisor_spec.rb @@ -48,6 +48,14 @@ describe ColorSchemeRevisor do }.to_not change { color_scheme.reload.version } expect(color_scheme.colors.first.hex).to eq(color.hex) end + + it "can change the user_selectable column" do + expect(color_scheme.user_selectable).to eq(false) + + ColorSchemeRevisor.revise(color_scheme, { user_selectable: true }) + expect(color_scheme.reload.user_selectable).to eq(true) + end + end end diff --git a/test/javascripts/acceptance/preferences-test.js b/test/javascripts/acceptance/preferences-test.js index e0e3ceaf0c8..9b5b3b3c6ee 100644 --- a/test/javascripts/acceptance/preferences-test.js +++ b/test/javascripts/acceptance/preferences-test.js @@ -2,7 +2,6 @@ import I18n from "I18n"; import { acceptance, updateCurrentUser } from "helpers/qunit-helpers"; import selectKit from "helpers/select-kit-helper"; import User from "discourse/models/user"; -import cookie, { removeCookie } from "discourse/lib/cookie"; function preferencesPretender(server, helper) { server.post("/u/second_factors.json", () => { @@ -122,51 +121,6 @@ QUnit.test("update some fields", async assert => { ); }); -QUnit.test("font size change", async assert => { - removeCookie("text_size"); - - const savePreferences = async () => { - assert.ok(!exists(".saved"), "it hasn't been saved yet"); - await click(".save-changes"); - assert.ok(exists(".saved"), "it displays the saved message"); - find(".saved").remove(); - }; - - await visit("/u/eviltrout/preferences/interface"); - - // Live changes without reload - await selectKit(".text-size .combobox").expand(); - await selectKit(".text-size .combobox").selectRowByValue("larger"); - assert.ok(document.documentElement.classList.contains("text-size-larger")); - - await selectKit(".text-size .combobox").expand(); - await selectKit(".text-size .combobox").selectRowByValue("largest"); - assert.ok(document.documentElement.classList.contains("text-size-largest")); - - assert.equal(cookie("text_size"), null, "cookie is not set"); - - // Click save (by default this sets for all browsers, no cookie) - await savePreferences(); - - assert.equal(cookie("text_size"), null, "cookie is not set"); - - await selectKit(".text-size .combobox").expand(); - await selectKit(".text-size .combobox").selectRowByValue("larger"); - await click(".text-size input[type=checkbox]"); - - await savePreferences(); - - assert.equal(cookie("text_size"), "larger|1", "cookie is set"); - await click(".text-size input[type=checkbox]"); - await selectKit(".text-size .combobox").expand(); - await selectKit(".text-size .combobox").selectRowByValue("largest"); - - await savePreferences(); - assert.equal(cookie("text_size"), null, "cookie is removed"); - - removeCookie("text_size"); -}); - QUnit.test("username", async assert => { await visit("/u/eviltrout/preferences/username"); assert.ok(exists("#change_username"), "it has the input element"); @@ -483,16 +437,3 @@ QUnit.test("can select an option from a dropdown", async assert => { await field.selectRowByValue("Cat"); assert.equal(field.header().value(), "Cat", "it sets the value of the field"); }); - -acceptance("User Preferences disabling dark mode", { - loggedIn: true, - settings: { default_dark_mode_color_scheme_id: 1 } -}); - -QUnit.test("shows option to disable dark mode", async assert => { - await visit("/u/eviltrout/preferences/interface"); - assert.ok( - $(".control-group.dark-mode").length, - "it has the option to disable dark mode" - ); -}); diff --git a/test/javascripts/acceptance/user-preferences-interface-test.js b/test/javascripts/acceptance/user-preferences-interface-test.js new file mode 100644 index 00000000000..1fe2586832d --- /dev/null +++ b/test/javascripts/acceptance/user-preferences-interface-test.js @@ -0,0 +1,206 @@ +import { acceptance } from "helpers/qunit-helpers"; +import selectKit from "helpers/select-kit-helper"; +import Site from "discourse/models/site"; +import Session from "discourse/models/session"; +import cookie, { removeCookie } from "discourse/lib/cookie"; + +acceptance("User Preferences - Interface", { + loggedIn: true +}); + +QUnit.test("font size change", async assert => { + removeCookie("text_size"); + + const savePreferences = async () => { + assert.ok(!exists(".saved"), "it hasn't been saved yet"); + await click(".save-changes"); + assert.ok(exists(".saved"), "it displays the saved message"); + find(".saved").remove(); + }; + + await visit("/u/eviltrout/preferences/interface"); + + // Live changes without reload + await selectKit(".text-size .combobox").expand(); + await selectKit(".text-size .combobox").selectRowByValue("larger"); + assert.ok(document.documentElement.classList.contains("text-size-larger")); + + await selectKit(".text-size .combobox").expand(); + await selectKit(".text-size .combobox").selectRowByValue("largest"); + assert.ok(document.documentElement.classList.contains("text-size-largest")); + + assert.equal(cookie("text_size"), null, "cookie is not set"); + + // Click save (by default this sets for all browsers, no cookie) + await savePreferences(); + + assert.equal(cookie("text_size"), null, "cookie is not set"); + + await selectKit(".text-size .combobox").expand(); + await selectKit(".text-size .combobox").selectRowByValue("larger"); + await click(".text-size input[type=checkbox]"); + + await savePreferences(); + + assert.equal(cookie("text_size"), "larger|1", "cookie is set"); + await click(".text-size input[type=checkbox]"); + await selectKit(".text-size .combobox").expand(); + await selectKit(".text-size .combobox").selectRowByValue("largest"); + + await savePreferences(); + assert.equal(cookie("text_size"), null, "cookie is removed"); + + removeCookie("text_size"); +}); + +QUnit.test( + "does not show option to disable dark mode by default", + async assert => { + await visit("/u/eviltrout/preferences/interface"); + assert.equal($(".control-group.dark-mode").length, 0); + } +); + +QUnit.test("shows light/dark color scheme pickers", async assert => { + let site = Site.current(); + site.set("user_color_schemes", [ + { id: 2, name: "Cool Breeze" }, + { id: 3, name: "Dark Night", is_dark: true } + ]); + + await visit("/u/eviltrout/preferences/interface"); + assert.ok($(".light-color-scheme").length, "has regular dropdown"); + assert.ok($(".dark-color-scheme").length, "has dark color scheme dropdown"); +}); + +function interfacePretender(server, helper) { + server.get("/color-scheme-stylesheet/2.json", () => { + return helper.response({ + success: "OK" + }); + }); +} + +acceptance("User Preferences Color Schemes (with default dark scheme)", { + loggedIn: true, + settings: { default_dark_mode_color_scheme_id: 1 }, + pretend: interfacePretender +}); + +QUnit.test("show option to disable dark mode", async assert => { + await visit("/u/eviltrout/preferences/interface"); + + assert.ok( + $(".control-group.dark-mode").length, + "it has the option to disable dark mode" + ); +}); + +QUnit.test("no color scheme picker by default", async assert => { + let site = Site.current(); + site.set("user_color_schemes", []); + + await visit("/u/eviltrout/preferences/interface"); + assert.equal($(".control-group.color-scheme").length, 0); +}); + +QUnit.test("light color scheme picker", async assert => { + let site = Site.current(); + site.set("user_color_schemes", [{ id: 2, name: "Cool Breeze" }]); + + await visit("/u/eviltrout/preferences/interface"); + assert.ok($(".light-color-scheme").length, "has regular picker dropdown"); + assert.equal( + $(".dark-color-scheme").length, + 0, + "does not have a dark color scheme picker" + ); +}); + +QUnit.test("light and dark color scheme pickers", async assert => { + let site = Site.current(); + let session = Session.current(); + session.userDarkSchemeId = 1; // same as default set in site settings + + site.set("default_dark_color_scheme", { id: 1, name: "Dark" }); + site.set("user_color_schemes", [ + { id: 2, name: "Cool Breeze" }, + { id: 3, name: "Dark Night", is_dark: true } + ]); + + const savePreferences = async () => { + assert.ok(!exists(".saved"), "it hasn't been saved yet"); + await click(".save-changes"); + assert.ok(exists(".saved"), "it displays the saved message"); + find(".saved").remove(); + }; + + await visit("/u/eviltrout/preferences/interface"); + assert.ok($(".light-color-scheme").length, "has regular dropdown"); + assert.ok($(".dark-color-scheme").length, "has dark color scheme dropdown"); + assert.equal( + $(".dark-color-scheme .selected-name").data("value"), + session.userDarkSchemeId, + "sets site default as selected dark scheme" + ); + assert.equal( + $(".control-group.dark-mode").length, + 0, + "it does not show disable dark mode checkbox" + ); + + removeCookie("color_scheme_id"); + removeCookie("dark_scheme_id"); + + await selectKit(".light-color-scheme .combobox").expand(); + await selectKit(".light-color-scheme .combobox").selectRowByValue(2); + assert.equal(cookie("color_scheme_id"), null, "cookie is not set"); + assert.ok( + exists(".color-scheme-checkbox input:checked"), + "defaults to storing values in user options" + ); + + await savePreferences(); + assert.equal(cookie("color_scheme_id"), null, "cookie is unchanged"); + + // Switch to saving changes in cookies + await click(".color-scheme-checkbox input[type=checkbox]"); + await savePreferences(); + assert.equal(cookie("color_scheme_id"), 2, "cookie is set"); + + // dark scheme + await selectKit(".dark-color-scheme .combobox").expand(); + assert.ok( + selectKit(".dark-color-scheme .combobox") + .rowByValue(1) + .exists(), + "default dark scheme is included" + ); + + await selectKit(".dark-color-scheme .combobox").selectRowByValue(-1); + assert.equal( + cookie("dark_scheme_id"), + null, + "cookie is not set before saving" + ); + + await savePreferences(); + assert.equal(cookie("dark_scheme_id"), -1, "cookie is set"); + + await click("button.undo-preview"); + assert.equal( + selectKit(".light-color-scheme .combobox") + .header() + .value(), + null, + "resets light scheme dropdown" + ); + + assert.equal( + selectKit(".dark-color-scheme .combobox") + .header() + .value(), + session.userDarkSchemeId, + "resets dark scheme dropdown" + ); +});
+ {{i18n "admin.customize.color"}} + {{#unless model.theme_id}} + + {{/unless}} +