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 / components / generate-audio / index.js

index.js in BeyondWords – AI audio for publishers 7.2.0, at src/editor/components/generate-audio/index.js

203 lines 4.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * WordPress dependencies
3 */
4 import { __ } from '@wordpress/i18n';
5 import { useSelect, useDispatch } from '@wordpress/data';
6 import { Fragment } from '@wordpress/element';
7
8 /**
9 * Internal dependencies
10 */
11 import Toggle from '../toggle';
12
13 const META_KEY = 'beyondwords_generate_audio';
14
15 /**
16 * Parse a stored Generate audio flag.
17 *
18 * @param {*} value The meta value.
19 *
20 * @return {boolean|null} true/false when set, null when unset.
21 */
22 const parseFlag = ( value ) => {
23 if ( value === '1' ) {
24 return true;
25 }
26 if ( value === '0' ) {
27 return false;
28 }
29 return null;
30 };
31
32 /**
33 * Resolve the preselect mode for a post type's config.
34 *
35 * Tolerant of the pre-7.0.0 shapes (`'1'` and a bare taxonomy array).
36 *
37 * @param {*} config The preselect entry for a post type.
38 *
39 * @return {string} One of 'off', 'all', 'terms'.
40 */
41 const resolveMode = ( config ) => {
42 if ( config === '1' || config === 1 || config === true ) {
43 return 'all';
44 }
45
46 if ( config && typeof config === 'object' ) {
47 if ( config.mode === 'all' || config.mode === 'terms' ) {
48 return config.mode;
49 }
50 // Legacy { taxonomy: [ ids ] } shape, before migration.
51 if ( ! ( 'mode' in config ) ) {
52 return Object.keys( config ).length ? 'terms' : 'off';
53 }
54 }
55
56 return 'off';
57 };
58
59 /**
60 * Resolve the selected term map for a post type's config.
61 *
62 * @param {*} config The preselect entry for a post type.
63 *
64 * @return {Object} Map of taxonomy slug to term IDs.
65 */
66 const resolveTerms = ( config ) => {
67 if ( config && typeof config === 'object' ) {
68 if (
69 config.mode === 'terms' &&
70 config.terms &&
71 typeof config.terms === 'object'
72 ) {
73 return config.terms;
74 }
75 if ( ! ( 'mode' in config ) ) {
76 return config; // Legacy shape.
77 }
78 }
79 return {};
80 };
81
82 export function GenerateAudio( { wrapper } ) {
83 const Wrapper = wrapper || Fragment;
84
85 const { editPost } = useDispatch( 'core/editor' );
86
87 const { checked } = useSelect( ( select ) => {
88 const {
89 getCurrentPostType,
90 getCurrentPostAttribute,
91 getEditedPostAttribute,
92 getPostEdits,
93 } = select( 'core/editor' );
94
95 const { getTaxonomy } = select( 'core' );
96 const { getSettings } = select( 'beyondwords/settings' );
97
98 const postType = getCurrentPostType();
99 const savedMeta = getCurrentPostAttribute( 'meta' ) || {};
100 const editedMeta = getPostEdits()?.meta || {};
101
102 // The user's explicit decision, if any: an in-session edit wins over
103 // saved meta.
104 let explicit;
105 if ( META_KEY in editedMeta ) {
106 explicit = parseFlag( editedMeta[ META_KEY ] );
107 } else {
108 explicit =
109 parseFlag( savedMeta[ META_KEY ] ) ??
110 parseFlag( savedMeta.speechkit_generate_audio );
111 }
112
113 // Reactive: for term-gated post types this recomputes as taxonomy terms
114 // are edited, so the toggle follows the preselect rule in both directions.
115 const getShouldPreselect = () => {
116 const settings = getSettings();
117 if ( ! settings ) {
118 return false;
119 }
120
121 const preselect =
122 settings.preselect && typeof settings.preselect === 'object'
123 ? settings.preselect
124 : {};
125
126 if ( ! ( postType in preselect ) ) {
127 return false;
128 }
129
130 const config = preselect[ postType ];
131 const mode = resolveMode( config );
132
133 if ( mode === 'all' ) {
134 return true;
135 }
136 if ( mode !== 'terms' ) {
137 return false;
138 }
139
140 const terms = resolveTerms( config );
141
142 // OR across every listed taxonomy/term (exact term-ID match).
143 return Object.keys( terms ).some( ( slug ) => {
144 const ids = terms[ slug ];
145 if ( ! Array.isArray( ids ) || ! ids.length ) {
146 return false;
147 }
148
149 // Tolerant: skip taxonomies that aren't registered/loaded.
150 const taxonomy = getTaxonomy( slug );
151 if ( ! taxonomy ) {
152 return false;
153 }
154
155 const assigned = getEditedPostAttribute(
156 taxonomy.rest_base || slug
157 );
158 if ( ! Array.isArray( assigned ) ) {
159 return false;
160 }
161
162 const wanted = ids.map( String );
163 return assigned.some( ( id ) =>
164 wanted.includes( String( id ) )
165 );
166 } );
167 };
168
169 return {
170 // Derived only — no meta write for the preselect case, so the post isn't
171 // dirtied; the server persists it via Preselect::should_preselect_for_post().
172 checked:
173 explicit !== undefined && explicit !== null
174 ? explicit
175 : getShouldPreselect(),
176 };
177 }, [] );
178
179 const handleChange = () => {
180 // An explicit choice is a real edit (dirties the post) and overrides the
181 // preselect rule from then on.
182 editPost( { meta: { [ META_KEY ]: ! checked ? '1' : '0' } } );
183 };
184
185 // State-reflecting caption: the label reads the current state, not the action.
186 const label = checked
187 ? __( 'Generation enabled', 'speechkit' )
188 : __( 'Generation disabled', 'speechkit' );
189
190 return (
191 <Wrapper>
192 <Toggle
193 className="beyondwords--generate-audio"
194 label={ label }
195 checked={ Boolean( checked ) }
196 onChange={ handleChange }
197 />
198 </Wrapper>
199 );
200 }
201
202 export default GenerateAudio;
203