static fn( array $post ) => Readerr_Sync::audio_is_stale( get_post( $post['id'] ) ), 'schema' => array( 'type' => 'boolean', 'context' => array( 'edit' ), 'readonly' => true, ), ) ); } public static function enqueue(): void { if ( 'post' !== get_post_type() ) { return; } wp_enqueue_script( 'readerr-editor', plugins_url( 'assets/editor-sidebar.js', READERR_FILE ), array( 'wp-plugins', 'wp-editor', 'wp-core-data', 'wp-element', 'wp-components', 'wp-data', 'wp-i18n' ), READERR_VERSION, true ); wp_set_script_translations( 'readerr-editor', 'readerr' ); // Only what the panel shows. The service key stays on the server. $data = array( 'voices' => self::voices(), 'generateByDefault' => (bool) Readerr_Settings::get( 'generate_by_default' ), 'minLength' => (int) Readerr_Settings::get( 'min_length' ), 'maxLength' => Readerr_Sync::MAX_TEXT_CHARS, 'onUpdate' => (string) Readerr_Settings::get( 'on_update' ), // Spec §6.1: the grace-week nudge, same message as the settings // screen (Readerr_Settings::render_access_notice()). 'access' => array( 'state' => Readerr_Access::state(), 'endsAt' => Readerr_Access::ends_at(), 'billingUrl' => Readerr_Access::BILLING_URL, ), ); wp_add_inline_script( 'readerr-editor', 'window.readerrEditor = ' . wp_json_encode( $data, JSON_HEX_TAG | JSON_HEX_AMP ) . ';', 'before' ); } /** * The engine's voices and this portal's default voice, as the service * reports them for our key (GET /v1/voices). The default lives in the * service only — the plugin shows it, never sets it. $fresh skips the * cached copy (the settings screen, where a changed default must show). * * @return array{voices: array, default: ?string} */ public static function catalog( bool $fresh = false ): array { $catalog = $fresh ? false : get_transient( self::CATALOG_TRANSIENT ); if ( is_array( $catalog ) && isset( $catalog['voices'] ) && is_array( $catalog['voices'] ) ) { return $catalog; } $catalog = array( 'voices' => array(), 'default' => null ); $client = Readerr_Client::from_settings(); if ( ! $client ) { return $catalog; } $answer = $client->voices( self::VOICES_TIMEOUT_S ); $listed = $answer['body']['voices'] ?? null; if ( 200 !== $answer['status'] || ! is_array( $listed ) ) { set_transient( self::CATALOG_TRANSIENT, $catalog, self::FAILURE_TTL_S ); return $catalog; } foreach ( $listed as $voice ) { if ( ! is_array( $voice ) || ! is_string( $voice['id'] ?? null ) || ! is_string( $voice['gender'] ?? null ) ) { continue; } $catalog['voices'][] = array( 'value' => $voice['id'], 'label' => $voice['id'] . ' (' . $voice['gender'] . ')' ); } $default = $answer['body']['default'] ?? null; $catalog['default'] = is_string( $default ) && '' !== $default ? $default : null; set_transient( self::CATALOG_TRANSIENT, $catalog, self::CATALOG_TTL_S ); return $catalog; } /** @return array */ public static function voices(): array { $catalog = self::catalog(); // '' stays the value: an untouched post follows whatever the portal's // default is at synthesis time, not the one shown here today. $label = null === $catalog['default'] ? __( 'Portal default', 'readerr' ) : sprintf( /* translators: %s: voice id set as this portal's default in the Readerr service */ __( 'Portal default (%s)', 'readerr' ), $catalog['default'] ); return array_merge( array( array( 'value' => '', 'label' => $label ) ), $catalog['voices'] ); } }