mirror of
https://github.com/xiaoyifang/goldendict-ng.git
synced 2024-11-23 20:14:05 +00:00
Win-specific: Add volume and rate tuning for TTS, fix some errors
This commit is contained in:
parent
58654a7423
commit
0fb8eed553
|
@ -973,7 +973,7 @@ void ArticleView::openLink( QUrl const & url, QUrl const & ref,
|
|||
|
||||
if ( itemMd5Id == md5Id )
|
||||
{
|
||||
SpeechClient * speechClient = new SpeechClient( i->id, this );
|
||||
SpeechClient * speechClient = new SpeechClient( *i, this );
|
||||
connect( speechClient, SIGNAL( finished() ), speechClient, SLOT( deleteLater() ) );
|
||||
speechClient->tell( text );
|
||||
break;
|
||||
|
|
16
config.cc
16
config.cc
|
@ -604,7 +604,7 @@ Class load() throw( exError )
|
|||
|
||||
QDomNode ves = root.namedItem( "voiceEngines" );
|
||||
|
||||
if ( !wss.isNull() )
|
||||
if ( !ves.isNull() )
|
||||
{
|
||||
QDomNodeList nl = ves.toElement().elementsByTagName( "voiceEngine" );
|
||||
|
||||
|
@ -617,6 +617,12 @@ Class load() throw( exError )
|
|||
v.id = ve.attribute( "id" );
|
||||
v.name = ve.attribute( "name" );
|
||||
v.iconFilename = ve.attribute( "icon" );
|
||||
v.volume = ve.attribute( "volume", "50" ).toInt();
|
||||
if( v.volume < 0 || v.volume > 100 )
|
||||
v.volume = 50;
|
||||
v.rate = ve.attribute( "rate", "50" ).toInt();
|
||||
if( v.rate < 0 || v.rate > 100 )
|
||||
v.rate = 50;
|
||||
c.voiceEngines.push_back( v );
|
||||
}
|
||||
}
|
||||
|
@ -1205,6 +1211,14 @@ void save( Class const & c ) throw( exError )
|
|||
QDomAttr icon = dd.createAttribute( "icon" );
|
||||
icon.setValue( i->iconFilename );
|
||||
v.setAttributeNode( icon );
|
||||
|
||||
QDomAttr volume = dd.createAttribute( "volume" );
|
||||
volume.setValue( QString::number( i->volume ) );
|
||||
v.setAttributeNode( volume );
|
||||
|
||||
QDomAttr rate = dd.createAttribute( "rate" );
|
||||
rate.setValue( QString::number( i->rate ) );
|
||||
v.setAttributeNode( rate );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
38
config.hh
38
config.hh
|
@ -383,25 +383,37 @@ typedef QVector< Program > Programs;
|
|||
|
||||
struct VoiceEngine
|
||||
{
|
||||
bool enabled;
|
||||
QString id;
|
||||
QString name;
|
||||
bool enabled;
|
||||
QString id;
|
||||
QString name;
|
||||
QString iconFilename;
|
||||
int volume; // 0-100 allowed
|
||||
int rate; // 0-100 allowed
|
||||
|
||||
VoiceEngine(): enabled( false )
|
||||
VoiceEngine(): enabled( false )
|
||||
, volume( 50 )
|
||||
, rate( 50 )
|
||||
{}
|
||||
VoiceEngine( QString id_, QString name_, int volume_, int rate_ ):
|
||||
enabled( false )
|
||||
, id( id_ )
|
||||
, name( name_ )
|
||||
, volume( volume_ )
|
||||
, rate( rate_ )
|
||||
{}
|
||||
|
||||
bool operator == ( VoiceEngine const & other ) const
|
||||
{
|
||||
}
|
||||
|
||||
bool operator == ( VoiceEngine const & other ) const
|
||||
{
|
||||
return enabled == other.enabled &&
|
||||
return enabled == other.enabled &&
|
||||
id == other.id &&
|
||||
name == other.name &&
|
||||
iconFilename == other.iconFilename;
|
||||
}
|
||||
iconFilename == other.iconFilename &&
|
||||
volume == other.volume &&
|
||||
rate == other.rate;
|
||||
}
|
||||
|
||||
bool operator != ( VoiceEngine const & other ) const
|
||||
{ return ! operator == ( other ); }
|
||||
bool operator != ( VoiceEngine const & other ) const
|
||||
{ return ! operator == ( other ); }
|
||||
};
|
||||
|
||||
typedef QVector< VoiceEngine> VoiceEngines;
|
||||
|
|
|
@ -348,7 +348,8 @@ win32 {
|
|||
texttospeechsource.hh \
|
||||
sapi.hh \
|
||||
sphelper.hh \
|
||||
speechclient.hh
|
||||
speechclient.hh \
|
||||
speechhlp.hh
|
||||
}
|
||||
|
||||
mac {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
#define __SPEECHCLIENT_HH_INCLUDED__
|
||||
|
||||
#include <QObject>
|
||||
#include "config.hh"
|
||||
|
||||
class SpeechClient: public QObject
|
||||
{
|
||||
|
@ -13,18 +14,28 @@ public:
|
|||
{
|
||||
QString id;
|
||||
QString name;
|
||||
// Volume and rate may vary from 0 to 100
|
||||
int volume;
|
||||
int rate;
|
||||
Engine( Config::VoiceEngine const & e ) :
|
||||
id( e.id )
|
||||
, name( e.name )
|
||||
, volume( e.volume )
|
||||
, rate( e.rate )
|
||||
{}
|
||||
};
|
||||
|
||||
typedef QList<Engine> Engines;
|
||||
|
||||
SpeechClient( QString const & engineId, QObject * parent = 0L );
|
||||
SpeechClient( Config::VoiceEngine const & e, QObject * parent = 0L );
|
||||
virtual ~SpeechClient();
|
||||
|
||||
static Engines availableEngines();
|
||||
|
||||
const Engine & engine() const;
|
||||
bool tell( QString const & text );
|
||||
bool say( QString const & text );
|
||||
|
||||
bool tell( QString const & text, int volume = -1, int rate = -1 );
|
||||
bool say( QString const & text, int volume = -1, int rate = -1 );
|
||||
|
||||
signals:
|
||||
void started( bool ok );
|
||||
|
|
|
@ -7,10 +7,13 @@
|
|||
|
||||
struct SpeechClient::InternalData
|
||||
{
|
||||
InternalData( QString const & engineId ):
|
||||
InternalData( Config::VoiceEngine const & e ):
|
||||
waitingFinish( false )
|
||||
, engine( e )
|
||||
, oldVolume( -1 )
|
||||
, oldRate( -1 )
|
||||
{
|
||||
sp = speechCreate( engineId.toStdWString().c_str() );
|
||||
sp = speechCreate( e.id.toStdWString().c_str() );
|
||||
}
|
||||
|
||||
~InternalData()
|
||||
|
@ -19,13 +22,15 @@ struct SpeechClient::InternalData
|
|||
}
|
||||
|
||||
SpeechHelper sp;
|
||||
Engine engine;
|
||||
bool waitingFinish;
|
||||
Engine engine;
|
||||
int oldVolume;
|
||||
int oldRate;
|
||||
};
|
||||
|
||||
SpeechClient::SpeechClient( QString const & engineId, QObject * parent ):
|
||||
SpeechClient::SpeechClient( Config::VoiceEngine const & e, QObject * parent ):
|
||||
QObject( parent ),
|
||||
internalData( new InternalData( engineId ) )
|
||||
internalData( new InternalData( e ) )
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -40,11 +45,10 @@ static bool enumEngines( void * /* token */,
|
|||
void * userData )
|
||||
{
|
||||
SpeechClient::Engines * pEngines = ( SpeechClient::Engines * )userData;
|
||||
SpeechClient::Engine engine =
|
||||
{
|
||||
SpeechClient::Engine engine( Config::VoiceEngine(
|
||||
QString::fromWCharArray( id ),
|
||||
QString::fromWCharArray( name )
|
||||
};
|
||||
QString::fromWCharArray( name ),
|
||||
50, 50 ) );
|
||||
pEngines->push_back( engine );
|
||||
return true;
|
||||
}
|
||||
|
@ -61,7 +65,7 @@ const SpeechClient::Engine & SpeechClient::engine() const
|
|||
return internalData->engine;
|
||||
}
|
||||
|
||||
bool SpeechClient::tell( QString const & text )
|
||||
bool SpeechClient::tell( QString const & text, int volume, int rate )
|
||||
{
|
||||
if ( !speechAvailable( internalData->sp ) )
|
||||
return false;
|
||||
|
@ -69,7 +73,16 @@ bool SpeechClient::tell( QString const & text )
|
|||
if ( internalData->waitingFinish )
|
||||
return false;
|
||||
|
||||
if( volume < 0 )
|
||||
volume = engine().volume;
|
||||
if( rate < 0 )
|
||||
rate = engine().rate;
|
||||
|
||||
internalData->oldVolume = setSpeechVolume( internalData->sp, volume );
|
||||
internalData->oldRate = setSpeechRate( internalData->sp, rate );
|
||||
|
||||
bool ok = speechTell( internalData->sp, text.toStdWString().c_str() );
|
||||
|
||||
emit started( ok );
|
||||
|
||||
if ( ok )
|
||||
|
@ -84,12 +97,27 @@ bool SpeechClient::tell( QString const & text )
|
|||
return ok;
|
||||
}
|
||||
|
||||
bool SpeechClient::say( QString const & text )
|
||||
bool SpeechClient::say( QString const & text, int volume, int rate )
|
||||
{
|
||||
if ( !speechAvailable( internalData->sp ) )
|
||||
return false;
|
||||
|
||||
return speechSay( internalData->sp, text.toStdWString().c_str() );
|
||||
if( volume < 0 )
|
||||
volume = engine().volume;
|
||||
if( rate < 0 )
|
||||
rate = engine().rate;
|
||||
|
||||
int oldVolume = setSpeechVolume( internalData->sp, volume );
|
||||
int oldRate = setSpeechRate( internalData->sp, rate );
|
||||
|
||||
bool ok = speechSay( internalData->sp, text.toStdWString().c_str() );
|
||||
|
||||
if( oldVolume >=0 )
|
||||
setSpeechVolume( internalData->sp, oldVolume );
|
||||
if( oldRate >=0 )
|
||||
setSpeechRate( internalData->sp, oldRate );
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
void SpeechClient::timerEvent( QTimerEvent * evt )
|
||||
|
@ -103,6 +131,14 @@ void SpeechClient::timerEvent( QTimerEvent * evt )
|
|||
{
|
||||
killTimer( evt->timerId() ) ;
|
||||
internalData->waitingFinish = false;
|
||||
|
||||
if( internalData->oldVolume >=0 )
|
||||
setSpeechVolume( internalData->sp, internalData->oldVolume );
|
||||
if( internalData->oldRate >=0 )
|
||||
setSpeechRate( internalData->sp, internalData->oldRate );
|
||||
internalData->oldVolume = -1;
|
||||
internalData->oldRate = -1;
|
||||
|
||||
emit finished();
|
||||
}
|
||||
}
|
||||
|
|
24
speechhlp.cc
24
speechhlp.cc
|
@ -165,3 +165,27 @@ bool speechSay(SpeechHelper sp, const wchar_t *text)
|
|||
HRESULT hr = sp->voice->Speak(text, SPF_IS_NOT_XML, 0);
|
||||
return !!SUCCEEDED(hr);
|
||||
}
|
||||
|
||||
int setSpeechVolume( SpeechHelper sp, int newVolume )
|
||||
{
|
||||
if( !sp || !sp->voice || newVolume < 0 || newVolume > 100 )
|
||||
return -1;
|
||||
unsigned short oldVolume;
|
||||
HRESULT hr = sp->voice->GetVolume( &oldVolume );
|
||||
if( !SUCCEEDED( hr ) )
|
||||
return -1;
|
||||
sp->voice->SetVolume( (unsigned short) newVolume );
|
||||
return oldVolume;
|
||||
}
|
||||
|
||||
int setSpeechRate( SpeechHelper sp, int newRate )
|
||||
{
|
||||
if( !sp || !sp->voice || newRate < 0 || newRate > 100 )
|
||||
return -1;
|
||||
long oldRate;
|
||||
HRESULT hr = sp->voice->GetRate( &oldRate );
|
||||
if( !SUCCEEDED( hr ) )
|
||||
return -1;
|
||||
sp->voice->SetRate( ( newRate - 50 ) / 5 );
|
||||
return oldRate * 5 + 50;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ const wchar_t * speechEngineName(SpeechHelper sp);
|
|||
bool speechTell(SpeechHelper sp, const wchar_t *text);
|
||||
bool speechTellFinished(SpeechHelper sp);
|
||||
bool speechSay(SpeechHelper sp, const wchar_t *text);
|
||||
int setSpeechVolume( SpeechHelper sp, int newVolume );
|
||||
int setSpeechRate( SpeechHelper sp, int newRate );
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
|
|
@ -17,6 +17,11 @@
|
|||
#define SPERR_NOT_FOUND MAKE_SAPI_ERROR(0x03a)
|
||||
#endif
|
||||
|
||||
#ifdef _SAPI_VER
|
||||
#undef _SAPI_VER
|
||||
#endif
|
||||
#define _SAPI_VER 0503
|
||||
|
||||
inline void SpHexFromUlong(WCHAR * psz, ULONG ul)
|
||||
{
|
||||
// If for some reason we cannot convert a number, set it to 0
|
||||
|
|
|
@ -25,6 +25,30 @@ TextToSpeechSource::TextToSpeechSource( QWidget * parent,
|
|||
{
|
||||
ui.availableVoiceEngines->addItem( engine.name, engine.id );
|
||||
}
|
||||
|
||||
if( voiceEngines.count() > 0 )
|
||||
{
|
||||
QModelIndex const &idx = ui.selectedVoiceEngines->model()->index( 0, 0 );
|
||||
if( idx.isValid() )
|
||||
ui.selectedVoiceEngines->setCurrentIndex( idx );
|
||||
}
|
||||
|
||||
adjustSliders();
|
||||
|
||||
connect( ui.volumeSlider, SIGNAL( valueChanged( int ) ),
|
||||
this, SLOT( slidersChanged() ) );
|
||||
connect( ui.rateSlider, SIGNAL( valueChanged( int ) ),
|
||||
this, SLOT( slidersChanged() ) );
|
||||
connect( ui.selectedVoiceEngines->selectionModel(), SIGNAL( selectionChanged( QItemSelection, QItemSelection ) ),
|
||||
this, SLOT( selectionChanged() ) );
|
||||
}
|
||||
|
||||
void TextToSpeechSource::slidersChanged()
|
||||
{
|
||||
if( ui.selectedVoiceEngines->selectionModel()->hasSelection() )
|
||||
voiceEnginesModel.setEngineParams( ui.selectedVoiceEngines->currentIndex(),
|
||||
ui.volumeSlider->value(),
|
||||
ui.rateSlider->value()) ;
|
||||
}
|
||||
|
||||
void TextToSpeechSource::on_addVoiceEngine_clicked()
|
||||
|
@ -37,11 +61,14 @@ void TextToSpeechSource::on_addVoiceEngine_clicked()
|
|||
return;
|
||||
}
|
||||
|
||||
// Fake id and name
|
||||
QString name = ui.availableVoiceEngines->itemText( 0 );
|
||||
QString id = ui.availableVoiceEngines->itemData( 0 ).toString();
|
||||
voiceEnginesModel.addNewVoiceEngine( id, name );
|
||||
fitSelectedVoiceEnginesColumns();
|
||||
int idx = ui.availableVoiceEngines->currentIndex();
|
||||
if( idx >= 0 )
|
||||
{
|
||||
QString name = ui.availableVoiceEngines->itemText( idx );
|
||||
QString id = ui.availableVoiceEngines->itemData( idx ).toString();
|
||||
voiceEnginesModel.addNewVoiceEngine( id, name, ui.volumeSlider->value(), ui.rateSlider->value() );
|
||||
fitSelectedVoiceEnginesColumns();
|
||||
}
|
||||
}
|
||||
|
||||
void TextToSpeechSource::on_removeVoiceEngine_clicked()
|
||||
|
@ -66,8 +93,12 @@ void TextToSpeechSource::on_previewVoice_clicked()
|
|||
return;
|
||||
|
||||
QString engineId = ui.availableVoiceEngines->itemData( idx ).toString();
|
||||
QString name = ui.availableVoiceEngines->itemText( idx );
|
||||
QString text = ui.previewText->text();
|
||||
SpeechClient * speechClient = new SpeechClient( engineId, this );
|
||||
int volume = ui.volumeSlider->value();
|
||||
int rate = ui.rateSlider->value();
|
||||
|
||||
SpeechClient * speechClient = new SpeechClient( Config::VoiceEngine( engineId, name, volume, rate ), this );
|
||||
|
||||
connect( speechClient, SIGNAL( started( bool ) ), ui.previewVoice, SLOT( setDisabled( bool ) ) );
|
||||
connect( speechClient, SIGNAL( finished() ), this, SLOT( previewVoiceFinished() ) );
|
||||
|
@ -87,6 +118,35 @@ void TextToSpeechSource::fitSelectedVoiceEnginesColumns()
|
|||
ui.selectedVoiceEngines->resizeColumnToContents( VoiceEnginesModel::kColumnIcon );
|
||||
}
|
||||
|
||||
void TextToSpeechSource::adjustSliders()
|
||||
{
|
||||
QModelIndex const & index = ui.selectedVoiceEngines->currentIndex();
|
||||
if ( index.isValid() )
|
||||
{
|
||||
Config::VoiceEngines const &engines = voiceEnginesModel.getCurrentVoiceEngines();
|
||||
ui.volumeSlider->setValue( engines[ index.row() ].volume );
|
||||
ui.rateSlider->setValue( engines[ index.row() ].rate );
|
||||
return;
|
||||
}
|
||||
ui.volumeSlider->setValue( 50 );
|
||||
ui.rateSlider->setValue( 50 );
|
||||
}
|
||||
|
||||
void TextToSpeechSource::selectionChanged()
|
||||
{
|
||||
disconnect( ui.volumeSlider, SIGNAL( valueChanged( int ) ),
|
||||
this, SLOT( slidersChanged() ) );
|
||||
disconnect( ui.rateSlider, SIGNAL( valueChanged( int ) ),
|
||||
this, SLOT( slidersChanged() ) );
|
||||
|
||||
adjustSliders();
|
||||
|
||||
connect( ui.volumeSlider, SIGNAL( valueChanged( int ) ),
|
||||
this, SLOT( slidersChanged() ) );
|
||||
connect( ui.rateSlider, SIGNAL( valueChanged( int ) ),
|
||||
this, SLOT( slidersChanged() ) );
|
||||
}
|
||||
|
||||
VoiceEnginesModel::VoiceEnginesModel( QWidget * parent,
|
||||
Config::VoiceEngines const & voiceEngines ):
|
||||
QAbstractItemModel( parent ), voiceEngines( voiceEngines )
|
||||
|
@ -100,7 +160,8 @@ void VoiceEnginesModel::removeVoiceEngine( int index )
|
|||
endRemoveRows();
|
||||
}
|
||||
|
||||
void VoiceEnginesModel::addNewVoiceEngine( QString const & id, QString const & name )
|
||||
void VoiceEnginesModel::addNewVoiceEngine( QString const & id, QString const & name,
|
||||
int volume, int rate )
|
||||
{
|
||||
if ( id.isEmpty() || name.isEmpty() )
|
||||
return;
|
||||
|
@ -109,6 +170,8 @@ void VoiceEnginesModel::addNewVoiceEngine( QString const & id, QString const & n
|
|||
v.enabled = true;
|
||||
v.id = id;
|
||||
v.name = name;
|
||||
v.volume = volume;
|
||||
v.rate = rate;
|
||||
|
||||
beginInsertRows( QModelIndex(), voiceEngines.size(), voiceEngines.size() );
|
||||
voiceEngines.push_back( v );
|
||||
|
@ -243,6 +306,15 @@ bool VoiceEnginesModel::setData( QModelIndex const & index, const QVariant & val
|
|||
return false;
|
||||
}
|
||||
|
||||
void VoiceEnginesModel::setEngineParams( QModelIndex idx, int volume, int rate )
|
||||
{
|
||||
if ( idx.isValid() )
|
||||
{
|
||||
voiceEngines[ idx.row() ].volume = volume;
|
||||
voiceEngines[ idx.row() ].rate = rate;
|
||||
}
|
||||
}
|
||||
|
||||
VoiceEngineEditor::VoiceEngineEditor( SpeechClient::Engines const & engines, QWidget * parent ):
|
||||
QComboBox( parent )
|
||||
{
|
||||
|
|
|
@ -29,10 +29,12 @@ public:
|
|||
VoiceEnginesModel( QWidget * parent, Config::VoiceEngines const & voiceEngines );
|
||||
|
||||
void removeVoiceEngine( int index );
|
||||
void addNewVoiceEngine( QString const & id, QString const & name );
|
||||
void addNewVoiceEngine( QString const & id, QString const & name,
|
||||
int volume, int rate );
|
||||
|
||||
Config::VoiceEngines const & getCurrentVoiceEngines() const
|
||||
{ return voiceEngines; }
|
||||
void setEngineParams( QModelIndex idx, int volume, int rate );
|
||||
|
||||
QModelIndex index( int row, int column, QModelIndex const & parent ) const;
|
||||
QModelIndex parent( QModelIndex const & parent ) const;
|
||||
|
@ -94,12 +96,15 @@ private slots:
|
|||
void on_removeVoiceEngine_clicked();
|
||||
void on_previewVoice_clicked();
|
||||
void previewVoiceFinished();
|
||||
void slidersChanged();
|
||||
void selectionChanged();
|
||||
|
||||
private:
|
||||
Ui::TextToSpeechSource ui;
|
||||
VoiceEnginesModel voiceEnginesModel;
|
||||
|
||||
void fitSelectedVoiceEnginesColumns();
|
||||
void adjustSliders();
|
||||
};
|
||||
|
||||
#endif // __TEXTTOSPEECHSOURCE_HH_INCLUDED__
|
||||
|
|
|
@ -78,6 +78,73 @@
|
|||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Preferences</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>Volume:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSlider" name="volumeSlider">
|
||||
<property name="maximum">
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="sliderPosition">
|
||||
<number>50</number>
|
||||
</property>
|
||||
<property name="tracking">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="tickPosition">
|
||||
<enum>QSlider::TicksAbove</enum>
|
||||
</property>
|
||||
<property name="tickInterval">
|
||||
<number>10</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Rate:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSlider" name="rateSlider">
|
||||
<property name="maximum">
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="sliderPosition">
|
||||
<number>50</number>
|
||||
</property>
|
||||
<property name="tracking">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="tickPosition">
|
||||
<enum>QSlider::TicksAbove</enum>
|
||||
</property>
|
||||
<property name="tickInterval">
|
||||
<number>10</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="title">
|
||||
|
|
Loading…
Reference in a new issue