'playlist', ]; public static function init() { $self = new self(); add_action( 'admin_menu', [ $self, 'register_menu' ] ); add_action( 'admin_enqueue_scripts', [ $self, 'brand_icon_style' ] ); // Must run before WP's own access check, which wp_die()s on a page slug // that no longer exists — that check lives at the end of the same file // that fires admin_menu, and admin_init is later still. add_action( 'admin_menu', [ $self, 'redirect_legacy_pages' ], 5 ); } /** * Send the retired section URLs to the library, filtered to what they used * to show, so anything still linking to them keeps working. */ public function redirect_legacy_pages(): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; if ( '' === $page ) { return; } $slug = TRUEPLAYER_PLUGIN_SLUG; foreach ( self::LEGACY_PAGES as $suffix => $kind ) { if ( $slug . $suffix !== $page ) { continue; } wp_safe_redirect( admin_url( add_query_arg( [ 'page' => $slug . '-videos', 'kind' => $kind ], 'admin.php' ) ) ); exit; } } public function register_menu() { $brand = \TruePlayer\Helper::brand_name(); add_menu_page( $brand, $brand, 'manage_options', TRUEPLAYER_PLUGIN_SLUG, [ $this, 'render_app' ], self::menu_icon(), 30 ); // Child menu (StoreEngine-style): each section is its own page slug so it // gets a real URL and appears in the WP admin submenu. All boot the same SPA. $slug = TRUEPLAYER_PLUGIN_SLUG; $subs = [ $slug => __( 'Dashboard', 'trueplayer' ), $slug . '-videos' => __( 'Media', 'trueplayer' ), $slug . '-presets' => __( 'Presets', 'trueplayer' ), $slug . '-settings' => __( 'Settings', 'trueplayer' ), ]; foreach ( $subs as $page_slug => $label ) { add_submenu_page( $slug, $label . ' – ' . $brand, $label, 'manage_options', $page_slug, [ $this, 'render_app' ] ); } } /** * The menu mark: the owner's own logo when white-label supplies one, * otherwise WordPress's video dashicon. add_menu_page takes a URL here as * happily as a dashicon name, so a custom logo needs no extra plumbing. */ private static function menu_icon(): string { $logo = \TruePlayer\Helper::brand_logo(); return $logo ? esc_url_raw( $logo ) : 'dashicons-format-video'; } /** * Keep a white-label logo the size of a menu icon. * * `add_menu_page()` renders whatever URL it is given as a plain , and * WordPress constrains it to nothing — its own icons are 20px sprites, so a * real logo (or, worse, a photo someone picked from the media library) * painted at full natural size straight across the screen. WordPress sizes * dashicons but has no rule for image icons, so this supplies one. * * Only printed when a custom logo is actually in use; the default dashicon * needs nothing. */ public function brand_icon_style(): void { if ( ! \TruePlayer\Helper::brand_logo() ) { return; } wp_register_style( 'trueplayer-brand-icon', false, [], TRUEPLAYER_VERSION ); wp_enqueue_style( 'trueplayer-brand-icon' ); wp_add_inline_style( 'trueplayer-brand-icon', sprintf( '#adminmenu .toplevel_page_%1$s .wp-menu-image img{width:20px;height:20px;max-width:20px;object-fit:contain;}', sanitize_html_class( TRUEPLAYER_PLUGIN_SLUG ) ) ); } public function render_app() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : TRUEPLAYER_PLUGIN_SLUG; printf( '
', esc_attr( $page ) ); } }