PluginProbe
BeyondWords – AI audio for publishers / 7.2.0
BeyondWords – AI audio for publishers v7.2.0
7.2.0 7.1.0 trunk 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.1.0 4.1.1 4.1.2 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.3.0 4.4.0 4.5.0 4.5.1 4.6.0 4.6.1 4.6.2 All 44 releases
speechkit / src / editor / classic / class-metabox.php

class-metabox.php in BeyondWords – AI audio for publishers 7.2.0, at src/editor/classic/class-metabox.php

397 lines 10.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare( strict_types = 1 );
4
5 /**
6 * BeyondWords Post Metabox.
7 *
8 * @package BeyondWords\Editor\Classic
9 * @author Stuart McAlpine <[email protected]>
10 * @since 3.0.0
11 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
12 */
13
14 namespace BeyondWords\Editor\Classic;
15
16 /**
17 * PostMetabox
18 *
19 * @since 3.0.0
20 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
21 */
22 defined( 'ABSPATH' ) || exit;
23
24 class Metabox {
25
26 /**
27 * Init.
28 *
29 * @since 4.0.0
30 * @since 6.0.0 Make static.
31 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
32 */
33 public static function init() {
34 add_action( 'add_meta_boxes', [ self::class, 'add_meta_box_callback'] );
35 }
36
37 /**
38 * Adds the meta box container.
39 *
40 * @since 6.0.0 Make static.
41 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
42 *
43 * @param string $post_type
44 */
45 public static function add_meta_box_callback( $post_type ) {
46 $post_types = \BeyondWords\Settings\Utils::get_compatible_post_types();
47
48 if ( ! in_array( $post_type, $post_types ) ) {
49 return;
50 }
51
52 add_meta_box(
53 'beyondwords',
54 __( 'BeyondWords', 'speechkit' ),
55 [ self::class, 'render_meta_box_content'],
56 $post_type,
57 'side',
58 'default',
59 [
60 '__back_compat_meta_box' => true,
61 ]
62 );
63 }
64
65 /**
66 * Render Meta Box content.
67 *
68 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
69 *
70 * @param int|\WP_Post $post The WordPress post ID, or post object.
71 *
72 * @since 3.0.0
73 * @since 3.7.0 Show "Pending review" notice for posts with status of "pending"
74 * @since 4.0.0 Content ID is no longer an int
75 * @since 4.1.0 Add "Player style" and update component display conditions
76 * @since 6.0.0 Make static and add Magic Embed support.
77 */
78 public static function render_meta_box_content( $post ) {
79 $post = get_post( $post );
80
81 if ( ! ( $post instanceof \WP_Post ) ) {
82 return;
83 }
84
85 $has_content = \BeyondWords\Post\Meta::has_content( $post->ID );
86
87 // Single nonce guarding the Content/Format/Player <select> fields.
88 \BeyondWords\Editor\Components\SettingsFields::nonce();
89
90 self::heading( __( 'Player', 'speechkit' ) );
91
92 self::errors( $post );
93
94 if ( $has_content ) {
95 if ( get_post_status( $post ) === 'pending' ) {
96 self::pending_review_notice( $post );
97 } else {
98 self::player_embed( $post );
99 }
100 } else {
101 self::awaiting_generation_embed( $post );
102 }
103
104 // The Embed dropdown ("None" = no player) replaces the old Display player checkbox.
105 \BeyondWords\Editor\Components\SettingsFields::render_player_section( $post );
106 ( new \BeyondWords\Editor\Components\GenerateAudio() )::element( $post );
107
108 echo '<hr class="beyondwords-metabox__divider" />';
109 self::heading( __( 'Content', 'speechkit' ) );
110 \BeyondWords\Editor\Components\SettingsFields::render_content_section( $post );
111
112 echo '<hr class="beyondwords-metabox__divider" />';
113 self::heading( __( 'Format', 'speechkit' ) );
114 \BeyondWords\Editor\Components\SettingsFields::render_format_section( $post );
115
116 echo '<hr class="beyondwords-metabox__divider" />';
117 self::heading( __( 'Voice', 'speechkit' ) );
118 ( new \BeyondWords\Editor\Components\SelectVoice() )::element( $post );
119
120 echo '<hr class="beyondwords-metabox__divider" />';
121 self::heading( __( 'Data', 'speechkit' ) );
122 \BeyondWords\Editor\Components\ContentId::element( $post );
123
124 echo '<hr class="beyondwords-metabox__divider" />';
125 self::help();
126 }
127
128 /**
129 * Print a settings-section heading.
130 *
131 * @since 7.0.0
132 *
133 * @param string $title The section heading.
134 */
135 public static function heading( $title ) {
136 printf(
137 '<h4 class="beyondwords-metabox__heading">%s</h4>',
138 esc_html( $title )
139 );
140 }
141
142 /**
143 * The "Pending review" message for posts with "pending" status.
144 *
145 * Shown instead of the player, which cannot render audio created with
146 * { published: false }.
147 *
148 * @since 3.7.0
149 * @since 6.0.0 Make static.
150 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
151 *
152 * @var int|\WP_Post $post The WordPress post ID, or post object.
153 */
154 public static function pending_review_notice( $post ) {
155 $post = get_post( $post );
156
157 if ( ! ( $post instanceof \WP_Post ) ) {
158 return;
159 }
160
161 $project_url = sprintf(
162 '%s/dashboard/project/%d/content',
163 \BeyondWords\Core\Urls::get_dashboard_url(),
164 \BeyondWords\Post\Meta::get_project_id( $post->ID )
165 );
166
167 ?>
168 <div id="beyondwords-pending-review-message">
169 <?php
170 printf(
171 /* translators: %s is replaced with the link to the BeyondWords dashboard */
172 esc_html__( 'Listen to content saved as “Pending” in the %s.', 'speechkit' ),
173 sprintf(
174 '<a href="%s" target="_blank" rel="nofollow">%s</a>',
175 esc_url( $project_url ),
176 esc_html__( 'BeyondWords dashboard', 'speechkit' )
177 )
178 );
179 ?>
180 </div>
181 <?php
182 }
183
184 /**
185 * Embed a player for a WordPress post.
186 *
187 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
188 *
189 * @param int|\WP_Post|null $post (Optional) Post ID, or WP_Post object, or null.
190 *
191 * @since 3.x Introduced
192 * @since 4.0.1 Admin player init is now all in this one function.
193 * @since 6.0.0 Make static and add Magic Embed support.
194 */
195 public static function player_embed( $post = null ) {
196 $post = get_post( $post );
197
198 if ( ! ( $post instanceof \WP_Post ) ) {
199 return;
200 }
201
202 $project_id = \BeyondWords\Post\Meta::get_project_id( $post->ID );
203 $has_content = \BeyondWords\Post\Meta::has_content( $post->ID );
204
205 if ( ! $project_id || ! $has_content ) {
206 return;
207 }
208
209 $content_id = \BeyondWords\Post\Meta::get_content_id( $post->ID );
210 $preview_token = \BeyondWords\Post\Meta::get_preview_token( $post->ID );
211
212 // phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedScript
213 if ( ! empty( $content_id ) ) :
214 /*
215 * Still-processing content would 404 (and the CDN would cache it), so
216 * render a loading state and let classic-metabox.js poll until `processed`.
217 * The untrusted content ID / preview token travel as esc_attr()'d data-*
218 * attributes — never interpolated into a JS execution context.
219 */
220 ?>
221 <div
222 id="beyondwords-metabox-player"
223 role="status"
224 aria-live="polite"
225 style="margin: 13px 0;"
226 data-project-id="<?php echo esc_attr( $project_id ); ?>"
227 data-content-id="<?php echo esc_attr( $content_id ); ?>"
228 data-preview-token="<?php echo esc_attr( $preview_token ); ?>"
229 >
230 <span
231 class="spinner is-active"
232 style="float: none; margin: 0 8px 0 0;"
233 ></span>
234 <span class="beyondwords-player-loading-text">
235 <?php esc_html_e( 'Generating…', 'speechkit' ); ?>
236 </span>
237 </div>
238 <script
239 defer
240 src='<?php echo esc_url( \BeyondWords\Core\Urls::get_js_sdk_url() ); ?>'
241 ></script>
242 <?php
243 else :
244 /*
245 * Client-side integration is keyed on the source ID — nothing to poll, so
246 * embed immediately. The untrusted preview token is JSON_HEX_*-encoded so
247 * no value breaks out of the onload attribute. Mirrors Javascript::render().
248 */
249 $config = [
250 'projectId' => (int) $project_id,
251 'sourceId' => (string) $post->ID,
252 'previewToken' => (string) $preview_token,
253 'adverts' => [],
254 'analyticsConsent' => 'none',
255 'introsOutros' => [],
256 'playerStyle' => 'small',
257 'widgetStyle' => 'none',
258 ];
259
260 $onload = sprintf(
261 'const player = new BeyondWords.Player({ target: this.parentElement, ...%s });',
262 wp_json_encode(
263 $config,
264 JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
265 )
266 );
267 ?>
268 <div id="beyondwords-metabox-player" style="margin: 13px 0;">
269 <script defer
270 src='<?php echo esc_url( \BeyondWords\Core\Urls::get_js_sdk_url() ); ?>'
271 onload='<?php echo esc_attr( $onload ); ?>'
272 >
273 </script>
274 </div>
275 <?php
276 endif;
277 // phpcs:enable WordPress.WP.EnqueuedResources.NonEnqueuedScript
278 }
279
280 /**
281 * Render a loading state while deferred audio generation is still queued.
282 *
283 * See doc/async-rest-migration.md.
284 *
285 * @since 7.0.0
286 *
287 * @param int|\WP_Post|null $post (Optional) Post ID, or WP_Post object, or null.
288 */
289 public static function awaiting_generation_embed( $post = null ) {
290 $post = get_post( $post );
291
292 if ( ! ( $post instanceof \WP_Post ) ) {
293 return;
294 }
295
296 // No queued job means no audio is coming, so a spinner would never resolve.
297 if ( ! wp_next_scheduled( \BeyondWords\Post\Sync::GENERATE_AUDIO_CRON_HOOK, [ $post->ID ] ) ) {
298 return;
299 }
300
301 $project_id = \BeyondWords\Post\Meta::get_project_id( $post->ID );
302
303 if ( ! $project_id ) {
304 return;
305 }
306
307 // phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedScript
308 ?>
309 <div
310 id="beyondwords-metabox-player"
311 role="status"
312 aria-live="polite"
313 style="margin: 13px 0;"
314 data-project-id="<?php echo esc_attr( $project_id ); ?>"
315 data-post-id="<?php echo esc_attr( $post->ID ); ?>"
316 data-await-content="1"
317 >
318 <span
319 class="spinner is-active"
320 style="float: none; margin: 0 8px 0 0;"
321 ></span>
322 <span class="beyondwords-player-loading-text">
323 <?php esc_html_e( 'Generating…', 'speechkit' ); ?>
324 </span>
325 </div>
326 <script
327 defer
328 src='<?php echo esc_url( \BeyondWords\Core\Urls::get_js_sdk_url() ); ?>'
329 ></script>
330 <?php
331 // phpcs:enable WordPress.WP.EnqueuedResources.NonEnqueuedScript
332 }
333
334 /**
335 * Display errors for the post.
336 *
337 * @since 6.0.0 Make static.
338 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
339 */
340 public static function errors( $post ) {
341 $error = \BeyondWords\Post\Meta::get_error_message( $post->ID );
342
343 if ( $error ) :
344 ?>
345 <div id="beyondwords-metabox-errors">
346 <div class="beyondwords-error">
347 <p>
348 <?php echo esc_html( $error ); ?>
349 </p>
350 </div>
351 <?php self::regenerate_instructions(); ?>
352 </div>
353 <?php
354 endif;
355 }
356
357 /**
358 * Display help text for the metabox.
359 *
360 * @since 6.0.0 Make static.
361 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
362 */
363 public static function help() {
364 ?>
365 <p id="beyondwords-metabox-help">
366 <?php
367 printf(
368 /* translators: %s is replaced with the link to the support email address */
369 esc_html__( 'Need help? Email our support team on %s', 'speechkit' ),
370 sprintf( '<a href="%s">%s</a>', 'mailto:[email protected]', '[email protected]' )
371 );
372 ?>
373 </p>
374 <?php
375 }
376
377 /**
378 * Display instructions for regenerating audio.
379 *
380 * @since 6.0.0 Make static.
381 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
382 */
383 public static function regenerate_instructions() {
384 ?>
385 <!-- Update/regenerate -->
386 <p>
387 <?php
388 esc_html_e(
389 'To create audio, resolve the error above then select ‘Update’ with ‘Generate audio’ checked.', // phpcs:ignore Generic.Files.LineLength.TooLong
390 'speechkit'
391 );
392 ?>
393 </p>
394 <?php
395 }
396 }
397