From f54fbd057ebb8d1e9631782b6d182b403598648f Mon Sep 17 00:00:00 2001 From: psvenk <45520974+psvenk@users.noreply.github.com> Date: Thu, 15 Aug 2019 16:29:55 +0000 Subject: [PATCH] Add prefers-color-scheme support (#601) * Add prefers-color-scheme support This should fix . The cookie storage format has been changed from boolean ("true"/"false") to tri-state ("dark"/"light"/""), so that users without a cookie set will get dark mode if they have enabled the dark theme in their operating system. The code for handling the cookie state, along with the user's operating system theme, has been factored out into a new function `update_mode`, which is called both at window load and at the "storage" event listener, because the "storage" event listener is only trigerred when a change is made to the localStorage from another tab/window (for more info - see ). --- assets/js/themes.js | 37 ++++++++++--- locales/ar.json | 4 ++ locales/de.json | 4 ++ locales/el.json | 4 ++ locales/en-US.json | 4 ++ locales/eo.json | 4 ++ locales/es.json | 4 ++ locales/eu.json | 4 ++ locales/fr.json | 4 ++ locales/is.json | 4 ++ locales/it.json | 4 ++ locales/nb_NO.json | 4 ++ locales/nl.json | 4 ++ locales/pl.json | 4 ++ locales/ru.json | 4 ++ locales/uk.json | 4 ++ locales/zh-CN.json | 4 ++ src/invidious.cr | 30 ++++++++--- src/invidious/helpers/helpers.cr | 74 ++++++++++++++++++++++++-- src/invidious/helpers/patch_mapping.cr | 2 +- src/invidious/helpers/utils.cr | 13 +++++ src/invidious/users.cr | 62 ++------------------- src/invidious/views/preferences.ecr | 8 ++- src/invidious/views/template.ecr | 9 ++-- 24 files changed, 215 insertions(+), 84 deletions(-) diff --git a/assets/js/themes.js b/assets/js/themes.js index 683aea39..90a05c36 100644 --- a/assets/js/themes.js +++ b/assets/js/themes.js @@ -1,8 +1,8 @@ -var toggle_theme = document.getElementById('toggle_theme') +var toggle_theme = document.getElementById('toggle_theme'); toggle_theme.href = 'javascript:void(0);'; toggle_theme.addEventListener('click', function () { - var dark_mode = document.getElementById('dark_theme').media == 'none'; + var dark_mode = document.getElementById('dark_theme').media === 'none'; var url = '/toggle_theme?redirect=false'; var xhr = new XMLHttpRequest(); @@ -11,19 +11,24 @@ toggle_theme.addEventListener('click', function () { xhr.open('GET', url, true); set_mode(dark_mode); - localStorage.setItem('dark_mode', dark_mode); + window.localStorage.setItem('dark_mode', dark_mode ? 'dark' : 'light'); xhr.send(); }); window.addEventListener('storage', function (e) { - if (e.key == 'dark_mode') { - var dark_mode = e.newValue === 'true'; - set_mode(dark_mode); + if (e.key === 'dark_mode') { + update_mode(e.newValue); } }); -function set_mode(bool) { +window.addEventListener('load', function () { + window.localStorage.setItem('dark_mode', document.getElementById('dark_mode_pref').textContent); + // Update localStorage if dark mode preference changed on preferences page + update_mode(window.localStorage.dark_mode); +}); + +function set_mode (bool) { document.getElementById('dark_theme').media = !bool ? 'none' : ''; document.getElementById('light_theme').media = bool ? 'none' : ''; @@ -33,3 +38,21 @@ function set_mode(bool) { toggle_theme.children[0].setAttribute('class', 'icon ion-ios-moon'); } } + +function update_mode (mode) { + if (mode === 'true' /* for backwards compatibility */ || mode === 'dark') { + // If preference for dark mode indicated + set_mode(true); + } + else if (mode === 'false' /* for backwards compaibility */ || mode === 'light') { + // If preference for light mode indicated + set_mode(false); + } + else if (document.getElementById('dark_mode_pref').textContent === '' && window.matchMedia('(prefers-color-scheme: dark)').matches) { + // If no preference indicated here and no preference indicated on the preferences page (backend), but the browser tells us that the operating system has a dark theme + set_mode(true); + } + // else do nothing, falling back to the mode defined by the `dark_mode` preference on the preferences page (backend) +} + + diff --git a/locales/ar.json b/locales/ar.json index e3716008..f2ed450c 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -68,7 +68,11 @@ "Show related videos: ": "عرض مقاطع الفيديو ذات الصلة؟", "Show annotations by default: ": "عرض الملاحظات فى الفيديو تلقائيا ؟", "Visual preferences": "التفضيلات المرئية", + "Player style: ": "", "Dark mode: ": "الوضع الليلى: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "الوضع الخفيف: ", "Subscription preferences": "تفضيلات الإشتراك", "Show annotations by default for subscribed channels: ": "عرض الملاحظات فى الفيديوهات تلقائيا فى القنوات المشترك بها فقط ؟", diff --git a/locales/de.json b/locales/de.json index 33edb706..e0ef9d67 100644 --- a/locales/de.json +++ b/locales/de.json @@ -68,7 +68,11 @@ "Show related videos: ": "Ähnliche Videos anzeigen? ", "Show annotations by default: ": "Standardmäßig Anmerkungen anzeigen? ", "Visual preferences": "Anzeigeeinstellungen", + "Player style: ": "", "Dark mode: ": "Nachtmodus: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Schlanker Modus: ", "Subscription preferences": "Abonnementeinstellungen", "Show annotations by default for subscribed channels: ": "Anmerkungen für abonnierte Kanäle standardmäßig anzeigen? ", diff --git a/locales/el.json b/locales/el.json index 03d25533..32f154a1 100644 --- a/locales/el.json +++ b/locales/el.json @@ -74,7 +74,11 @@ "Show related videos: ": "Προβολή σχετικών βίντεο; ", "Show annotations by default: ": "Αυτόματη προβολή σημειώσεων; :", "Visual preferences": "Προτιμήσεις εμφάνισης", + "Player style: ": "", "Dark mode: ": "Σκοτεινή λειτουργία: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Ελαφριά λειτουργία: ", "Subscription preferences": "Προτιμήσεις συνδρομών", "Show annotations by default for subscribed channels: ": "Προβολή σημειώσεων μόνο για κανάλια στα οποία είστε συνδρομητής; ", diff --git a/locales/en-US.json b/locales/en-US.json index 16301b1d..580d9ead 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -74,7 +74,11 @@ "Show related videos: ": "Show related videos: ", "Show annotations by default: ": "Show annotations by default: ", "Visual preferences": "Visual preferences", + "Player style: ": "", "Dark mode: ": "Dark mode: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Thin mode: ", "Subscription preferences": "Subscription preferences", "Show annotations by default for subscribed channels: ": "Show annotations by default for subscribed channels? ", diff --git a/locales/eo.json b/locales/eo.json index fe85edf2..2c22af59 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -68,7 +68,11 @@ "Show related videos: ": "Ĉu montri rilatajn videojn? ", "Show annotations by default: ": "Ĉu montri prinotojn defaŭlte? ", "Visual preferences": "Vidaj preferoj", + "Player style: ": "", "Dark mode: ": "Malhela reĝimo: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Maldika reĝimo: ", "Subscription preferences": "Abonaj agordoj", "Show annotations by default for subscribed channels: ": "Ĉu montri prinotojn defaŭlte por abonitaj kanaloj? ", diff --git a/locales/es.json b/locales/es.json index fdbf2fb2..5860b882 100644 --- a/locales/es.json +++ b/locales/es.json @@ -68,7 +68,11 @@ "Show related videos: ": "¿Mostrar vídeos relacionados? ", "Show annotations by default: ": "¿Mostrar anotaciones por defecto? ", "Visual preferences": "Preferencias visuales", + "Player style: ": "", "Dark mode: ": "Modo oscuro: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Modo compacto: ", "Subscription preferences": "Preferencias de la suscripción", "Show annotations by default for subscribed channels: ": "¿Mostrar anotaciones por defecto para los canales suscritos? ", diff --git a/locales/eu.json b/locales/eu.json index 5a756154..cbdbbefc 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -68,7 +68,11 @@ "Show related videos: ": "", "Show annotations by default: ": "", "Visual preferences": "", + "Player style: ": "", "Dark mode: ": "", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "", "Subscription preferences": "", "Show annotations by default for subscribed channels: ": "", diff --git a/locales/fr.json b/locales/fr.json index 37a773f1..af561a0c 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -68,7 +68,11 @@ "Show related videos: ": "Voir les vidéos liées : ", "Show annotations by default: ": "Voir les annotations par défaut : ", "Visual preferences": "Préférences du site", + "Player style: ": "", "Dark mode: ": "Mode Sombre : ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Mode Simplifié : ", "Subscription preferences": "Préférences de la page d'abonnements", "Show annotations by default for subscribed channels: ": "Voir les annotations par défaut sur les chaînes suivies : ", diff --git a/locales/is.json b/locales/is.json index 43ba26e9..808063c4 100644 --- a/locales/is.json +++ b/locales/is.json @@ -68,7 +68,11 @@ "Show related videos: ": "Sýna tengd myndbönd? ", "Show annotations by default: ": "Á að sýna glósur sjálfgefið? ", "Visual preferences": "Sjónrænar stillingar", + "Player style: ": "", "Dark mode: ": "Myrkur ham: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Þunnt ham: ", "Subscription preferences": "Áskriftarstillingar", "Show annotations by default for subscribed channels: ": "Á að sýna glósur sjálfgefið fyrir áskriftarrásir? ", diff --git a/locales/it.json b/locales/it.json index bf028c91..7f532d0d 100644 --- a/locales/it.json +++ b/locales/it.json @@ -68,7 +68,11 @@ "Show related videos: ": "Mostra video correlati? ", "Show annotations by default: ": "Mostra le annotazioni per impostazione predefinita? ", "Visual preferences": "Preferenze grafiche", + "Player style: ": "", "Dark mode: ": "Tema scuro: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Modalità per connessioni lente: ", "Subscription preferences": "Preferenze iscrizioni", "Show annotations by default for subscribed channels: ": "", diff --git a/locales/nb_NO.json b/locales/nb_NO.json index 589512d4..f50e2290 100644 --- a/locales/nb_NO.json +++ b/locales/nb_NO.json @@ -68,7 +68,11 @@ "Show related videos: ": "Vis relaterte videoer? ", "Show annotations by default: ": "Vis merknader som forvalg? ", "Visual preferences": "Visuelle innstillinger", + "Player style: ": "", "Dark mode: ": "Mørk drakt: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Tynt modus: ", "Subscription preferences": "Abonnementsinnstillinger", "Show annotations by default for subscribed channels: ": "Vis merknader som forvalg for kanaler det abonneres på? ", diff --git a/locales/nl.json b/locales/nl.json index b24b13b8..3e2c6c64 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -68,7 +68,11 @@ "Show related videos: ": "Gerelateerde video's tonen? ", "Show annotations by default: ": "Standaard annotaties tonen? ", "Visual preferences": "Visuele instellingen", + "Player style: ": "", "Dark mode: ": "Donkere modus: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Smalle modus: ", "Subscription preferences": "Abonnementsinstellingen", "Show annotations by default for subscribed channels: ": "Standaard annotaties tonen voor geabonneerde kanalen? ", diff --git a/locales/pl.json b/locales/pl.json index 550e664b..1e3a2068 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -68,7 +68,11 @@ "Show related videos: ": "Pokaż powiązane filmy? ", "Show annotations by default: ": "", "Visual preferences": "Preferencje Wizualne", + "Player style: ": "", "Dark mode: ": "Ciemny motyw: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Tryb minimalny: ", "Subscription preferences": "Preferencje subskrybcji", "Show annotations by default for subscribed channels: ": "", diff --git a/locales/ru.json b/locales/ru.json index f9cc1e2d..90aa4a3b 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -68,7 +68,11 @@ "Show related videos: ": "Показывать похожие видео? ", "Show annotations by default: ": "Всегда показывать аннотации? ", "Visual preferences": "Настройки сайта", + "Player style: ": "", "Dark mode: ": "Тёмное оформление: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Облегчённое оформление: ", "Subscription preferences": "Настройки подписок", "Show annotations by default for subscribed channels: ": "Всегда показывать аннотации в видео каналов, на которые вы подписаны? ", diff --git a/locales/uk.json b/locales/uk.json index 95e5798d..e537008c 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -68,7 +68,11 @@ "Show related videos: ": "Показувати схожі відео? ", "Show annotations by default: ": "Завжди показувати анотації? ", "Visual preferences": "Налаштування сайту", + "Player style: ": "", "Dark mode: ": "Темне оформлення: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Полегшене оформлення: ", "Subscription preferences": "Налаштування підписок", "Show annotations by default for subscribed channels: ": "Завжди показувати анотації у відео каналів, на які ви підписані? ", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 0a3f53d9..23617d04 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -68,7 +68,11 @@ "Show related videos: ": "显示相关视频?", "Show annotations by default: ": "默认显示视频注释?", "Visual preferences": "视觉选项", + "Player style: ": "", "Dark mode: ": "暗色模式:", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "窄页模式:", "Subscription preferences": "订阅设置", "Show annotations by default for subscribed channels: ": "在订阅频道的视频默认显示注释?", diff --git a/src/invidious.cr b/src/invidious.cr index 16695c5f..712a408f 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -267,8 +267,7 @@ before_all do |env| end end - dark_mode = env.params.query["dark_mode"]? || preferences.dark_mode.to_s - dark_mode = dark_mode == "true" + dark_mode = convert_theme(env.params.query["dark_mode"]?) || preferences.dark_mode.to_s thin_mode = env.params.query["thin_mode"]? || preferences.thin_mode.to_s thin_mode = thin_mode == "true" @@ -1528,8 +1527,7 @@ post "/preferences" do |env| locale ||= CONFIG.default_user_preferences.locale dark_mode = env.params.body["dark_mode"]?.try &.as(String) - dark_mode ||= "off" - dark_mode = dark_mode == "on" + dark_mode ||= CONFIG.default_user_preferences.dark_mode thin_mode = env.params.body["thin_mode"]?.try &.as(String) thin_mode ||= "off" @@ -1553,6 +1551,7 @@ post "/preferences" do |env| notifications_only ||= "off" notifications_only = notifications_only == "on" + # Convert to JSON and back again to take advantage of converters used for compatability preferences = Preferences.from_json({ annotations: annotations, annotations_subscribed: annotations_subscribed, @@ -1648,12 +1647,27 @@ get "/toggle_theme" do |env| if user = env.get? "user" user = user.as(User) preferences = user.preferences - preferences.dark_mode = !preferences.dark_mode - PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", preferences.to_json, user.email) + case preferences.dark_mode + when "dark" + preferences.dark_mode = "light" + else + preferences.dark_mode = "dark" + end + + preferences = preferences.to_json + + PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", preferences, user.email) else preferences = env.get("preferences").as(Preferences) - preferences.dark_mode = !preferences.dark_mode + + case preferences.dark_mode + when "dark" + preferences.dark_mode = "light" + else + preferences.dark_mode = "dark" + end + preferences = preferences.to_json if Kemal.config.ssl || config.https_only @@ -2026,7 +2040,7 @@ post "/data_control" do |env| env.response.puts %() env.response.puts %() env.response.puts %() - if env.get("preferences").as(Preferences).dark_mode + if env.get("preferences").as(Preferences).dark_mode == "dark" env.response.puts %() else env.response.puts %() diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index ce0ded32..03c1654c 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -24,6 +24,27 @@ end struct ConfigPreferences module StringToArray + def self.to_json(value : Array(String), json : JSON::Builder) + json.array do + value.each do |element| + json.string element + end + end + end + + def self.from_json(value : JSON::PullParser) : Array(String) + begin + result = [] of String + value.read_array do + result << HTML.escape(value.read_string[0, 100]) + end + rescue ex + result = [HTML.escape(value.read_string[0, 100]), ""] + end + + result + end + def self.to_yaml(value : Array(String), yaml : YAML::Nodes::Builder) yaml.sequence do value.each do |element| @@ -44,11 +65,11 @@ struct ConfigPreferences node.raise "Expected scalar, not #{item.class}" end - result << item.value + result << HTML.escape(item.value[0, 100]) end rescue ex if node.is_a?(YAML::Nodes::Scalar) - result = [node.value, ""] + result = [HTML.escape(node.value[0, 100]), ""] else result = ["", ""] end @@ -58,6 +79,53 @@ struct ConfigPreferences end end + module BoolToString + def self.to_json(value : String, json : JSON::Builder) + json.string value + end + + def self.from_json(value : JSON::PullParser) : String + begin + result = value.read_string + + if result.empty? + CONFIG.default_user_preferences.dark_mode + else + result + end + rescue ex + result = value.read_bool + + if result + "dark" + else + "light" + end + end + end + + def self.to_yaml(value : String, yaml : YAML::Nodes::Builder) + yaml.scalar value + end + + def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : String + unless node.is_a?(YAML::Nodes::Scalar) + node.raise "Expected sequence, not #{node.class}" + end + + case node.value + when "true" + "dark" + when "false" + "light" + when "" + CONFIG.default_user_preferences.dark_mode + else + node.value + end + end + end + yaml_mapping({ annotations: {type: Bool, default: false}, annotations_subscribed: {type: Bool, default: false}, @@ -66,7 +134,7 @@ struct ConfigPreferences comments: {type: Array(String), default: ["youtube", ""], converter: StringToArray}, continue: {type: Bool, default: false}, continue_autoplay: {type: Bool, default: true}, - dark_mode: {type: Bool, default: false}, + dark_mode: {type: String, default: "", converter: BoolToString}, latest_only: {type: Bool, default: false}, listen: {type: Bool, default: false}, local: {type: Bool, default: false}, diff --git a/src/invidious/helpers/patch_mapping.cr b/src/invidious/helpers/patch_mapping.cr index 8360caa6..e138aa1c 100644 --- a/src/invidious/helpers/patch_mapping.cr +++ b/src/invidious/helpers/patch_mapping.cr @@ -4,7 +4,7 @@ def Object.from_json(string_or_io, default) : self new parser, default end -# Adds configurable 'default' to +# Adds configurable 'default' macro patched_json_mapping(_properties_, strict = false) {% for key, value in _properties_ %} {% _properties_[key] = {type: value} unless value.is_a?(HashLiteral) || value.is_a?(NamedTupleLiteral) %} diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 69aae839..b39f65c5 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -356,3 +356,16 @@ def parse_range(range) return 0_i64, nil end + +def convert_theme(theme) + case theme + when "true" + "dark" + when "false" + "light" + when "", nil + nil + else + theme + end +end diff --git a/src/invidious/users.cr b/src/invidious/users.cr index 35d8a49e..8bd82bf1 100644 --- a/src/invidious/users.cr +++ b/src/invidious/users.cr @@ -31,62 +31,6 @@ struct User end struct Preferences - module StringToArray - def self.to_json(value : Array(String), json : JSON::Builder) - json.array do - value.each do |element| - json.string element - end - end - end - - def self.from_json(value : JSON::PullParser) : Array(String) - begin - result = [] of String - value.read_array do - result << HTML.escape(value.read_string[0, 100]) - end - rescue ex - result = [HTML.escape(value.read_string[0, 100]), ""] - end - - result - end - - def self.to_yaml(value : Array(String), yaml : YAML::Nodes::Builder) - yaml.sequence do - value.each do |element| - yaml.scalar element - end - end - end - - def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Array(String) - begin - unless node.is_a?(YAML::Nodes::Sequence) - node.raise "Expected sequence, not #{node.class}" - end - - result = [] of String - node.nodes.each do |item| - unless item.is_a?(YAML::Nodes::Scalar) - node.raise "Expected scalar, not #{item.class}" - end - - result << HTML.escape(item.value[0, 100]) - end - rescue ex - if node.is_a?(YAML::Nodes::Scalar) - result = [HTML.escape(node.value[0, 100]), ""] - else - result = ["", ""] - end - end - - result - end - end - module ProcessString def self.to_json(value : String, json : JSON::Builder) json.string value @@ -127,11 +71,11 @@ struct Preferences annotations: {type: Bool, default: CONFIG.default_user_preferences.annotations}, annotations_subscribed: {type: Bool, default: CONFIG.default_user_preferences.annotations_subscribed}, autoplay: {type: Bool, default: CONFIG.default_user_preferences.autoplay}, - captions: {type: Array(String), default: CONFIG.default_user_preferences.captions, converter: StringToArray}, - comments: {type: Array(String), default: CONFIG.default_user_preferences.comments, converter: StringToArray}, + captions: {type: Array(String), default: CONFIG.default_user_preferences.captions, converter: ConfigPreferences::StringToArray}, + comments: {type: Array(String), default: CONFIG.default_user_preferences.comments, converter: ConfigPreferences::StringToArray}, continue: {type: Bool, default: CONFIG.default_user_preferences.continue}, continue_autoplay: {type: Bool, default: CONFIG.default_user_preferences.continue_autoplay}, - dark_mode: {type: Bool, default: CONFIG.default_user_preferences.dark_mode}, + dark_mode: {type: String, default: CONFIG.default_user_preferences.dark_mode, converter: ConfigPreferences::BoolToString}, latest_only: {type: Bool, default: CONFIG.default_user_preferences.latest_only}, listen: {type: Bool, default: CONFIG.default_user_preferences.listen}, local: {type: Bool, default: CONFIG.default_user_preferences.local}, diff --git a/src/invidious/views/preferences.ecr b/src/invidious/views/preferences.ecr index b04bcd4d..6ea01fba 100644 --- a/src/invidious/views/preferences.ecr +++ b/src/invidious/views/preferences.ecr @@ -122,8 +122,12 @@ function update_value(element) {
- - checked<% end %>> + +
diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 6272d2be..8d8cec88 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -18,13 +18,14 @@ - media="none"<% end %>> - media="none"<% end %>> + media="none"<% end %>> + media="none"<% end %>> <% locale = LOCALES[env.get("preferences").as(Preferences).locale]? %> +