PluginProbe ʕ •ᴥ•ʔ
Presto Player / trunk
Presto Player vtrunk
4.3.1 4.3.0 4.2.4 4.2.3 4.2.2 4.2.0 4.2.1 trunk 1.10.0 1.10.1 1.10.2 1.11.0 1.12.0 1.13.0 1.14.0 1.14.1 1.5.10 1.5.11 1.5.12 1.5.13 1.5.14 1.5.15 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.10 1.6.11 1.6.12 1.6.13 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.8.0 1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15 2.0.16 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.2.0 2.2.1 2.2.2 2.2.3 2.2.3-beta1 2.3.0 2.3.1 2.3.2 2.3.3 3.0.0 3.0.0-beta1 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.1.0 3.1.1 3.1.2 3.1.3 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4
presto-player / src / admin / dashboard / pages / settings / integrations / BunnyNetPage.js
presto-player / src / admin / dashboard / pages / settings / integrations Last commit date
BunnyNetPage.js 1 month ago EmailCapturePage.js 1 month ago GoogleAnalyticsPage.js 1 month ago WebhooksPage.js 1 month ago YouTubePage.js 1 month ago
BunnyNetPage.js
483 lines
1 import { __ } from '@wordpress/i18n';
2 import { useState } from 'react';
3 import {
4 Container,
5 Switch,
6 Label,
7 Input,
8 Text,
9 Title,
10 toast,
11 Tooltip,
12 } from '@bsf/force-ui';
13 import { Info } from 'lucide-react';
14 import SettingsPageShell from '../shared/SettingsPageShell';
15 import ProGate from '../shared/ProGate';
16 import LabelWithTooltip from '../shared/LabelWithTooltip';
17 import ConfirmDialog from '../shared/ConfirmDialog';
18 import useSettingOption from '../../../hooks/useSettingOption';
19 import useRegisterActivePage from '../../../hooks/useRegisterActivePage';
20 import { useSettingsData } from '../shared/SettingsDataProvider';
21 import { OPTION_KEYS, BUNNY_STREAM_KEYS } from '../config';
22 // eslint-disable-next-line import/no-unresolved
23 import TranscriptionLanguageSelect from '@/admin/blocks/shared/components/TranscriptionLanguageSelect';
24
25 const isPremium = window.prestoPlayer?.isPremium ?? false;
26
27 const Subsection = ( { title, description, children } ) => (
28 <Container.Item className="flex flex-col gap-3">
29 <div className="flex flex-col gap-1">
30 <Title tag="h4" title={ title } size="xs" className="font-semibold" />
31 { description && (
32 <Text size="xs" className="text-text-secondary">
33 { description }
34 </Text>
35 ) }
36 </div>
37 <Container.Item className="flex flex-col gap-4">
38 { children }
39 </Container.Item>
40 </Container.Item>
41 );
42
43 const CollapsibleSection = ( { title, description, children } ) => {
44 return (
45 <Container
46 direction="column"
47 className="bg-background-primary border border-solid border-border-subtle rounded-xl shadow-sm p-5 gap-4"
48 >
49 <Title tag="h4" title={ title } size="xs" className="font-semibold" />
50 { description && (
51 <Text size="sm" className="text-text-secondary">
52 { description }
53 </Text>
54 ) }
55 <Container direction="column" className="gap-4">
56 { children }
57 </Container>
58 </Container>
59 );
60 };
61
62 const BunnyNetPage = ( { registerActivePage } ) => {
63 const { saveSlice } = useSettingsData();
64 const stream = useSettingOption( OPTION_KEYS.bunnyStream, {} );
65 const streamPublic = useSettingOption( OPTION_KEYS.bunnyStreamPublic, {} );
66 const streamPrivate = useSettingOption( OPTION_KEYS.bunnyStreamPrivate, {} );
67 const pullZones = useSettingOption( OPTION_KEYS.bunnyPullZones, {} );
68
69 const [ editBunny, setEditBunny ] = useState( false );
70 const [ showTranscribeConfirm, setShowTranscribeConfirm ] = useState( false );
71 const [ isSaving, setIsSaving ] = useState( false );
72
73 const updateStream = ( patch ) =>
74 stream.setData( ( p ) => ( { ...( p || {} ), ...patch } ) );
75 const updateStreamPublic = ( patch ) =>
76 streamPublic.setData( ( p ) => ( { ...( p || {} ), ...patch } ) );
77 const updateStreamPrivate = ( patch ) =>
78 streamPrivate.setData( ( p ) => ( { ...( p || {} ), ...patch } ) );
79 const updatePullZones = ( patch ) =>
80 pullZones.setData( ( p ) => ( { ...( p || {} ), ...patch } ) );
81
82 const isDirty =
83 stream.isDirty ||
84 streamPublic.isDirty ||
85 streamPrivate.isDirty ||
86 pullZones.isDirty;
87 const isLoading = stream.isLoading;
88
89 // Bunny rejects transcription with zero languages — surface this as a
90 // save-time block via the shell's `canSave` prop, but keep `isDirty` honest
91 // so the unsaved-changes modal + beforeunload guard still fire for the
92 // *other* Bunny fields the user may have edited.
93 const isTranscriptionInvalid =
94 !! stream.data?.transcribe_enabled &&
95 ( ! stream.data?.transcribe_languages ||
96 stream.data.transcribe_languages.length === 0 );
97
98 const handleSave = async () => {
99 if ( isTranscriptionInvalid ) {
100 toast.error(
101 __(
102 'Please select at least one transcription language before saving.',
103 'presto-player'
104 )
105 );
106 return;
107 }
108 setIsSaving( true );
109 try {
110 await saveSlice( BUNNY_STREAM_KEYS );
111 toast.success( __( 'Settings saved.', 'presto-player' ), {
112 autoDismiss: 3000,
113 } );
114 } catch ( err ) {
115 toast.error( __( 'Could not save settings.', 'presto-player' ), {
116 autoDismiss: 5000,
117 } );
118 } finally {
119 setIsSaving( false );
120 }
121 };
122
123 const handleDiscard = () => {
124 stream.reset();
125 streamPublic.reset();
126 streamPrivate.reset();
127 pullZones.reset();
128 };
129
130 useRegisterActivePage( registerActivePage, {
131 isDirty,
132 save: handleSave,
133 discard: handleDiscard,
134 } );
135
136 return (
137 <SettingsPageShell
138 title={ __( 'Bunny.net', 'presto-player' ) }
139 isDirty={ isDirty }
140 isSaving={ isSaving }
141 isLoading={ isLoading }
142 canSave={ isDirty && ! isSaving && ! isTranscriptionInvalid }
143 onSave={ handleSave }
144 >
145 <ProGate
146 enabled={ isPremium }
147 title={ __( 'Bunny.net Integration', 'presto-player' ) }
148 description={ __(
149 'Serve secure, high-performance video through your Bunny.net account.',
150 'presto-player'
151 ) }
152 >
153 <Container direction="column" className="gap-4">
154 <CollapsibleSection
155 slug="streaming-defaults"
156 title={ __( 'Streaming Defaults', 'presto-player' ) }
157 description={ __(
158 'Default playback behavior for all Bunny.net streams.',
159 'presto-player'
160 ) }
161 >
162 <Container direction="column" className="gap-4">
163 <Container.Item className="flex flex-col gap-2">
164 <Label size="sm">
165 { __( 'Initial Quality Level', 'presto-player' ) }
166 </Label>
167 <Input
168 size="md"
169 type="number"
170 value={ stream.data?.hls_start_level ?? '' }
171 placeholder="480"
172 onChange={ ( val ) => updateStream( { hls_start_level: val } ) }
173 />
174 <Label size="xs" variant="help">
175 { __(
176 'Set the default quality start level for all streams (e.g. 240, 360, 480, 720, 1080).',
177 'presto-player'
178 ) }
179 </Label>
180 </Container.Item>
181
182 <Switch
183 size="md"
184 value={ !! stream.data?.disable_legacy_storage }
185 onChange={ ( val ) =>
186 updateStream( { disable_legacy_storage: val } )
187 }
188 label={ {
189 heading: __(
190 'Disable Classic Bunny.net Storage',
191 'presto-player'
192 ),
193 description: __(
194 'This will disable Bunny.net classic storage in your block UI.',
195 'presto-player'
196 ),
197 } }
198 />
199 </Container>
200 </CollapsibleSection>
201
202 <CollapsibleSection
203 slug="connection-settings"
204 title={ __( 'Connection Settings', 'presto-player' ) }
205 description={ __(
206 'Manage your Bunny.net library credentials. Only edit if you know what you\'re doing.',
207 'presto-player'
208 ) }
209 >
210 <Container direction="column" className="gap-4">
211 <Switch
212 size="md"
213 value={ editBunny }
214 onChange={ setEditBunny }
215 label={ {
216 heading: __( 'Edit Connection Settings', 'presto-player' ),
217 description: __(
218 "Toggle on to view and edit your Bunny.net library and storage credentials.",
219 'presto-player'
220 ),
221 } }
222 />
223
224 { editBunny && (
225 <Container.Item className="flex flex-col gap-4">
226 <Subsection
227 title={ __( 'Bunny Stream (Public)', 'presto-player' ) }
228 >
229 <Container.Item className="flex flex-col gap-2">
230 <LabelWithTooltip
231 label={ __( 'Public Stream Library ID', 'presto-player' ) }
232 tooltip={ __( 'Found in your Bunny Stream library settings.', 'presto-player' ) }
233 />
234 <Input
235 size="md"
236 value={ streamPublic.data?.video_library_id || '' }
237 onChange={ ( val ) =>
238 updateStreamPublic( { video_library_id: val } )
239 }
240 />
241 </Container.Item>
242 <Container.Item className="flex flex-col gap-2">
243 <LabelWithTooltip
244 label={ __( 'Public Stream Library API Key', 'presto-player' ) }
245 tooltip={ __( 'API key from your Bunny Stream library settings.', 'presto-player' ) }
246 />
247 <Input
248 size="md"
249 value={ streamPublic.data?.video_library_api_key || '' }
250 onChange={ ( val ) =>
251 updateStreamPublic( { video_library_api_key: val } )
252 }
253 />
254 </Container.Item>
255 <Container.Item className="flex flex-col gap-2">
256 <LabelWithTooltip
257 label={ __( 'Public Stream CDN Hostname', 'presto-player' ) }
258 tooltip={ __( 'Pull zone hostname for your public stream (e.g. your-library.b-cdn.net).', 'presto-player' ) }
259 />
260 <Input
261 size="md"
262 value={ streamPublic.data?.pull_zone_url || '' }
263 onChange={ ( val ) =>
264 updateStreamPublic( { pull_zone_url: val } )
265 }
266 />
267 </Container.Item>
268 </Subsection>
269
270 <Subsection
271 title={ __( 'Bunny Stream (Private)', 'presto-player' ) }
272 >
273 <Container.Item className="flex flex-col gap-2">
274 <LabelWithTooltip
275 label={ __( 'Private Stream Library ID', 'presto-player' ) }
276 tooltip={ __( 'Found in your private Bunny Stream library settings.', 'presto-player' ) }
277 />
278 <Input
279 size="md"
280 value={ streamPrivate.data?.video_library_id || '' }
281 onChange={ ( val ) =>
282 updateStreamPrivate( { video_library_id: val } )
283 }
284 />
285 </Container.Item>
286 <Container.Item className="flex flex-col gap-2">
287 <LabelWithTooltip
288 label={ __( 'Private Stream Library API Key', 'presto-player' ) }
289 tooltip={ __( 'API key from your private Bunny Stream library settings.', 'presto-player' ) }
290 />
291 <Input
292 size="md"
293 value={ streamPrivate.data?.video_library_api_key || '' }
294 onChange={ ( val ) =>
295 updateStreamPrivate( { video_library_api_key: val } )
296 }
297 />
298 </Container.Item>
299 <Container.Item className="flex flex-col gap-2">
300 <LabelWithTooltip
301 label={ __( 'Private Stream CDN Hostname', 'presto-player' ) }
302 tooltip={ __( 'Pull zone hostname for your private stream.', 'presto-player' ) }
303 />
304 <Input
305 size="md"
306 value={ streamPrivate.data?.pull_zone_url || '' }
307 onChange={ ( val ) =>
308 updateStreamPrivate( { pull_zone_url: val } )
309 }
310 />
311 </Container.Item>
312 <Container.Item className="flex flex-col gap-2">
313 <LabelWithTooltip
314 label={ __( 'Private Stream Token Authentication Key', 'presto-player' ) }
315 tooltip={ __( 'Used to sign secure video URLs. Found in your Bunny Stream library settings under "Token Authentication".', 'presto-player' ) }
316 />
317 <Input
318 size="md"
319 value={ streamPrivate.data?.token_auth_key || '' }
320 onChange={ ( val ) =>
321 updateStreamPrivate( { token_auth_key: val } )
322 }
323 />
324 </Container.Item>
325 </Subsection>
326
327 <Subsection
328 title={ __( 'Bunny.net Storage (Classic)', 'presto-player' ) }
329 description={ __(
330 'To change your API key, click "Reconnect" from a Bunny.net block.',
331 'presto-player'
332 ) }
333 >
334 <Container.Item className="flex flex-col gap-2">
335 <LabelWithTooltip
336 label={ __( 'Public ID', 'presto-player' ) }
337 tooltip={ __( 'Pull zone ID for your public Bunny.net storage zone.', 'presto-player' ) }
338 />
339 <Input
340 size="md"
341 value={ pullZones.data?.public_id || '' }
342 onChange={ ( val ) =>
343 updatePullZones( { public_id: val } )
344 }
345 />
346 </Container.Item>
347 <Container.Item className="flex flex-col gap-2">
348 <LabelWithTooltip
349 label={ __( 'Public Host Name', 'presto-player' ) }
350 tooltip={ __( 'CDN hostname for your public storage pull zone.', 'presto-player' ) }
351 />
352 <Input
353 size="md"
354 value={ pullZones.data?.public_hostname || '' }
355 onChange={ ( val ) =>
356 updatePullZones( { public_hostname: val } )
357 }
358 />
359 </Container.Item>
360 <Container.Item className="flex flex-col gap-2">
361 <LabelWithTooltip
362 label={ __( 'Private ID', 'presto-player' ) }
363 tooltip={ __( 'Pull zone ID for your private Bunny.net storage zone.', 'presto-player' ) }
364 />
365 <Input
366 size="md"
367 value={ pullZones.data?.private_id || '' }
368 onChange={ ( val ) =>
369 updatePullZones( { private_id: val } )
370 }
371 />
372 </Container.Item>
373 <Container.Item className="flex flex-col gap-2">
374 <LabelWithTooltip
375 label={ __( 'Private Host Name', 'presto-player' ) }
376 tooltip={ __( 'CDN hostname for your private storage pull zone.', 'presto-player' ) }
377 />
378 <Input
379 size="md"
380 value={ pullZones.data?.private_hostname || '' }
381 onChange={ ( val ) =>
382 updatePullZones( { private_hostname: val } )
383 }
384 />
385 </Container.Item>
386 <Container.Item className="flex flex-col gap-2">
387 <LabelWithTooltip
388 label={ __( 'Private URL Token Authentication Key', 'presto-player' ) }
389 tooltip={ __( 'Security key for URL signing in classic storage. Found in your Bunny CDN pull zone settings.', 'presto-player' ) }
390 />
391 <Input
392 size="md"
393 type="password"
394 value={ pullZones.data?.private_security_key || '' }
395 onChange={ ( val ) =>
396 updatePullZones( { private_security_key: val } )
397 }
398 />
399 </Container.Item>
400 </Subsection>
401 </Container.Item>
402 ) }
403 </Container>
404 </CollapsibleSection>
405
406 <CollapsibleSection
407 slug="automatic-captions"
408 title={ __( 'Automatic Captions', 'presto-player' ) }
409 description={ __(
410 'Detects speech and generates captions for new videos. Includes translation into 50+ languages.',
411 'presto-player'
412 ) }
413 >
414 <Container direction="column" className="gap-4">
415 <Switch
416 size="md"
417 value={ !! stream.data?.transcribe_enabled }
418 onChange={ ( val ) => {
419 if ( val ) {
420 setShowTranscribeConfirm( true );
421 } else {
422 updateStream( { transcribe_enabled: false } );
423 }
424 } }
425 label={ {
426 heading: __(
427 'Enable Automatic Caption Generation',
428 'presto-player'
429 ),
430 description: __(
431 'Enable auto-captioning for every video uploaded to your Bunny.net Stream Library. Translations can be managed from the player block toolbar.',
432 'presto-player'
433 ),
434 } }
435 />
436
437 { !! stream.data?.transcribe_enabled && (
438 <TranscriptionLanguageSelect
439 value={ stream.data?.transcribe_languages || [] }
440 /* eslint-disable camelcase */
441 onChange={ ( transcribe_languages ) =>
442 updateStream( { transcribe_languages } )
443 }
444 /* eslint-enable camelcase */
445 showWarning
446 />
447 ) }
448 </Container>
449 </CollapsibleSection>
450 </Container>
451 </ProGate>
452
453 <ConfirmDialog
454 open={ showTranscribeConfirm }
455 title={ __( 'Caption Generation Charges', 'presto-player' ) }
456 description={ __(
457 'This will automatically generate captions for every video you upload, unlocking automatic captions and translations. You will be charged $0.10 per minute for each language from your Bunny.net account balance.',
458 'presto-player'
459 ) }
460 confirmLabel={ __( 'Enable', 'presto-player' ) }
461 onConfirm={ () => {
462 updateStream( { transcribe_enabled: true } );
463 setShowTranscribeConfirm( false );
464 } }
465 onCancel={ () => setShowTranscribeConfirm( false ) }
466 >
467 <Text
468 as="a"
469 href="https://docs.bunny.net/docs/stream-pricing#transcribing"
470 target="_blank"
471 rel="noreferrer"
472 size="sm"
473 className="text-brand-primary-600 hover:underline"
474 >
475 { __( 'View Bunny.net pricing details', 'presto-player' ) }
476 </Text>
477 </ConfirmDialog>
478 </SettingsPageShell>
479 );
480 };
481
482 export default BunnyNetPage;
483