Files
2026-05-11 22:00:32 +02:00

312 lines
10 KiB
JavaScript

/**
* Piperless Gutenberg Sidebar Plugin
*
* Registers a PluginSidebar panel in the post editor with:
* - Generate / Regenerate audio button
* - Per-post voice, language, quality overrides
* - Audio status display with preview player
*
* @package Piperless
*/
( function ( wp ) {
const { __ } = wp.i18n;
const { registerPlugin } = wp.plugins;
const { PluginSidebar } = wp.editPost;
const {
PanelBody,
PanelRow,
Button,
SelectControl,
TextControl,
Notice,
Spinner,
} = wp.components;
const { useSelect, useDispatch } = wp.data;
const { useState, useEffect, createElement } = wp.element;
const apiFetch = wp.apiFetch;
/**
* Format seconds to mm:ss.
*/
function formatDuration( secs ) {
if ( ! secs || ! isFinite( secs ) ) return '';
const m = Math.floor( secs / 60 );
const s = Math.floor( secs % 60 );
return String( m ).padStart( 2, '0' ) + ':' + String( s ).padStart( 2, '0' );
}
/**
* Main sidebar component.
*/
function PiperlessSidebar() {
const postId = useSelect( function ( select ) {
return select( 'core/editor' ).getCurrentPostId();
} );
const postMeta = useSelect( function ( select ) {
return select( 'core/editor' ).getEditedPostAttribute( 'meta' ) || {};
} );
const { editPost } = useDispatch( 'core/editor' );
const { createNotice } = useDispatch( 'core/notices' );
const [ generating, setGenerating ] = useState( false );
const [ audioUrl, setAudioUrl ] = useState( null );
const [ audioFormat, setAudioFormat ] = useState( 'mp3' );
const [ audioDuration, setAudioDuration ] = useState( null );
const [ models, setModels ] = useState( [] );
const [ voices, setVoices ] = useState( [] );
const [ voiceAliases, setVoiceAliases ] = useState( {} );
const [ languagesList, setLanguages ] = useState( [] );
const [ qualitiesList, setQualities ] = useState( [] );
const [ error, setError ] = useState( null );
// Load initial audio status and models on mount.
useEffect( function () {
if ( ! postId ) return;
// Load audio status.
apiFetch( { path: '/piperless/v1/status/' + postId } )
.then( function ( data ) {
if ( data.has_audio ) {
setAudioUrl( data.url );
setAudioFormat( data.format || 'mp3' );
setAudioDuration( data.duration );
}
} )
.catch( function () {} );
// Load available models.
apiFetch( { path: '/piperless/v1/models' } )
.then( function ( data ) {
setModels( data.models || [] );
setVoices( data.voices || [] );
setVoiceAliases( data.voice_aliases || {} );
setLanguages( data.languages || [] );
setQualities( data.qualities || [] );
} )
.catch( function () {} );
}, [ postId ] );
// Handle generate click.
function handleGenerate() {
if ( audioUrl && ! confirm( piperlessEditor.i18n.confirmRegen ) ) {
return;
}
setGenerating( true );
setError( null );
apiFetch( {
path: '/piperless/v1/generate',
method: 'POST',
data: {
post_id: postId,
voice: postMeta._piperless_voice || '',
language: postMeta._piperless_language || '',
quality: postMeta._piperless_quality || '',
},
} )
.then( function ( data ) {
setAudioUrl( data.url );
setAudioFormat( data.format || 'mp3' );
setAudioDuration( data.duration );
setGenerating( false );
createNotice( 'success', piperlessEditor.i18n.success, {
type: 'snackbar',
} );
} )
.catch( function ( err ) {
setError( err.message || piperlessEditor.i18n.error );
setGenerating( false );
} );
}
// Handle remove.
function handleRemove() {
apiFetch( {
path: '/piperless/v1/audio/' + postId,
method: 'DELETE',
} )
.then( function () {
setAudioUrl( null );
setAudioDuration( null );
} );
}
// Build voice options — use alias as label when available.
const voiceOptions = voices.map( function ( v ) {
var alias = voiceAliases[ v ] || '';
return { value: v, label: alias || v };
} );
const langOptions = languagesList.map( function ( l ) {
return { value: l, label: l };
} );
const qualityOptions = qualitiesList.map( function ( q ) {
return { value: q, label: q.charAt( 0 ).toUpperCase() + q.slice( 1 ) };
} );
if ( qualityOptions.length === 0 ) {
qualityOptions.push(
{ value: 'low', label: 'Low' },
{ value: 'medium', label: 'Medium' },
{ value: 'high', label: 'High' }
);
}
function updateMeta( key, value ) {
editPost( { meta: { [ key ]: value } } );
}
return createElement(
PluginSidebar,
{
name: 'piperless-sidebar',
title: piperlessEditor.i18n.title,
icon: 'controls-volumeon',
},
// ── Generation Controls ─────────────────────────────────────
createElement(
PanelBody,
{ title: __( 'Generation', 'piperless' ), initialOpen: true },
createElement( PanelRow, null,
createElement( Button, {
variant: 'primary',
onClick: handleGenerate,
disabled: generating,
style: { width: '100%', justifyContent: 'center' },
},
generating
? createElement( 'span', null,
createElement( Spinner, null ),
' ' + piperlessEditor.i18n.generating
)
: ( audioUrl
? piperlessEditor.i18n.regenerate
: piperlessEditor.i18n.generate
)
)
),
createElement( PanelRow, null,
createElement( TextControl, {
label: piperlessEditor.i18n.titleLabel,
placeholder: piperlessEditor.i18n.titlePlaceholder,
value: postMeta._piperless_title || '',
onChange: function ( val ) { updateMeta( '_piperless_title', val ); },
} )
),
audioUrl && createElement( PanelRow, null,
createElement( Button, {
variant: 'link',
isDestructive: true,
onClick: handleRemove,
}, piperlessEditor.i18n.remove )
),
error && createElement( PanelRow, null,
createElement( Notice, { status: 'error', isDismissible: false },
error
)
)
),
// ── Voice / Language / Quality ──────────────────────────────
createElement(
PanelBody,
{ title: __( 'Voice Settings', 'piperless' ), initialOpen: false },
voices.length > 0 && createElement( SelectControl, {
label: piperlessEditor.i18n.voiceLabel,
value: postMeta._piperless_voice || '',
options: [ { value: '', label: '— Default —' } ].concat( voiceOptions ),
onChange: function ( val ) { updateMeta( '_piperless_voice', val ); },
} ),
languagesList.length > 0 && createElement( SelectControl, {
label: piperlessEditor.i18n.languageLabel,
value: postMeta._piperless_language || '',
options: [ { value: '', label: '— Default —' } ].concat( langOptions ),
onChange: function ( val ) { updateMeta( '_piperless_language', val ); },
} ),
createElement( SelectControl, {
label: piperlessEditor.i18n.qualityLabel,
value: postMeta._piperless_quality || '',
options: [ { value: '', label: '— Default —' } ].concat( qualityOptions ),
onChange: function ( val ) { updateMeta( '_piperless_quality', val ); },
} ),
createElement( TextControl, {
label: piperlessEditor.i18n.sentenceSilenceLabel || 'Sentence Silence',
help: piperlessEditor.i18n.sentenceSilenceHelp || 'Seconds of silence after sentences. Leave empty for default.',
placeholder: '0.2',
value: postMeta._piperless_sentence_silence || '',
onChange: function ( val ) { updateMeta( '_piperless_sentence_silence', val ); },
} ),
createElement( TextControl, {
label: piperlessEditor.i18n.lengthScaleLabel || 'Length Scale',
help: piperlessEditor.i18n.lengthScaleHelp || 'Word spacing override. 1.0 = normal. Leave empty for quality preset.',
placeholder: '1.0',
value: postMeta._piperless_length_scale || '',
onChange: function ( val ) { updateMeta( '_piperless_length_scale', val ); },
} ),
),
// ── Display Settings ────────────────────────────────────────
createElement(
PanelBody,
{ title: __( 'Display Settings', 'piperless' ), initialOpen: false },
createElement( SelectControl, {
label: piperlessEditor.i18n.styleLabel || 'Player Style',
value: postMeta._piperless_style || '',
options: [
{ value: '', label: '— Default —' },
{ value: 'classic', label: 'Classic' },
{ value: 'minimal', label: 'Minimal' },
{ value: 'dark', label: 'Modern Dark' },
{ value: 'newsviews', label: 'Ron Burgundy' },
{ value: 'newsviews-classic', label: 'Dan Rather Blue' },
],
onChange: function ( val ) { updateMeta( '_piperless_style', val ); },
} ),
createElement( SelectControl, {
label: piperlessEditor.i18n.placementLabel || 'Placement',
value: postMeta._piperless_placement || '',
options: [
{ value: '', label: '— Default —' },
{ value: 'before', label: 'Before content' },
{ value: 'after', label: 'After content' },
{ value: 'both', label: 'Above & below' },
{ value: 'manual', label: 'Manual (shortcode only)' },
],
onChange: function ( val ) { updateMeta( '_piperless_placement', val ); },
} )
),
// ── Audio Preview ───────────────────────────────────────────
createElement(
PanelBody,
{ title: __( 'Preview', 'piperless' ), initialOpen: true },
audioUrl
? createElement( 'div', null,
createElement( 'audio', {
controls: true,
style: { width: '100%', marginBottom: '8px' },
},
createElement( 'source', {
src: audioUrl,
type: audioFormat === 'opus' ? 'audio/ogg' : 'audio/mpeg',
} ),
piperlessEditor.i18n.noAudio
),
audioDuration && createElement( 'p', {
style: { color: '#757575', fontSize: '12px' },
}, piperlessEditor.i18n.duration + ' ' + formatDuration( audioDuration ) )
)
: createElement( 'p', {
style: { color: '#757575', fontStyle: 'italic' },
}, piperlessEditor.i18n.noAudio )
)
);
}
registerPlugin( 'piperless-sidebar', {
render: PiperlessSidebar,
} );
} )( window.wp );