From 20fa4043be49d0f615e1f2617b7ee8e811ec78fd Mon Sep 17 00:00:00 2001 From: jdarwood007 Date: Fri, 23 Jan 2026 18:14:39 -0800 Subject: [PATCH] [3.0] Add support for Dark mode in themes Logic pulled from #7933 Does not add a dark mode to the default theme, but enables it to support it --- Languages/en_US/Profile.php | 2 + Languages/en_US/Themes.php | 10 +++ Sources/Actions/Admin/Themes.php | 46 ++++++++++- Sources/Theme.php | 76 +++++++++++++++--- Themes/default/Themes.template.php | 124 ++++++++++++++++++----------- Themes/default/index.template.php | 28 ++++++- 6 files changed, 225 insertions(+), 61 deletions(-) diff --git a/Languages/en_US/Profile.php b/Languages/en_US/Profile.php index 292a6ee7e2a..ab22589ddd6 100644 --- a/Languages/en_US/Profile.php +++ b/Languages/en_US/Profile.php @@ -644,6 +644,8 @@ $txt['theme_opt_posting'] = 'Posting'; $txt['theme_opt_moderation'] = 'Moderation'; $txt['theme_opt_personal_messages'] = 'Personal Messages'; +$txt['theme_opt_colormode'] = 'Theme Color Mode'; +$txt['theme_opt_variant'] = 'Theme Variant'; $txt['export_profile_data'] = 'Download profile data'; $txt['export_profile_data_desc'] = 'This section allows you to export a copy of your forum profile data to a downloadable file, optionally including your posts and personal messages.
Please note:'; diff --git a/Languages/en_US/Themes.php b/Languages/en_US/Themes.php index 31c14c5ced0..ec63964b2c4 100644 --- a/Languages/en_US/Themes.php +++ b/Languages/en_US/Themes.php @@ -65,6 +65,7 @@ $txt['theme_url_config'] = 'Theme URLs and Configuration'; $txt['theme_variants'] = 'Theme Variants'; +$txt['theme_colormodes'] = 'Theme Color Modes'; $txt['theme_options'] = 'Theme Options and Preferences'; $txt['actual_theme_name'] = 'This theme’s name: '; $txt['actual_theme_dir'] = 'This theme’s directory: '; @@ -73,6 +74,7 @@ $txt['current_theme_style'] = 'This theme’s style: '; $txt['theme_variants_default'] = 'Default theme variant'; +$txt['variant_default'] = 'Default'; $txt['theme_variants_user_disable'] = 'Disable user variant selection'; $txt['site_slogan'] = 'Site slogan'; @@ -164,3 +166,11 @@ // Open Graph $txt['og_image'] = 'Open Graph image'; $txt['og_image_desc'] = 'Suggested size: 175x175px. Open Graph is used for social media sharing.'; + +// Theme Mode (dark, light, system, etc) +$txt['theme_pick_colormode'] = 'Select Color Mode'; +$txt['theme_colormode_default'] = 'Default color mode'; +$txt['theme_colormode_user_disable'] = 'Disable user color mode selection'; +$txt['colormode_light'] = 'Light'; +$txt['colormode_dark'] = 'Dark'; +$txt['colormode_system'] = 'System Default'; diff --git a/Sources/Actions/Admin/Themes.php b/Sources/Actions/Admin/Themes.php index 52a5fab5399..06d7692f284 100644 --- a/Sources/Actions/Admin/Themes.php +++ b/Sources/Actions/Admin/Themes.php @@ -602,7 +602,51 @@ public function setOptions(): void Utils::$context['sub_template'] = 'set_options'; Utils::$context['page_title'] = Lang::getTxt('theme_settings', file: 'Admin'); - Utils::$context['options'] = Utils::$context['theme_options']; + // Check for variants or dark mode + if (!empty(Theme::$current->settings['theme_variants']) || !empty(Theme::$current->settings['has_dark_mode'])) { + Utils::$context['options'] = []; + + // Theme Variants + if (!empty(Theme::$current->settings['theme_variants'])) { + $available_variants = []; + + foreach (Theme::$current->settings['theme_variants'] as $variant) { + $available_variants[$variant] = Lang::$txt['variant_' . $variant] ?? $variant; + } + + Utils::$context['options'][] = Lang::$txt['theme_opt_variant']; + Utils::$context['options'][] = [ + 'id' => 'theme_variant', + 'label' => Lang::$txt['theme_pick_variant'], + 'options' => $available_variants, + 'default' => isset(Theme::$current->settings['default_variant']) && !empty(Theme::$current->settings['default_variant']) ? Theme::$current->settings['default_variant'] : Theme::$current->settings['theme_variants'][0], + 'enabled' => !empty(Theme::$current->settings['theme_variants']), + ]; + } + + // Theme Color Mode + if (!empty(Theme::$current->settings['has_dark_mode'])) { + $available_modes = []; + + foreach (Theme::$current->settings['theme_colormodes'] as $mode) { + $available_modes[$mode] = Lang::$txt['colormode_' . $mode] ?? $mode; + } + + Utils::$context['options'][] = Lang::$txt['theme_opt_colormode']; + Utils::$context['options'][] = [ + 'id' => 'theme_colormode', + 'label' => Lang::$txt['theme_pick_colormode'], + 'options' => $available_modes, + 'default' => isset(Theme::$current->settings['default_colormode']) && !empty(Theme::$current->settings['default_colormode']) ? Theme::$current->settings['default_colormode'] : Theme::$current->settings['theme_colormodes'][0], + 'enabled' => !empty(Theme::$current->settings['has_dark_mode']), + ]; + } + + Utils::$context['options'] = array_merge(Utils::$context['options'], Utils::$context['theme_options']); + } else { + Utils::$context['options'] = Utils::$context['theme_options']; + } + Utils::$context['theme_settings'] = Theme::$current->settings; if (empty($_REQUEST['who'])) { diff --git a/Sources/Theme.php b/Sources/Theme.php index eea7b0e6964..6751776352c 100644 --- a/Sources/Theme.php +++ b/Sources/Theme.php @@ -2157,6 +2157,47 @@ protected function loadCss(): void self::loadCSSFile('noscript.css', ['minimize' => true, 'order_pos' => 1, 'noscript' => true], 'smf_noscript'); } + /** + * Loads the theme mode, if applicable. + */ + protected function loadMode(): void + { + Utils::$context['theme_colormode'] = ''; + + if (!empty($this->settings['has_dark_mode'])) { + // Theme Modes + $this->settings['theme_colormodes'] = ['light', 'system', 'dark']; + + // Overriding - for previews and that ilk. + if (!empty($_REQUEST['mode'])) { + $_SESSION['theme_colormode'] = $_REQUEST['mode']; + + // If the user is logged, save this to their profile + if (User::$me->is_logged && \in_array($_SESSION['theme_colormode'], $this->settings['theme_colormodes'])) { + Db::$db->insert( + 'replace', + '{db_prefix}themes', + ['id_theme' => 'int', 'id_member' => 'int', 'variable' => 'string-255', 'value' => 'string-65534'], + [self::$current->settings['theme_id'], User::$me->id, 'theme_colormode', $_SESSION['theme_colormode']], + ['id_theme', 'id_member', 'variable'], + ); + } + } + + // User selection? + if (empty($this->settings['disable_user_mode']) || User::$me->allowedTo('admin_forum')) { + Utils::$context['theme_colormode'] = !empty($_SESSION['theme_colormode']) && \in_array($_SESSION['theme_colormode'], $this->settings['theme_colormodes']) ? $_SESSION['theme_colormode'] : (!empty($this->options['theme_colormode']) && \in_array($this->options['theme_colormode'], $this->settings['theme_colormodes']) ? $this->options['theme_colormode'] : ''); + } + + // If no color mode, set a default + if (empty(Utils::$context['theme_colormode']) || !\in_array(Utils::$context['theme_colormode'], $this->settings['theme_colormodes'])) { + Utils::$context['theme_colormode'] = !empty($this->settings['default_colormode']) && \in_array($this->settings['default_colormode'], $this->settings['theme_colormodes']) ? $this->settings['default_colormode'] : $this->settings['theme_colormodes'][0]; + } + + self::loadCSSFile('dark.css', ['order_pos' => 2, 'attributes' => (Utils::$context['theme_colormode'] == 'system' ? ['media' => '(prefers-color-scheme: dark)'] : [])], 'smf_dark'); + } + } + /** * Loads the correct theme variant, if applicable. */ @@ -2167,11 +2208,34 @@ protected function loadVariant(): void Utils::$context['theme_variant_url'] = ''; if (!empty($this->settings['theme_variants'])) { + // Add the default variant + $this->settings['theme_variants'] = array_unique(array_merge(['default'], $this->settings['theme_variants'])); + // Overriding - for previews and that ilk. if (!empty($_REQUEST['variant'])) { $_SESSION['id_variant'] = $_REQUEST['variant']; + + // If the user is logged, save this to their profile + if (User::$me->is_logged && \in_array($_SESSION['id_variant'], $this->settings['theme_variants'])) { + Db::$db->insert( + 'replace', + '{db_prefix}themes', + ['id_theme' => 'int', 'id_member' => 'int', 'variable' => 'string-255', 'value' => 'string-65534'], + [self::$current->settings['theme_id'], User::$me->id, 'theme_variant', $_SESSION['id_variant']], + ['id_theme', 'id_member', 'variable'], + ); + } } + /* + * Attempt to load a variants file for variable overriding + * using data attribute (:root[data-variant="variant"]) + * + * This is useful when you only want a single file for + * recoloring the variants. + */ + self::loadCSSFile('variants.css', ['order_pos' => 0], 'smf_variants'); + // User selection? if (empty($this->settings['disable_user_variant']) || User::$me->allowedTo('admin_forum')) { Utils::$context['theme_variant'] = !empty($_SESSION['id_variant']) && \in_array($_SESSION['id_variant'], $this->settings['theme_variants']) ? $_SESSION['id_variant'] : (!empty($this->options['theme_variant']) && \in_array($this->options['theme_variant'], $this->settings['theme_variants']) ? $this->options['theme_variant'] : ''); @@ -2181,18 +2245,6 @@ protected function loadVariant(): void if (Utils::$context['theme_variant'] == '' || !\in_array(Utils::$context['theme_variant'], $this->settings['theme_variants'])) { Utils::$context['theme_variant'] = !empty($this->settings['default_variant']) && \in_array($this->settings['default_variant'], $this->settings['theme_variants']) ? $this->settings['default_variant'] : $this->settings['theme_variants'][0]; } - - // Do this to keep things easier in the templates. - Utils::$context['theme_variant'] = '_' . Utils::$context['theme_variant']; - Utils::$context['theme_variant_url'] = Utils::$context['theme_variant'] . '/'; - - if (!empty(Utils::$context['theme_variant'])) { - self::loadCSSFile('index' . Utils::$context['theme_variant'] . '.css', ['order_pos' => 300], 'smf_index' . Utils::$context['theme_variant']); - - if (Utils::$context['right_to_left']) { - self::loadCSSFile('rtl' . Utils::$context['theme_variant'] . '.css', ['order_pos' => 4200], 'smf_rtl' . Utils::$context['theme_variant']); - } - } } } diff --git a/Themes/default/Themes.template.php b/Themes/default/Themes.template.php index e29ef747797..240a3c5fb1a 100644 --- a/Themes/default/Themes.template.php +++ b/Themes/default/Themes.template.php @@ -511,8 +511,7 @@ function template_set_settings() '; // Do we allow theme variants? - if (!empty(Utils::$context['theme_variants'])) - { + if (!empty(Utils::$context['theme_variants'])) { echo '

