feat: redmine_tonalmine plugin for default and per-account appearance
Admins set the default mode, contrast and color on Settings > Display, shown as three labelled form rows below the Theme select. Logged-in users' choices are saved on their account and override only the parts they changed; anonymous visitors get the default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
77040e24a8
commit
20b7269df2
@@ -0,0 +1,58 @@
|
||||
module RedmineTonalmine
|
||||
# Validates appearance hashes ({mode, contrast, palette, seed, css}) coming from users and from
|
||||
# the Display settings form. Any part may be missing: it then falls back to the default.
|
||||
module Appearance
|
||||
MODES = %w[auto light dark].freeze
|
||||
CONTRASTS = %w[standard medium high].freeze
|
||||
# Ids must match PALETTES in tonalmine/javascripts/theme.js (tools/palettes checks this).
|
||||
PALETTES = %w[purple blue green orange].freeze
|
||||
KEYS = %w[mode contrast palette seed css].freeze
|
||||
MAX_CSS = 32.kilobytes
|
||||
|
||||
module_function
|
||||
|
||||
# Returns a list of problems; empty when the hash is valid.
|
||||
def errors(hash)
|
||||
prefs = normalize(hash)
|
||||
errors = hash.to_h.filter_map do |key, value|
|
||||
if !KEYS.include?(key.to_s) then "unknown key #{key}"
|
||||
elsif !value.nil? && !value.is_a?(String) then "invalid #{key}"
|
||||
end
|
||||
end
|
||||
errors << 'invalid mode' if prefs['mode'] && !MODES.include?(prefs['mode'])
|
||||
errors << 'invalid contrast' if prefs['contrast'] && !CONTRASTS.include?(prefs['contrast'])
|
||||
errors.concat(custom_errors(prefs))
|
||||
errors
|
||||
end
|
||||
|
||||
# Returns only the valid parts, with string keys.
|
||||
def clean(hash)
|
||||
prefs = normalize(hash)
|
||||
prefs.delete('mode') unless MODES.include?(prefs['mode'])
|
||||
prefs.delete('contrast') unless CONTRASTS.include?(prefs['contrast'])
|
||||
prefs = prefs.except('palette', 'seed', 'css') if custom_errors(prefs).any?
|
||||
prefs.compact
|
||||
end
|
||||
|
||||
# String keys, known keys only, blank values dropped (the settings form posts "" for unset parts).
|
||||
def normalize(hash)
|
||||
hash.to_h.transform_keys(&:to_s).slice(*KEYS).select { |_, value| value.is_a?(String) && value.present? }
|
||||
end
|
||||
|
||||
# palette, seed and css belong together: a custom palette needs its seed and generated CSS.
|
||||
# A built-in palette may keep the last custom color, so the custom swatch can switch back.
|
||||
def custom_errors(prefs)
|
||||
palette, seed, css = prefs.values_at('palette', 'seed', 'css')
|
||||
return [] if palette.nil? && seed.nil? && css.nil?
|
||||
|
||||
required = palette == 'custom'
|
||||
errors = []
|
||||
errors << 'invalid palette' unless required || PALETTES.include?(palette)
|
||||
errors << 'invalid seed' unless (seed.nil? && !required) || seed.to_s.match?(/\A#\h{6}\z/)
|
||||
# The CSS is rendered into a <script> as JSON and applied via style.textContent; refusing
|
||||
# "<" keeps it from ever closing either element.
|
||||
errors << 'invalid css' unless (css.nil? && !required) || (css && !css.include?('<') && css.bytesize <= MAX_CSS)
|
||||
errors
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
module RedmineTonalmine
|
||||
# Hands the default and the current user's appearance to theme.js. Rendered in <head> right
|
||||
# after the theme's scripts, so the colors are applied before the page is painted.
|
||||
class Hooks < Redmine::Hook::ViewListener
|
||||
def view_layouts_base_html_head(context = {})
|
||||
defaults = Appearance.clean(Setting.plugin_redmine_tonalmine)
|
||||
config = {defaults: defaults, user: nil, url: nil}
|
||||
if User.current.logged?
|
||||
user = Appearance.clean(User.current.pref[:tonalmine])
|
||||
# The user's own palette replaces the default one: skip the default's generated CSS.
|
||||
# Admins keep it: the Display settings tab posts the whole default back.
|
||||
defaults.delete('css') if user['palette'] && !User.current.admin?
|
||||
config.update(user: user, url: tonalmine_appearance_path)
|
||||
end
|
||||
# to_json escapes <, > and &, so values can't end the script element.
|
||||
javascript_tag "window.Tonalmine && Tonalmine.configure(#{config.to_json});"
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user