';
/** wp_enqueue_script() handle for the player loader. */
private const HANDLE = 'readerr-player';
/** §8.1, verbatim — the same sentence the ID3 COMM frame carries. */
public const LABELS = array(
'pl' => 'Audio generowane przez AI (Readerr) - może zawierać błędy',
'de' => 'KI-generiertes Audio (Readerr) - kann Fehler enthalten',
'fr' => 'Audio généré par IA (Readerr) - peut contenir des erreurs',
'en' => 'AI-generated audio (Readerr) - may contain errors',
);
/**
* Spec §14: the stage and the label hold the card's exact height before the
* module runs, inline so they win over theme classes and need no stylesheet.
*/
private const STAGE_STYLE = 'box-sizing:border-box;height:128px;display:flex;align-items:center;padding:0 12px;border-radius:16px;background:rgba(128,128,128,.08)';
private const LABEL_STYLE = 'margin:8px 0 0;font-size:13px;line-height:20px';
/** Spec §12 and §16: the reader-facing "coming soon" text, in the article language. */
public const PENDING = array(
'pl' => 'Wersja audio w przygotowaniu',
'de' => 'Audioversion in Vorbereitung',
'fr' => 'Version audio en préparation',
'en' => 'Audio version coming soon',
);
/** Spec §12: an older placeholder is a promise nobody is keeping. */
public const PENDING_MAX_AGE_S = 3600;
private const HEADPHONES = '';
public static function register(): void {
// After wpautop (10) and shortcodes (11), so neither rewraps our markup —
// and so the shortcode's marker is already in the content when we look.
add_filter( 'the_content', array( self::class, 'place_player' ), 20 );
add_shortcode( 'readerr_player', array( self::class, 'shortcode' ) );
add_action( 'wp_enqueue_scripts', array( self::class, 'enqueue_player' ) );
add_action( 'init', array( self::class, 'register_block' ) );
add_action( 'enqueue_block_editor_assets', array( self::class, 'enqueue_block_editor_script' ) );
add_filter( 'wp_script_attributes', array( self::class, 'harden_script_tag' ) );
}
/**
* The Gutenberg block: dynamic, so its saved content is always empty
* (block-player.js's save() returns null) and shortcode() — the same
* method the [readerr_player] shortcode uses — decides at render time
* what the block leaves behind. do_blocks() runs at the_content priority
* 9, before place_player() at 20, so the block's marker is in the content
* exactly like the shortcode's by the time place_player() looks for it.
*/
public static function register_block(): void {
self::register_block_script();
register_block_type( __DIR__ . '/../blocks/player', array( 'render_callback' => array( self::class, 'shortcode' ) ) );
}
/**
* Split out from register_block() so it can be re-run on its own —
* wp_register_script() is safe to call repeatedly, but register_block_type()
* is not (a second call for the same name is a doing_it_wrong()).
*/
public static function register_block_script(): void {
wp_register_script(
'readerr-block-player',
plugins_url( 'assets/block-player.js', READERR_FILE ),
array( 'wp-blocks', 'wp-block-editor', 'wp-element', 'wp-i18n', 'wp-data' ),
READERR_VERSION,
true
);
wp_set_script_translations( 'readerr-block-player', 'readerr' );
}
/**
* The block renders only on a single `post` (renders_here()), so it has
* no reason to appear in the inserter anywhere else — but gating that
* with allowed_block_types_all (tried first, docs/zalozenia.md #50) went
* too far: on any non-post context (a page, a CPT, the site editor, the
* widgets editor, where $context->post is null) it replaces WordPress's
* own `true` with an explicit list of every server-registered block,
* which also hides every JS-only third-party block that never registered
* server-side — and a `false` input from another plugin becomes `[false]`.
* Gating the editor script's enqueue instead (the same gate
* Readerr_Editor::enqueue() uses) hides the block from the inserter on a
* page without touching what any other editor is allowed to show — the
* server-side registration below still makes existing blocks in old
* content render correctly everywhere.
*/
public static function enqueue_block_editor_script(): void {
if ( 'post' !== get_post_type() ) {
return;
}
wp_enqueue_script( 'readerr-block-player' );
}
/** §10: one deferred script, only where a player will actually render. */
public static function enqueue_player(): void {
if ( ! is_singular( 'post' ) ) {
return;
}
$post_id = get_queried_object_id();
$path = Readerr_Meta::sanitize_path( get_post_meta( $post_id, Readerr_Meta::PATH, true ) );
if ( '' === $path || self::hidden( $post_id ) || self::password_protected( $post_id ) || 'lapsed' === Readerr_Access::state() ) {
return;
}
// Under 'manual' only the shortcode or the block renders a player
// (place_player()).
if ( 'manual' === Readerr_Settings::get( 'position' )
&& ! has_shortcode( (string) get_post_field( 'post_content', $post_id ), 'readerr_player' )
&& ! has_block( 'readerr/player', $post_id ) ) {
return;
}
self::enqueue_player_script();
}
/** The player loader under our handle, so harden_script_tag() reaches it (the settings preview too). */
public static function enqueue_player_script(): void {
wp_enqueue_script(
self::HANDLE,
(string) Readerr_Settings::get( 'player_url' ),
array(),
READERR_VERSION,
array(
'strategy' => 'defer',
'in_footer' => false,
)
);
}
/**
* docs/todo.md, "Wygląd playera — nowy projekt", „Przy okazji z ich
* wtyczki…": Cloudflare Rocket Loader and JS-optimizing plugins rewrite
*