# booktics/1.0.27/core/admin/theme-mode.php

Booktics – Appointment Booking Calendar for Service Businesses, version 1.0.27. 179 lines.

- Page: https://pluginprobe.com/plugins/booktics/1.0.27/code/core/admin/theme-mode.php
- Raw: https://pluginprobe.com/plugins/booktics/1.0.27/raw/core/admin/theme-mode.php
- Modified: 2026-09-28T15:21:16+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/booktics/1.0.27/code/core/admin/theme-mode.php#L10-L20`.

```php
<?php

namespace Booktics\Admin;

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

use Booktics\Contracts\Hookable_Service_Contract;

/**
 * Per-user light/dark preference for the Booktics admin.
 *
 * The preference is stored against the WordPress user, not in plugin options,
 * so one admin choosing dark does not impose it on everyone else who logs in.
 *
 * It is printed on <body> by PHP, before any script runs. Without that, a dark
 * mode user gets a white flash while React mounts, which reads as a broken
 * plugin rather than a loading one. PHP cannot resolve 'system' on the server,
 * so it prints the preference and a CSS media query settles it; once React
 * mounts it swaps in the resolved value.
 *
 * Saving needs no controller of our own: register_meta() with show_in_rest
 * makes core's POST /wp/v2/users/me accept the field, and a logged-in user may
 * always edit their own profile.
 *
 * @package Booktics/Admin
 */
class Theme_Mode implements Hookable_Service_Contract {

    /**
     * User meta key holding the preference.
     */
    const META_KEY = 'booktics_theme_mode';

    /**
     * The only values that may ever be stored or printed.
     */
    const MODES = array( 'light', 'dark', 'system' );

    /**
     * Used when nothing is stored, and when anything invalid is.
     */
    const DEFAULT_MODE = 'system';

    /**
     * The one admin screen Booktics owns.
     */
    const SCREEN = 'toplevel_page_booktics';

    /**
     * Register hooks.
     *
     * @return void
     */
    public function register() {
        // Called directly, not hooked. Service providers are booted from
        // init_classes() which already runs on `init` (base/booktics.php:92),
        // so adding another `init` callback here fires too late and the meta
        // never reaches the REST user schema.
        $this->register_meta();

        add_filter( 'admin_body_class', array( $this, 'body_class' ) );
    }

    /**
     * Expose the preference on core's user REST endpoint.
     *
     * @return void
     */
    public function register_meta() {
        register_meta(
            'user',
            self::META_KEY,
            array(
                'type'              => 'string',
                'single'            => true,
                'default'           => self::DEFAULT_MODE,
                'show_in_rest'      => true,
                'sanitize_callback' => array( __CLASS__, 'sanitize' ),
                'auth_callback'     => function ( $allowed, $meta_key, $object_id ) {
                    return current_user_can( 'edit_user', $object_id );
                },
            )
        );
    }

    /**
     * Force any value into one of the three modes.
     *
     * This value ends up inside a class attribute on <body>, so it is a trust
     * boundary. The REST endpoint belongs to core, which means the payload
     * shape is not ours to control: arrays and nulls have to fall through
     * quietly rather than raise a warning.
     *
     * @param mixed $value Raw value from REST, the database, or WP-CLI.
     *
     * @return string One of self::MODES.
     */
    public static function sanitize( $value ) {
        if ( ! is_string( $value ) ) {
            return self::DEFAULT_MODE;
        }

        return in_array( $value, self::MODES, true ) ? $value : self::DEFAULT_MODE;
    }

    /**
     * The preference for a user.
     *
     * Sanitises on the way out as well as in. Meta can be written by WP-CLI,
     * by other code, or by an older version of this plugin, so reading it is
     * just as much a trust boundary as writing it.
     *
     * @param int $user_id 0 for the current user.
     *
     * @return string One of self::MODES.
     */
    public static function get_for_user( $user_id = 0 ) {
        $user_id = $user_id ? $user_id : get_current_user_id();

        if ( ! $user_id ) {
            return self::DEFAULT_MODE;
        }

        return self::sanitize( get_user_meta( $user_id, self::META_KEY, true ) );
    }

    /**
     * Whether the feature is visible to anyone.
     *
     * On since the conversion finished. The filter stays so a site owner who
     * does not want dark mode can turn the whole thing off in one line:
     *
     *     add_filter( 'booktics_enable_dark_mode', '__return_false' );
     *
     * @return bool
     */
    public static function is_enabled() {
        return (bool) apply_filters( 'booktics_enable_dark_mode', true );
    }

    /**
     * Add the theme class to <body> on the Booktics screen only.
     *
     * Returns other screens untouched, and does not look up a user on them,
     * so this costs nothing anywhere else in wp-admin.
     *
     * @param string $classes Space separated classes from WordPress.
     *
     * @return string
     */
    public function body_class( $classes ) {
        if ( ! $this->is_booktics_screen() ) {
            return $classes;
        }

        return $classes . ' booktics-theme-' . self::get_for_user();
    }

    /**
     * Whether the current request is rendering the Booktics admin screen.
     *
     * get_current_screen() is unavailable early in the request, hence the
     * function_exists guard.
     *
     * @return bool
     */
    private function is_booktics_screen() {
        if ( ! function_exists( 'get_current_screen' ) ) {
            return false;
        }

        $screen = get_current_screen();

        return $screen && self::SCREEN === $screen->base;
    }
}

```