@@ -544,6 +543,38 @@ function template_set_settings() '; } + // Color modes for the theme? + if (!empty(Theme::$current->settings['has_dark_mode'])) { + echo ' +
+

+ ', Lang::$txt['theme_colormodes'], ' +

+
+
+
+ +
+
+ +
+
+ +
+
+ + +
+
'; + } + echo '

@@ -690,65 +721,66 @@ function template_set_settings() function template_pick() { echo ' -
-
'; + '; // Just go through each theme and show its information - thumbnail, etc. - foreach (Utils::$context['available_themes'] as $theme) + for ($i = 0; $i < 2; $i++) { echo ' -
-

+
+

', Lang::getTxt($i == 0 ? 'current_theme' : 'theme_pick', file: 'Themes'), '

+
+
'; + + // Just go through each theme and show its information - thumbnail, etc. + foreach (Utils::$context['available_themes'] as $theme) + { + if (($theme['selected'] && $i == 0) || (!$theme['selected'] && $i == 1)) + { + echo ' +
+

', $theme['name'], '

-
-
-
- - - -
-

', $theme['description'], '

'; - - if (!empty($theme['variants'])) - { - echo ' - : - '; - echo ' - '; - } + foreach ($theme['variants'] as $key => $variant) + echo ' + '; - echo ' -
-

- ', Lang::getTxt('theme_num_users', [$theme['num_users']], file: 'Themes'), ' -

-
- + echo ' + + '; + } + + echo ' + +
+ '; + } } echo ' - - - - -
'; +
+ + + '; + } } /** diff --git a/Themes/default/index.template.php b/Themes/default/index.template.php index cdb7e3601d9..a90adb943f7 100644 --- a/Themes/default/index.template.php +++ b/Themes/default/index.template.php @@ -53,6 +53,29 @@ function template_init() // The version this template/theme is for. This should probably be the version of SMF it was created for. Theme::$current->settings['theme_version'] = '2.1'; + /* + * Whether this theme supports a dark mode. + * + * Set this to `false` to disable. + * + * A not so trivial note: + * A 'dark' theme with dark mode is exactly the same as a 'light' + * theme with dark mode. This means the index.css file should + * always contain the light colors. + */ + Theme::$current->settings['has_dark_mode'] = false; + + /* + * Define the theme variants. Each variant has its own CSS file. + * + * Example: + * - index_red.css is loaded when the user selects the `red` variant. + * + * Additionally, a variants.css file is always loaded as well, in + * case you'd rather keep the styles in a single file or they're minimal. + */ + Theme::$current->settings['theme_variants'] = []; + // Set the following variable to true if this theme wants to display the avatar of the user that posted the last and the first post on the message index and recent pages. Theme::$current->settings['avatars_on_indexes'] = false; @@ -76,7 +99,7 @@ function template_init() // Allow css/js files to be disabled for this specific theme. // Add the identifier as an array key. IE array('smf_script'); Some external files might not add identifiers, on those cases SMF uses its filename as reference. if (!isset(Theme::$current->settings['disable_files'])) - Theme::$current->settings['disable_files'] = array(); + Theme::$current->settings['disable_files'] = []; } /** @@ -86,7 +109,8 @@ function template_html_above() { // Show right to left, the language code, and the character set for ease of translating. echo ' - +settings['theme_variants']) ? ' data-variant=' . (Utils::$context['theme_variant'] ?: 'default') . '' : '', !empty(Theme::$current->settings['has_dark_mode']) ? ' data-mode=' . (Utils::$context['theme_colormode'] ?? 'light') . '' : '', '> + ';