discourse/app/models/color_scheme.rb
2022-07-08 16:46:32 -05:00

481 lines
14 KiB
Ruby

# frozen_string_literal: true
class ColorScheme < ActiveRecord::Base
# rubocop:disable Layout/HashAlignment
CUSTOM_SCHEMES = {
'Dark': {
"primary" => 'dddddd',
"secondary" => '222222',
"tertiary" => '0f82af',
"quaternary" => 'c14924',
"header_background" => '111111',
"header_primary" => 'dddddd',
"highlight" => 'a87137',
"danger" => 'e45735',
"success" => '1ca551',
"love" => 'fa6c8d'
},
# By @itsbhanusharma
'Neutral': {
"primary" => '000000',
"secondary" => 'ffffff',
"tertiary" => '51839b',
"quaternary" => 'b85e48',
"header_background" => '333333',
"header_primary" => 'f3f3f3',
"highlight" => 'ecec70',
"danger" => 'b85e48',
"success" => '518751',
"love" => 'fa6c8d'
},
# By @Flower_Child
'Grey Amber': {
"primary" => 'd9d9d9',
"secondary" => '3d4147',
"tertiary" => 'fdd459',
"quaternary" => 'fdd459',
"header_background" => '36393e',
"header_primary" => 'd9d9d9',
"highlight" => 'fdd459',
"danger" => 'e45735',
"success" => 'fdd459',
"love" => 'fdd459'
},
# By @rafafotes
'Shades of Blue': {
"primary" => '203243',
"secondary" => 'eef4f7',
"tertiary" => '416376',
"quaternary" => '5e99b9',
"header_background" => '86bddb',
"header_primary" => '203243',
"highlight" => '86bddb',
"danger" => 'bf3c3c',
"success" => '70db82',
"love" => 'fc94cb'
},
# By @mikechristopher
'Latte': {
"primary" => 'f2e5d7',
"secondary" => '262322',
"tertiary" => 'f7f2ed',
"quaternary" => 'd7c9aa',
"header_background" => 'd7c9aa',
"header_primary" => '262322',
"highlight" => 'd7c9aa',
"danger" => 'db9584',
"success" => '78be78',
"love" => '8f6201'
},
# By @Flower_Child
'Summer': {
"primary" => '874342',
"secondary" => 'fffff4',
"tertiary" => 'fe9896',
"quaternary" => 'fcc9d0',
"header_background" => '96ccbf',
"header_primary" => 'fff1e7',
"highlight" => 'f3c07f',
"danger" => 'cfebdc',
"success" => 'fcb4b5',
"love" => 'f3c07f'
},
# By @Flower_Child
'Dark Rose': {
"primary" => 'ca9cb2',
"secondary" => '3a2a37',
"tertiary" => 'fdd459',
"quaternary" => '7e566a',
"header_background" => 'a97189',
"header_primary" => 'd9b2bb',
"highlight" => '6c3e63',
"danger" => '6c3e63',
"success" => 'd9b2bb',
"love" => 'd9b2bb'
},
"WCAG": {
"primary" => '000000',
"primary-medium" => '696969',
"primary-low-mid" => '909090',
"secondary" => 'ffffff',
"tertiary" => '3369FF',
"quaternary" => '3369FF',
"header_background" => 'ffffff',
"header_primary" => '000000',
"highlight" => '3369FF',
"highlight-high" => '0036E6',
"highlight-medium" => 'e0e9ff',
"highlight-low" => 'e0e9ff',
"danger" => 'BB1122',
"success" => '3d854d',
"love" => '9D256B'
},
"WCAG Dark": {
"primary" => 'ffffff',
"primary-medium" => '999999',
"primary-low-mid" => '888888',
"secondary" => '0c0c0c',
"tertiary" => '759AFF',
"quaternary" => '759AFF',
"header_background" => '000000',
"header_primary" => 'ffffff',
"highlight" => '3369FF',
"danger" => 'BB1122',
"success" => '3d854d',
"love" => '9D256B'
},
# By @zenorocha
"Dracula": {
"primary_very_low" => "373A47",
"primary_low" => "414350",
"primary_low_mid" => "8C8D94",
"primary_medium" => "A3A4AA",
"primary_high" => "CCCCCF",
"primary" => 'f2f2f2',
"secondary_low" => "CCCCCF",
"secondary_medium" => "91939A",
"secondary_high" => "6A6C76",
"secondary_very_high" => "3D404C",
"secondary" => '2d303e',
"tertiary_low" => "4A4463",
"tertiary_medium" => "6E5D92",
"tertiary" => 'bd93f9',
"tertiary_high" => "9275C1",
"quaternary_low" => "6AA8BA",
"quaternary" => '8be9fd',
"header_background" => '373A47',
"header_primary" => 'f2f2f2',
"highlight_low" => "686D55",
"danger_medium" => "B6484D",
"highlight" => 'f1fa8c',
"highlight_high" => "C0C879",
"danger_low" => "957279",
"danger" => 'ff5555',
"success_low" => "386D50",
"success_medium" => "44B366",
"success" => '50fa7b',
"love_low" => "6C4667",
"love" => 'ff79c6'
},
# By @altercation
"Solarized Light": {
"primary_very_low" => "F0ECD7",
"primary_low" => "D6D8C7",
"primary_low_mid" => "A4AFA5",
"primary_medium" => "7E918C",
"primary_high" => "4C6869",
"primary" => '002B36',
"secondary_low" => "325458",
"secondary_medium" => "6C8280",
"secondary_high" => "97A59D",
"secondary_very_high" => "E8E6D3",
"secondary" => 'FCF6E1',
"tertiary_low" => "D6E6DE",
"tertiary_medium" => "7EBFD7",
"tertiary" => '0088cc',
"tertiary_high" => "329ED0",
"quaternary" => 'e45735',
"header_background" => 'FCF6E1',
"header_primary" => '002B36',
"highlight_low" => "FDF9AD",
"highlight_medium" => "E3D0A3",
"highlight" => 'ffff4d',
"highlight_high" => "BCAA7F",
"danger_low" => "F8D9C2",
"danger" => 'e45735',
"success_low" => "CFE5B9",
"success_medium" => "4CB544",
"success" => '009900',
"love_low" => "FCDDD2",
"love" => 'fa6c8d'
},
# By @altercation
"Solarized Dark": {
"primary_very_low" => "0D353F",
"primary_low" => "193F47",
"primary_low_mid" => "798C88",
"primary_medium" => "97A59D",
"primary_high" => "B5BDB1",
"primary" => 'FCF6E1',
"secondary_low" => "B5BDB1",
"secondary_medium" => "81938D",
"secondary_high" => "4E6A6B",
"secondary_very_high" => "143B44",
"secondary" => '002B36',
"tertiary_low" => "003E54",
"tertiary_medium" => "00557A",
"tertiary" => '0088cc',
"tertiary_high" => "006C9F",
"quaternary_low" => "944835",
"quaternary" => 'e45735',
"header_background" => '002B36',
"header_primary" => 'FCF6E1',
"highlight_low" => "4D6B3D",
"highlight_medium" => "464C33",
"highlight" => 'ffff4d',
"highlight_high" => "BFCA47",
"danger_low" => "443836",
"danger_medium" => "944835",
"danger" => 'e45735',
"success_low" => "004C26",
"success_medium" => "007313",
"success" => '009900',
"love_low" => "4B3F50",
"love" => 'fa6c8d',
}
}
# rubocop:enable Layout/HashAlignment
LIGHT_THEME_ID = 'Light'
def self.base_color_scheme_colors
base_with_hash = []
base_colors.each do |name, color|
base_with_hash << { name: name, hex: "#{color}" }
end
list = [
{ id: LIGHT_THEME_ID, colors: base_with_hash }
]
CUSTOM_SCHEMES.each do |k, v|
colors = []
v.each do |name, color|
colors << { name: name, hex: "#{color}" }
end
list.push(id: k.to_s, colors: colors)
end
list
end
def self.hex_cache
@hex_cache ||= DistributedCache.new("scheme_hex_for_name")
end
attr_accessor :is_base
attr_accessor :skip_publish
has_many :color_scheme_colors, -> { order('id ASC') }, dependent: :destroy
alias_method :colors, :color_scheme_colors
before_save :bump_version
after_save_commit :publish_discourse_stylesheet, unless: :skip_publish
after_save_commit :dump_caches
after_destroy :dump_caches
belongs_to :theme
validates_associated :color_scheme_colors
BASE_COLORS_FILE = "#{Rails.root}/app/assets/stylesheets/common/foundation/colors.scss"
COLOR_TRANSFORMATION_FILE = "#{Rails.root}/app/assets/stylesheets/common/foundation/color_transformations.scss"
@mutex = Mutex.new
def self.base_colors
return @base_colors if @base_colors
@mutex.synchronize do
return @base_colors if @base_colors
base_colors = {}
File.readlines(BASE_COLORS_FILE).each do |line|
matches = /\$([\w]+):\s*#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})(?:[;]|\s)/.match(line.strip)
base_colors[matches[1]] = matches[2] if matches
end
@base_colors = base_colors
end
@base_colors
end
def self.color_transformation_variables
return @transformation_variables if @transformation_variables
@mutex.synchronize do
return @transformation_variables if @transformation_variables
transformation_variables = []
File.readlines(COLOR_TRANSFORMATION_FILE).each do |line|
matches = /\$([\w\-_]+):.*/.match(line.strip)
transformation_variables.append(matches[1]) if matches
end
@transformation_variables = transformation_variables
end
@transformation_variables
end
def self.base_color_schemes
base_color_scheme_colors.map do |hash|
scheme = new(name: I18n.t("color_schemes.#{hash[:id].downcase.gsub(' ', '_')}"), base_scheme_id: hash[:id])
scheme.colors = hash[:colors].map { |k| { name: k[:name], hex: k[:hex] } }
scheme.is_base = true
scheme
end
end
def self.base
return @base_color_scheme if @base_color_scheme
@base_color_scheme = new(name: I18n.t('color_schemes.base_theme_name'))
@base_color_scheme.colors = base_colors.map { |name, hex| { name: name, hex: hex } }
@base_color_scheme.is_base = true
@base_color_scheme
end
def self.is_base?(scheme_name)
base_color_scheme_colors.map { |c| c[:id] }.include?(scheme_name)
end
# create_from_base will create a new ColorScheme that overrides Discourse's base color scheme with the given colors.
def self.create_from_base(params)
new_color_scheme = new(name: params[:name])
new_color_scheme.via_wizard = true if params[:via_wizard]
new_color_scheme.base_scheme_id = params[:base_scheme_id]
new_color_scheme.user_selectable = true
colors = CUSTOM_SCHEMES[params[:base_scheme_id].to_sym]&.map do |name, hex|
{ name: name, hex: hex }
end if params[:base_scheme_id]
colors ||= base.colors_hashes
# Override base values
params[:colors].each do |name, hex|
c = colors.find { |x| x[:name].to_s == name.to_s }
c[:hex] = hex
end if params[:colors]
new_color_scheme.colors = colors
new_color_scheme.skip_publish if params[:skip_publish]
new_color_scheme.save
new_color_scheme
end
def self.lookup_hex_for_name(name, scheme_id = nil)
enabled_color_scheme = find_by(id: scheme_id) if scheme_id
enabled_color_scheme ||= Theme.where(id: SiteSetting.default_theme_id).first&.color_scheme
(enabled_color_scheme || base).colors.find { |c| c.name == name }.try(:hex) || "nil"
end
def self.hex_for_name(name, scheme_id = nil)
cache_key = scheme_id ? name + "_#{scheme_id}" : name
hex_cache[cache_key] ||= lookup_hex_for_name(name, scheme_id)
hex_cache[cache_key] == "nil" ? nil : hex_cache[cache_key]
end
def colors=(arr)
@colors_by_name = nil
arr.each do |c|
self.color_scheme_colors << ColorSchemeColor.new(name: c[:name], hex: c[:hex])
end
end
def colors_by_name
@colors_by_name ||= self.colors.inject({}) { |sum, c| sum[c.name] = c; sum; }
end
def clear_colors_cache
@colors_by_name = nil
end
def colors_hashes
color_scheme_colors.map do |c|
{ name: c.name, hex: c.hex }
end
end
def base_colors
colors = nil
if base_scheme_id && base_scheme_id != "Light"
colors = CUSTOM_SCHEMES[base_scheme_id.to_sym]
end
colors || ColorScheme.base_colors
end
def resolved_colors
resolved = ColorScheme.base_colors.dup
if base_scheme_id && base_scheme_id != "Light"
if scheme = CUSTOM_SCHEMES[base_scheme_id.to_sym]
scheme.each do |name, value|
resolved[name] = value
end
end
end
colors.each do |c|
resolved[c.name] = c.hex
end
resolved
end
def publish_discourse_stylesheet
if self.id
self.class.publish_discourse_stylesheets!(self.id)
end
end
def self.publish_discourse_stylesheets!(id = nil)
Stylesheet::Manager.clear_color_scheme_cache!
theme_ids = []
if id
theme_ids = Theme.where(color_scheme_id: id).pluck(:id)
else
theme_ids = Theme.all.pluck(:id)
end
if theme_ids.present?
Stylesheet::Manager.cache.clear
Theme.notify_theme_change(
theme_ids,
with_scheme: true,
clear_manager_cache: false,
all_themes: true
)
end
end
def dump_caches
self.class.hex_cache.clear
ApplicationSerializer.expire_cache_fragment!("user_color_schemes")
end
def bump_version
if self.id
self.version += 1
end
end
def is_dark?
return if colors.to_a.empty?
primary_b = brightness(colors_by_name["primary"].hex)
secondary_b = brightness(colors_by_name["secondary"].hex)
primary_b > secondary_b
end
def is_wcag?
base_scheme_id&.start_with?('WCAG')
end
# Equivalent to dc-color-brightness() in variables.scss
def brightness(color)
rgb = color.scan(/../).map { |c| c.to_i(16) }
(rgb[0].to_i * 299 + rgb[1].to_i * 587 + rgb[2].to_i * 114) / 1000.0
end
end
# == Schema Information
#
# 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
# user_selectable :boolean default(FALSE), not null
#