screen. */ const SURFACES = [ '_bp3d_settings_' => 'settings', '_bp3dimages_' => 'viewer', '_bp3d_product_' => 'product', ]; /** @var string|null The site-wide mode, resolved once per request. */ private static $mode = null; /** @var bool Whether a bp3d_admin_ui filter took part when the mode was resolved. */ private static $filtered = false; public function register(): void { // Added at load time: bfields caches this map the first time a schema is built. add_filter('bfields_type_map', [self::class, 'typeMap']); add_filter('bfields_fallback_url', [self::class, 'fallbackUrl'], 10, 2); add_action('admin_post_' . self::ACTION, [$this, 'handleSwitch']); add_action('admin_notices', [$this, 'notice']); add_action('save_post', [$this, 'flagDroppedSave'], 5, 2); add_action('admin_notices', [$this, 'droppedNotice']); // The viewer screen hides #misc-publishing-actions, so its line goes above Update. add_action('post_submitbox_misc_actions', [$this, 'publishBoxRow']); add_action('post_submitbox_start', [$this, 'publishBoxRow']); add_action('admin_head', [$this, 'styles']); add_action('admin_enqueue_scripts', [$this, 'notices']); } /** One-line notice bars on the viewer's page-frame editor only (D2); other 3D Viewer screens keep WordPress's notices. */ public function notices(): void { $screen = function_exists('get_current_screen') ? get_current_screen() : null; if (!$screen || 'post' !== $screen->base || 'bp3d-model-viewer' !== $screen->post_type || !function_exists('bfields_enqueue_notices')) { return; } $post = get_post(); // The block editor draws no page frame. if ($post && function_exists('use_block_editor_for_post') && use_block_editor_for_post($post)) { return; } if (self::MODERN === self::mode('viewer')) { bfields_enqueue_notices(); } } /** Filter, then constant, then option, then default; per screen after that. Classic when bfields did not boot. */ public static function mode(string $surface = ''): string { $mode = self::siteMode(); if ('' !== $surface) { $mode = self::normalize(apply_filters('bp3d_admin_ui_surface', $mode, $surface)) ?: $mode; } if (self::MODERN === $mode && !self::bfieldsReady()) { return self::CLASSIC; } return $mode; } public static function siteMode(): string { if (null !== self::$mode) { return self::$mode; } $mode = self::chosen(); if (defined('BP3D_ADMIN_UI') && self::normalize(BP3D_ADMIN_UI)) { $mode = self::normalize(BP3D_ADMIN_UI); } if ('' === $mode) { $mode = self::normalize(apply_filters('bp3d_admin_ui_default', self::MODERN)) ?: self::MODERN; } // Add bp3d_admin_ui before init: the mode is resolved once, so a later filter is neither applied nor reported as the lock. self::$filtered = (bool) has_filter('bp3d_admin_ui'); $mode = self::normalize(apply_filters('bp3d_admin_ui', $mode)) ?: $mode; return self::$mode = $mode; } /** What an admin picked on screen, or '' when nobody has chosen yet. */ public static function chosen(): string { return self::normalize(get_option(self::OPTION, '')); } /** Why the on-screen switch cannot change the mode, or '' when it can. */ public static function lockedBy(): string { if (null !== self::$mode ? self::$filtered : has_filter('bp3d_admin_ui')) { return 'filter'; } if (defined('BP3D_ADMIN_UI') && self::normalize(BP3D_ADMIN_UI)) { return 'constant'; } return ''; } public static function bfieldsReady(): bool { return did_action('bfields_loaded') > 0 && class_exists('BFields\\Compat\\Codestar', false); } /** The surface the current admin screen edits, or '' for screens without a 3D Viewer form. */ public static function screenSurface(): string { $screen = function_exists('get_current_screen') ? get_current_screen() : null; if (!$screen) { return ''; } if (false !== strpos((string) $screen->id, '3dviewer-settings')) { return 'settings'; } if ('post' === $screen->base && in_array($screen->post_type, ['bp3d-model-viewer', 'product'], true)) { return 'bp3d-model-viewer' === $screen->post_type ? 'viewer' : 'product'; } return ''; } private static function normalize($mode): string { return in_array($mode, [self::MODERN, self::CLASSIC], true) ? $mode : ''; } /** Meta boxes register only in wp-admin, so bfields' REST routes (hotspot product search) need this. */ public static function isBfieldsRestRequest(string $unique): bool { // The route check first: it is a string match, while the mode can cost a query on every front-end request. return function_exists('bfields_is_rest_request') && bfields_is_rest_request($unique) && self::MODERN === self::mode(self::SURFACES[$unique] ?? ''); } public static function typeMap(array $map): array { return $map + ['bp3d_responsive_dimensions' => 'dimension']; } /** The "switch to classic" button bfields shows if its screen fails to load. */ public static function fallbackUrl($url, $unique) { if (!isset(self::SURFACES[$unique]) || !current_user_can(self::CAPABILITY) || '' !== self::lockedBy()) { return $url; } return self::switchUrl(self::CLASSIC, self::here()); } private static function here(): string { return isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : ''; } /** `option_meta` for the Lite / Advanced mode grid; Codestar ignores the key. */ public static function viewerModes(): array { return [ 'modelViewer' => ['icon' => 'zap', 'tag' => __('Recommended', '3d-viewer'), 'perks' => [__('GLB and glTF', '3d-viewer'), __('Augmented reality', '3d-viewer')]], 'O3DViewer' => ['icon' => 'sliders', 'perks' => [__('OBJ, STL, FBX, STEP and more', '3d-viewer')]], ]; } public static function switchUrl(string $to, string $redirect = ''): string { $args = ['action' => self::ACTION, 'ui' => $to]; if ('' !== $redirect) { $args['redirect_to'] = rawurlencode($redirect); // PHP decodes it once into $_GET. } return wp_nonce_url(add_query_arg($args, admin_url('admin-post.php')), self::ACTION); } /** A plain nonce link, so it works with no JavaScript: exactly when a blocked bundle needs it. */ public function handleSwitch(): void { check_admin_referer(self::ACTION); if (!current_user_can(self::CAPABILITY)) { wp_die(esc_html__('You do not have permission to change the admin interface.', '3d-viewer'), 403); } $to = self::normalize(isset($_GET['ui']) ? sanitize_key(wp_unslash($_GET['ui'])) : ''); if ('' === $to) { wp_die(esc_html__('Unknown admin interface.', '3d-viewer'), 400); } update_option(self::OPTION, $to); // Without redirect_to or a Referer the admin would land on a blank admin-post.php. $fallback = admin_url('edit.php?post_type=bp3d-model-viewer&page=3dviewer-settings'); $target = isset($_GET['redirect_to']) && is_string($_GET['redirect_to']) ? wp_unslash($_GET['redirect_to']) : (string) wp_get_referer(); $target = '' === trim($target) ? $fallback : wp_validate_redirect($target, $fallback); wp_safe_redirect(add_query_arg('bp3d_admin_ui_switched', $to, $target)); exit; } /** The Settings page's first row; a `content` field with no id, so neither framework stores it. */ public static function switchField(): array { $modern = self::MODERN === self::mode('settings'); $locked = self::lockedBy(); $subtitle = __('Only changes how the screens look. Settings and viewers stay the same.', '3d-viewer'); if ('' !== $locked) { $subtitle = 'constant' === $locked ? __('The interface is set by the BP3D_ADMIN_UI constant in wp-config.php.', '3d-viewer') : __('The interface is set by the bp3d_admin_ui filter.', '3d-viewer'); } elseif (!$modern && !self::bfieldsReady()) { // lib/bfields/ did not load, so switching would change nothing. $subtitle = __('The new interface could not be loaded on this site, so the classic one is used.', '3d-viewer'); } $canSwitch = '' === $locked && ($modern || self::bfieldsReady()); $options = [ self::MODERN => [__('New', '3d-viewer'), __('Switch to the new interface', '3d-viewer')], self::CLASSIC => [__('Classic', '3d-viewer'), __('Switch to the classic interface', '3d-viewer')], ]; $current = $modern ? self::MODERN : self::CLASSIC; $segments = ''; foreach ($options as $mode => [$label, $action]) { if ($mode === $current) { $segments .= '' . esc_html($label) . ''; } elseif ($canSwitch) { $segments .= '' . esc_html($label) . ''; } else { $segments .= '' . esc_html($label) . ''; } } return [ 'type' => 'content', 'icon' => 'monitor', 'title' => __('Admin interface', '3d-viewer'), 'subtitle' => $subtitle, 'content' => '
%s