mirror of
https://github.com/forkless/Piperless.git
synced 2026-08-17 01:01:48 +02:00
312 lines
10 KiB
JavaScript
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 );
|