* @since 7.0.0 */ namespace BeyondWords\Editor\Components; /** * SettingsFields * * @since 7.0.0 */ defined( 'ABSPATH' ) || exit; class SettingsFields { public const SOURCE_POST = 'post'; public const SOURCE_POST_AND_SCRIPT = 'post_and_script'; // Source removed in 7.0.0; see doc/legacy-meta-migration.md. public const LEGACY_SOURCE_SCRIPT = 'script'; public const OUTPUT_AUDIO = 'audio'; public const OUTPUT_VIDEO = 'video'; public const OUTPUT_AUDIO_AND_VIDEO = 'audio_and_video'; public const EMBED_NONE = 'none'; public const EMBED_AUDIO_POST = 'audio_post'; public const EMBED_AUDIO_SCRIPT = 'audio_script'; public const EMBED_VIDEO_POST = 'video_post'; public const EMBED_VIDEO_SCRIPT = 'video_script'; /** * Init. * * @since 7.0.0 */ public static function init() { add_action( 'wp_loaded', function (): void { $post_types = \BeyondWords\Settings\Utils::get_compatible_post_types(); if ( is_array( $post_types ) ) { foreach ( $post_types as $post_type ) { add_action( "save_post_{$post_type}", [ self::class, 'save' ], 10 ); } } } ); } /** * Render the nonce field shared by all Settings Fields sections. * * Called once by the metabox so a single nonce guards the combined * Content/Format/Player save. * * @since 7.0.0 */ public static function nonce(): void { wp_nonce_field( 'beyondwords_settings_fields', 'beyondwords_settings_fields_nonce' ); } // Option helpers (mirror src/editor/components/settings-panel/helpers.js). /** * The "Project default" leaf option. * * An empty value defers to the project setting — the plugin omits the field * from the content payload when empty. * * @since 7.0.0 * * @return array{label: string, value: string} */ public static function project_default_option(): array { return [ 'label' => __( 'Project default', 'speechkit' ), 'value' => '', ]; } /** * Source dropdown options. * * @since 7.0.0 * * @return array */ public static function source_options(): array { return [ [ 'label' => __( 'Post', 'speechkit' ), 'value' => self::SOURCE_POST, ], [ 'label' => __( 'Post + script', 'speechkit' ), 'value' => self::SOURCE_POST_AND_SCRIPT, ], ]; } /** * Output dropdown options. * * @since 7.0.0 * * @return array */ public static function output_options(): array { return [ [ 'label' => __( 'Audio', 'speechkit' ), 'value' => self::OUTPUT_AUDIO, ], [ 'label' => __( 'Video', 'speechkit' ), 'value' => self::OUTPUT_VIDEO, ], [ 'label' => __( 'Audio + video', 'speechkit' ), 'value' => self::OUTPUT_AUDIO_AND_VIDEO, ], ]; } /** * Resolve a stored source to SOURCE_POST or SOURCE_POST_AND_SCRIPT. * * @since 7.0.0 */ public static function normalize_source( string $source ): string { return in_array( $source, [ self::SOURCE_POST_AND_SCRIPT, self::LEGACY_SOURCE_SCRIPT ], true ) ? self::SOURCE_POST_AND_SCRIPT : self::SOURCE_POST; } /** * The effective Source for a post. * * @since 7.0.0 */ public static function get_source( int $post_id ): string { return self::normalize_source( self::get_meta( $post_id, 'beyondwords_source', self::SOURCE_POST ) ); } /** * Whether the source includes a generated script. * * @since 7.0.0 * * @param string $source One of the SOURCE_* constants. */ public static function source_includes_script( string $source ): bool { return self::SOURCE_POST_AND_SCRIPT === self::normalize_source( $source ); } /** * Whether the output includes audio. * * @since 7.0.0 * * @param string $output One of the OUTPUT_* constants. */ public static function output_includes_audio( string $output ): bool { return in_array( $output, [ self::OUTPUT_AUDIO, self::OUTPUT_AUDIO_AND_VIDEO ], true ); } /** * Whether the output includes video. * * @since 7.0.0 * * @param string $output One of the OUTPUT_* constants. */ public static function output_includes_video( string $output ): bool { return in_array( $output, [ self::OUTPUT_VIDEO, self::OUTPUT_AUDIO_AND_VIDEO ], true ); } /** * Derive the valid "Embed" dropdown options from the current Source × Output. * * Returns None plus one entry for each asset combination the current * source/output would produce. Post assets are unconditional — every source * generates the post. * * @since 7.0.0 * * @param string $source One of the SOURCE_* constants. * @param string $output One of the OUTPUT_* constants. * * @return array */ public static function embed_options( string $source, string $output ): array { $options = [ [ 'label' => __( 'None', 'speechkit' ), 'value' => self::EMBED_NONE, ], ]; $includes_script = self::source_includes_script( $source ); if ( self::output_includes_audio( $output ) ) { $options[] = [ 'label' => __( 'Audio (post)', 'speechkit' ), 'value' => self::EMBED_AUDIO_POST, ]; if ( $includes_script ) { $options[] = [ 'label' => __( 'Audio (script)', 'speechkit' ), 'value' => self::EMBED_AUDIO_SCRIPT, ]; } } if ( self::output_includes_video( $output ) ) { $options[] = [ 'label' => __( 'Video (post)', 'speechkit' ), 'value' => self::EMBED_VIDEO_POST, ]; if ( $includes_script ) { $options[] = [ 'label' => __( 'Video (script)', 'speechkit' ), 'value' => self::EMBED_VIDEO_SCRIPT, ]; } } return $options; } /** * Whether the given embed value is selectable for the current Source × Output. * * @since 7.0.0 * * @param string $embed One of the EMBED_* constants. * @param string $source One of the SOURCE_* constants. * @param string $output One of the OUTPUT_* constants. */ public static function is_embed_valid( string $embed, string $source, string $output ): bool { foreach ( self::embed_options( $source, $output ) as $option ) { if ( $option['value'] === $embed ) { return true; } } return false; } /** * The default Embed for a post that hasn't chosen one: the first produced asset. * * Keeps the player visible by default — "None" is the deliberate opt-out. * Mirrors getDefaultEmbed() in settings-panel/helpers.js. * * @since 7.0.0 * * @param string $source One of the SOURCE_* constants. * @param string $output One of the OUTPUT_* constants. * * @return string The default embed value. */ public static function default_embed( string $source, string $output ): string { foreach ( self::embed_options( $source, $output ) as $option ) { if ( self::EMBED_NONE !== $option['value'] ) { return $option['value']; } } return self::EMBED_NONE; } /** * Resolve the effective Embed value for a post. * * Centralised so the shown default and the rendered player (ConfigBuilder) never * diverge; legacy opt-outs resolve to None, stale values to the default asset. * * @since 7.0.0 * * @param int $post_id The post ID. * * @return string One of the EMBED_* constants. */ public static function get_effective_embed( int $post_id ): string { $source = self::get_source( $post_id ); $output = self::get_meta( $post_id, 'beyondwords_output', self::OUTPUT_AUDIO ); $embed = get_post_meta( $post_id, 'beyondwords_embed', true ); if ( ! is_string( $embed ) || '' === $embed ) { $embed = \BeyondWords\Post\Meta::get_disabled( $post_id ) ? self::EMBED_NONE : self::default_embed( $source, $output ); } if ( ! self::is_embed_valid( $embed, $source, $output ) ) { $embed = self::default_embed( $source, $output ); } return $embed; } /** * Whether the player is suppressed on a post. * * An explicit Embed value is authoritative ("None" replaces the pre-v7 opt-out); * only when unset do we fall back to the legacy `beyondwords_disabled` flag. * * @since 7.0.0 * * @param int $post_id The post ID. */ public static function is_player_disabled_for_post( int $post_id ): bool { $embed = get_post_meta( $post_id, 'beyondwords_embed', true ); if ( is_string( $embed ) && '' !== $embed ) { return self::EMBED_NONE === $embed; } return (bool) \BeyondWords\Post\Meta::get_disabled( $post_id ); } // Renderers. /** * Render the Content section fields: Source + Script template. * * @since 7.0.0 * * @param \WP_Post $post The post object. */ public static function render_content_section( $post ): void { $source = self::get_source( $post->ID ); $script_template_id = self::get_meta( $post->ID, 'beyondwords_script_template_id', '' ); $templates = \BeyondWords\Api\Client::get_summarization_settings_templates(); $templates = is_array( $templates ) ? $templates : []; self::render_select( 'beyondwords_source', __( 'Source', 'speechkit' ), self::source_options(), $source ); self::render_select( 'beyondwords_script_template_id', __( 'Script template', 'speechkit' ), array_merge( [ self::project_default_option() ], self::templates_to_options( $templates ) ), $script_template_id, ! self::source_includes_script( $source ) ); } /** * Render the Format section fields: Output + Video template + Video size. * * @since 7.0.0 * * @param \WP_Post $post The post object. */ public static function render_format_section( $post ): void { $output = self::get_meta( $post->ID, 'beyondwords_output', self::OUTPUT_AUDIO ); $video_template_id = self::get_meta( $post->ID, 'beyondwords_video_template_id', '' ); $video_size = self::get_meta( $post->ID, 'beyondwords_video_size', '' ); $templates = \BeyondWords\Api\Client::get_video_settings_templates(); $templates = is_array( $templates ) ? $templates : []; $video_settings = \BeyondWords\Api\Client::get_video_settings(); $sizes = is_array( $video_settings ) && isset( $video_settings['sizes'] ) && is_array( $video_settings['sizes'] ) ? $video_settings['sizes'] : []; $hide_video = ! self::output_includes_video( $output ); self::render_select( 'beyondwords_output', __( 'Output', 'speechkit' ), self::output_options(), $output ); self::render_select( 'beyondwords_video_template_id', __( 'Video template', 'speechkit' ), array_merge( [ self::project_default_option() ], self::templates_to_options( $templates ) ), $video_template_id, $hide_video ); self::render_select( 'beyondwords_video_size', __( 'Video size', 'speechkit' ), array_merge( [ self::project_default_option() ], self::sizes_to_options( $sizes ) ), $video_size, $hide_video ); } /** * Render the Player section fields: Embed. * * @since 7.0.0 * * @param \WP_Post $post The post object. */ public static function render_player_section( $post ): void { $source = self::get_source( $post->ID ); $output = self::get_meta( $post->ID, 'beyondwords_output', self::OUTPUT_AUDIO ); $embed = self::get_effective_embed( $post->ID ); self::render_select( 'beyondwords_embed', __( 'Embed', 'speechkit' ), self::embed_options( $source, $output ), $embed, false, __( 'Pick which generated asset is shown on this post. All other generated assets stay available in BeyondWords.', // phpcs:ignore Generic.Files.LineLength.TooLong 'speechkit' ) ); // Lets save() tell a real choice from the rendered default. ?> options. * * @since 7.0.0 * * @param array> $templates API templates. * * @return array */ private static function templates_to_options( array $templates ): array { $options = []; foreach ( $templates as $template ) { if ( ! isset( $template['id'] ) ) { continue; } $options[] = [ 'label' => (string) ( $template['name'] ?? $template['slug'] ?? '' ), 'value' => (string) $template['id'], ]; } return $options; } /** * Convert a list of API video sizes to control in the classic-metabox style. * * @since 7.0.0 * * @param string $id Field id/name. * @param string $label Field label. * @param array $options Select options. * @param string $selected Selected value. * @param bool $hidden Whether the field starts hidden. * @param string $help Optional help text. */ private static function render_select( string $id, string $label, array $options, string $selected, bool $hidden = false, string $help = '' ): void { $wrapper_id = 'beyondwords-metabox-settings--' . str_replace( '_', '-', $id ); ?>
>