1 /****************************************************************************************
2  * Copyright (c) 2004 Mark Kretschmann <kretschmann@kde.org>                            *
3  * Copyright (c) 2004 Stefan Bogner <bochi@online.ms>                                   *
4  * Copyright (c) 2004 Max Howell <max.howell@methylblue.com>                            *
5  * Copyright (c) 2007 Dan Meltzer <parallelgrapefruit@gmail.com>                        *
6  * Copyright (c) 2009 Martin Sandsmark <sandsmark@samfundet.no>                         *
7  *                                                                                      *
8  * This program is free software; you can redistribute it and/or modify it under        *
9  * the terms of the GNU General Public License as published by the Free Software        *
10  * Foundation; either version 2 of the License, or (at your option) any later           *
11  * version.                                                                             *
12  *                                                                                      *
13  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
14  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
15  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
16  *                                                                                      *
17  * You should have received a copy of the GNU General Public License along with         *
18  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
19  ****************************************************************************************/
20 
21 #define DEBUG_PREFIX "CoverFoundDialog"
22 
23 #include "CoverFoundDialog.h"
24 
25 #include "SvgHandler.h"
26 #include "core/support/Amarok.h"
27 #include "core/support/Debug.h"
28 #include "covermanager/CoverViewDialog.h"
29 #include "statusbar/KJobProgressBar.h"
30 #include "widgets/AlbumBreadcrumbWidget.h"
31 #include "widgets/PixmapViewer.h"
32 
33 #include <QCloseEvent>
34 #include <QDir>
35 #include <QFileDialog>
36 #include <QFontDatabase>
37 #include <QFormLayout>
38 #include <QGridLayout>
39 #include <QHeaderView>
40 #include <QLineEdit>
41 #include <QListWidget>
42 #include <QMenu>
43 #include <QMimeDatabase>
44 #include <QMimeType>
45 #include <QPushButton>
46 #include <QScrollArea>
47 #include <QSplitter>
48 #include <QStandardPaths>
49 #include <QTabWidget>
50 
51 #include <KComboBox>
52 #include <KConfigGroup>
53 #include <KLineEdit>
54 #include <KLocalizedString>
55 #include <KMessageBox>
56 #include <KWindowConfig>
57 
CoverFoundDialog(const CoverFetchUnit::Ptr & unit,const CoverFetch::Metadata & data,QWidget * parent)58 CoverFoundDialog::CoverFoundDialog( const CoverFetchUnit::Ptr &unit,
59                                     const CoverFetch::Metadata &data,
60                                     QWidget *parent )
61     : QDialog( parent )
62     , m_album( unit->album() )
63     , m_isSorted( false )
64     , m_sortEnabled( false )
65     , m_unit( unit )
66     , m_queryPage( 0 )
67 {
68     DEBUG_BLOCK
69 
70     setLayout( new QVBoxLayout );
71 
72     QSplitter *splitter = new QSplitter( this );
73     layout()->addWidget( splitter );
74 
75     QDialogButtonBox *buttonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this );
76     QPushButton *clearButton = buttonBox->addButton( QStringLiteral( "clear" ), QDialogButtonBox::ActionRole ); // clear icon view
77     layout()->addWidget( buttonBox );
78 
79     connect( clearButton, &QAbstractButton::clicked, this, &CoverFoundDialog::clearView );
80     connect( buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept );
81     connect( buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject );
82 
83     m_sideBar = new CoverFoundSideBar( m_album, splitter );
84 
85     BoxWidget *vbox = new BoxWidget( true, splitter );
86     vbox->layout()->setSpacing( 4 );
87 
88     BoxWidget *breadcrumbBox = new BoxWidget( false, vbox );
89     QLabel *breadcrumbLabel = new QLabel( i18n( "Finding cover for" ), breadcrumbBox );
90     AlbumBreadcrumbWidget *breadcrumb = new AlbumBreadcrumbWidget( m_album, breadcrumbBox );
91 
92     QFont breadcrumbLabelFont;
93     breadcrumbLabelFont.setBold( true );
94     breadcrumbLabel->setFont( breadcrumbLabelFont );
95     breadcrumbLabel->setIndent( 4 );
96 
97     connect( breadcrumb, &AlbumBreadcrumbWidget::artistClicked, this, &CoverFoundDialog::addToCustomSearch );
98     connect( breadcrumb, &AlbumBreadcrumbWidget::albumClicked, this, &CoverFoundDialog::addToCustomSearch );
99 
100     BoxWidget *searchBox = new BoxWidget( false, vbox );
101 
102     QStringList completionNames;
103     QString firstRunQuery( m_album->name() );
104     completionNames << firstRunQuery;
105     if( m_album->hasAlbumArtist() )
106     {
107         const QString &name = m_album->albumArtist()->name();
108         completionNames << name;
109         firstRunQuery += ' ' + name;
110     }
111     m_query = firstRunQuery;
112     m_album->setSuppressImageAutoFetch( true );
113 
114     m_search = new KComboBox( searchBox );
115     m_search->setEditable( true ); // creates a QLineEdit for the combobox
116     m_search->setTrapReturnKey( true );
117     m_search->setInsertPolicy( KComboBox::NoInsert ); // insertion is handled by us
118     m_search->setCompletionMode( KCompletion::CompletionPopup );
119     m_search->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Fixed );
120     m_search->lineEdit()->setPlaceholderText( i18n( "Enter Custom Search" ) );
121     m_search->completionObject()->setOrder( KCompletion::Insertion );
122     m_search->completionObject()->setIgnoreCase( true );
123     m_search->completionObject()->setItems( completionNames );
124     m_search->insertItem( 0, KStandardGuiItem::find().icon(), QString() );
125     m_search->insertSeparator( 1 );
126     m_search->insertItem( 2, QIcon::fromTheme("filename-album-amarok"), m_album->name() );
127     if( m_album->hasAlbumArtist() )
128         m_search->insertItem( 3, QIcon::fromTheme("filename-artist-amarok"), m_album->albumArtist()->name() );
129 
130     auto findItem = KStandardGuiItem::find();
131     m_searchButton = new QPushButton( findItem.icon(), findItem.text(), searchBox );
132     auto configureItem = KStandardGuiItem::configure();
133     QPushButton *sourceButton = new QPushButton( configureItem.icon(), configureItem.text(), searchBox );
134     updateSearchButton( firstRunQuery );
135 
136     QMenu *sourceMenu = new QMenu( sourceButton );
137     QAction *lastFmAct = new QAction( i18n( "Last.fm" ), sourceMenu );
138     QAction *googleAct = new QAction( i18n( "Google" ), sourceMenu );
139     QAction *discogsAct = new QAction( i18n( "Discogs" ), sourceMenu );
140     // TODO: currently broken, re-enable after adjusting to current API/returned doc
141     googleAct->setEnabled( false );
142     // TODO: currently broken, re-enable after adjusting to current API/returned doc
143     discogsAct->setEnabled( false );
144     lastFmAct->setCheckable( true );
145     googleAct->setCheckable( true );
146     discogsAct->setCheckable( true );
147     connect( lastFmAct, &QAction::triggered, this, &CoverFoundDialog::selectLastFm );
148     connect( googleAct, &QAction::triggered, this, &CoverFoundDialog::selectGoogle );
149     connect( discogsAct, &QAction::triggered, this, &CoverFoundDialog::selectDiscogs );
150 
151     m_sortAction = new QAction( i18n( "Sort by size" ), sourceMenu );
152     m_sortAction->setCheckable( true );
153     connect( m_sortAction, &QAction::triggered, this, &CoverFoundDialog::sortingTriggered );
154 
155     QActionGroup *ag = new QActionGroup( sourceButton );
156     ag->addAction( lastFmAct );
157     ag->addAction( googleAct );
158     ag->addAction( discogsAct );
159     sourceMenu->addActions( ag->actions() );
160     sourceMenu->addSeparator();
161     sourceMenu->addAction( m_sortAction );
162     sourceButton->setMenu( sourceMenu );
163 
164     connect( m_search, QOverload<const QString&>::of(&KComboBox::returnPressed),
165              this, &CoverFoundDialog::insertComboText );
166     connect( m_search, QOverload<const QString&>::of(&KComboBox::returnPressed),
167              this, &CoverFoundDialog::processQuery );
168     connect( m_search, QOverload<const QString&>::of(&KComboBox::returnPressed),
169              this, &CoverFoundDialog::updateSearchButton );
170     connect( m_search, &KComboBox::editTextChanged, this, &CoverFoundDialog::updateSearchButton );
171 
172     sourceMenu->addAction( m_sortAction );
173     sourceButton->setMenu( sourceMenu );
174 
175     connect( m_search, QOverload<const QString&>::of(&KComboBox::returnPressed),
176              this, &CoverFoundDialog::insertComboText );
177     connect( m_search, QOverload<const QString&>::of(&KComboBox::returnPressed),
178              this, &CoverFoundDialog::processQuery );
179     connect( m_search, QOverload<const QString&>::of(&KComboBox::returnPressed),
180              this, &CoverFoundDialog::updateSearchButton );
181     connect( m_search, &KComboBox::editTextChanged, this, &CoverFoundDialog::updateSearchButton );
182     connect( dynamic_cast<KLineEdit*>(m_search->lineEdit()), &KLineEdit::clearButtonClicked,
183              this, &CoverFoundDialog::clearQueryButtonClicked);
184     connect( m_searchButton, &QPushButton::clicked, this, &CoverFoundDialog::processCurrentQuery );
185 
186     m_view = new QListWidget( vbox );
187     m_view->setAcceptDrops( false );
188     m_view->setContextMenuPolicy( Qt::CustomContextMenu );
189     m_view->setDragDropMode( QAbstractItemView::NoDragDrop );
190     m_view->setDragEnabled( false );
191     m_view->setDropIndicatorShown( false );
192     m_view->setMovement( QListView::Static );
193     m_view->setGridSize( QSize( 140, 150 ) );
194     m_view->setIconSize( QSize( 120, 120 ) );
195     m_view->setSpacing( 4 );
196     m_view->setViewMode( QListView::IconMode );
197     m_view->setResizeMode( QListView::Adjust );
198 
199     connect( m_view, &QListWidget::currentItemChanged,
200              this, &CoverFoundDialog::currentItemChanged );
201     connect( m_view, &QListWidget::itemDoubleClicked,
202              this, &CoverFoundDialog::itemDoubleClicked );
203     connect( m_view, &QListWidget::customContextMenuRequested,
204              this, &CoverFoundDialog::itemMenuRequested );
205 
206     splitter->addWidget( m_sideBar );
207     splitter->addWidget( vbox );
208 //     setMainWidget( splitter );
209 
210     const KConfigGroup config = Amarok::config( "Cover Fetcher" );
211     const QString source = config.readEntry( "Interactive Image Source", "LastFm" );
212     m_sortEnabled = config.readEntry( "Sort by Size", false );
213     m_sortAction->setChecked( m_sortEnabled );
214     m_isSorted = m_sortEnabled;
215     KWindowConfig::restoreWindowSize( windowHandle(), config ); // call this after setMainWidget()
216 
217     if( source == "LastFm" )
218         lastFmAct->setChecked( true );
219     else if( source == "Discogs" )
220         discogsAct->setChecked( true );
221     else
222         googleAct->setChecked( true );
223 
224     typedef CoverFetchArtPayload CFAP;
225     const CFAP *payload = dynamic_cast< const CFAP* >( unit->payload() );
226     if( !m_album->hasImage() )
227         m_sideBar->setPixmap( QPixmap::fromImage( m_album->image(190 ) ) );
228     else if( payload )
229         add( m_album->image(), data, payload->imageSize() );
230     else
231         add( m_album->image(), data );
232     m_view->setCurrentItem( m_view->item( 0 ) );
233     updateGui();
234 
235     connect( The::networkAccessManager(), &NetworkAccessManagerProxy::requestRedirectedReply,
236              this, &CoverFoundDialog::fetchRequestRedirected );
237 }
238 
~CoverFoundDialog()239 CoverFoundDialog::~CoverFoundDialog()
240 {
241     m_album->setSuppressImageAutoFetch( false );
242 
243     const QList<QListWidgetItem*> &viewItems = m_view->findItems( QChar('*'), Qt::MatchWildcard );
244     qDeleteAll( viewItems );
245     delete m_dialog.data();
246 }
247 
hideEvent(QHideEvent * event)248 void CoverFoundDialog::hideEvent( QHideEvent *event )
249 {
250     KConfigGroup config = Amarok::config( "Cover Fetcher" );
251     config.writeEntry( "geometry", saveGeometry() );
252     event->accept();
253 }
254 
add(const QImage & cover,const CoverFetch::Metadata & metadata,const CoverFetch::ImageSize imageSize)255 void CoverFoundDialog::add( const QImage &cover,
256                             const CoverFetch::Metadata &metadata,
257                             const CoverFetch::ImageSize imageSize )
258 {
259     if( cover.isNull() )
260         return;
261 
262     if( !contains( metadata ) )
263     {
264         CoverFoundItem *item = new CoverFoundItem( cover, metadata, imageSize );
265         addToView( item );
266     }
267 }
268 
addToView(CoverFoundItem * item)269 void CoverFoundDialog::addToView( CoverFoundItem *item )
270 {
271     const CoverFetch::Metadata &metadata = item->metadata();
272 
273     if( m_sortEnabled && metadata.contains( "width" ) && metadata.contains( "height" ) )
274     {
275         if( m_isSorted )
276         {
277             const int size = metadata.value( "width" ).toInt() * metadata.value( "height" ).toInt();
278             QList< int >::iterator i = qLowerBound( m_sortSizes.begin(), m_sortSizes.end(), size );
279             m_sortSizes.insert( i, size );
280             const int index = m_sortSizes.count() - m_sortSizes.indexOf( size ) - 1;
281             m_view->insertItem( index, item );
282         }
283         else
284         {
285             m_view->addItem( item );
286             sortCoversBySize();
287         }
288     }
289     else
290     {
291         m_view->addItem( item );
292     }
293     updateGui();
294 }
295 
contains(const CoverFetch::Metadata & metadata) const296 bool CoverFoundDialog::contains( const CoverFetch::Metadata &metadata ) const
297 {
298     for( int i = 0, count = m_view->count(); i < count; ++i )
299     {
300         CoverFoundItem *item = static_cast<CoverFoundItem*>( m_view->item(i) );
301         if( item->metadata() == metadata )
302             return true;
303     }
304     return false;
305 }
306 
addToCustomSearch(const QString & text)307 void CoverFoundDialog::addToCustomSearch( const QString &text )
308 {
309     const QString &query = m_search->currentText();
310     if( !text.isEmpty() && !query.contains( text ) )
311     {
312         QStringList q;
313         if( !query.isEmpty() )
314             q << query;
315         q << text;
316         const QString result = q.join( QLatin1Char( ' ' ) );
317         qobject_cast<QLineEdit*>( m_search->lineEdit() )->setText( result );
318     }
319 }
320 
clearQueryButtonClicked()321 void CoverFoundDialog::clearQueryButtonClicked()
322 {
323     m_query.clear();
324     m_queryPage = 0;
325     updateGui();
326 }
327 
clearView()328 void CoverFoundDialog::clearView()
329 {
330     m_view->clear();
331     m_sideBar->clear();
332     m_sortSizes.clear();
333     updateGui();
334 }
335 
insertComboText(const QString & text)336 void CoverFoundDialog::insertComboText( const QString &text )
337 {
338     if( text.isEmpty() )
339         return;
340 
341     if( m_search->contains( text ) )
342     {
343         m_search->setCurrentIndex( m_search->findText( text ) );
344         return;
345     }
346     m_search->completionObject()->addItem( text );
347     m_search->insertItem( 0, KStandardGuiItem::find().icon(), text );
348     m_search->setCurrentIndex( 0 );
349 }
350 
currentItemChanged(QListWidgetItem * current,QListWidgetItem * previous)351 void CoverFoundDialog::currentItemChanged( QListWidgetItem *current, QListWidgetItem *previous )
352 {
353     Q_UNUSED( previous )
354     if( !current )
355         return;
356     CoverFoundItem *it = static_cast< CoverFoundItem* >( current );
357     QImage image = it->hasBigPix() ? it->bigPix() : it->thumb();
358     m_image = image;
359     m_sideBar->setPixmap( QPixmap::fromImage(image), it->metadata() );
360 }
361 
itemDoubleClicked(QListWidgetItem * item)362 void CoverFoundDialog::itemDoubleClicked( QListWidgetItem *item )
363 {
364     Q_UNUSED( item )
365     slotButtonClicked( QDialog::Accepted );
366 }
367 
itemMenuRequested(const QPoint & pos)368 void CoverFoundDialog::itemMenuRequested( const QPoint &pos )
369 {
370     const QPoint globalPos = m_view->mapToGlobal( pos );
371     QModelIndex index = m_view->indexAt( pos );
372 
373     if( !index.isValid() )
374         return;
375 
376     CoverFoundItem *item = static_cast< CoverFoundItem* >( m_view->item( index.row() ) );
377     item->setSelected( true );
378 
379     QMenu menu( this );
380     QAction *display = new QAction( QIcon::fromTheme(QStringLiteral("zoom-original")), i18n("Display Cover"), &menu );
381     connect( display, &QAction::triggered, this, &CoverFoundDialog::display );
382 
383     QAction *save = new QAction( QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save As"), &menu );
384     connect( save, &QAction::triggered, this, &CoverFoundDialog::saveAs );
385 
386     menu.addAction( display );
387     menu.addAction( save );
388     menu.exec( globalPos );
389 }
390 
saveAs()391 void CoverFoundDialog::saveAs()
392 {
393     CoverFoundItem *item = static_cast< CoverFoundItem* >( m_view->currentItem() );
394     if( !item->hasBigPix() && !fetchBigPix() )
395         return;
396 
397     Meta::TrackList tracks = m_album->tracks();
398     if( tracks.isEmpty() )
399     {
400         warning() << "no tracks associated with album" << m_album->name();
401         return;
402     }
403 
404     QFileDialog dlg;
405     QWidget::setWindowTitle( i18n("Cover Image Save Location") );
406     dlg.setFileMode( QFileDialog::AnyFile );
407     dlg.setSupportedSchemes( QStringList( QStringLiteral( "file" ) ) );
408     dlg.setAcceptMode( QFileDialog::AcceptSave );
409 
410     QUrl selectedUrl;
411     selectedUrl.setPath( "cover.jpg" );
412     dlg.selectUrl( selectedUrl );
413 
414     QStringList supportedMimeTypes;
415     supportedMimeTypes << "image/jpeg";
416     supportedMimeTypes << "image/png";
417     dlg.setMimeTypeFilters( supportedMimeTypes );
418 
419     QUrl saveUrl;
420     int res = dlg.exec();
421     switch( res )
422     {
423     case QDialog::Accepted:
424         saveUrl = dlg.selectedUrls().value( 0 );
425         break;
426     case QDialog::Rejected:
427         return;
428     }
429 
430     QFile saveFile( saveUrl.path() );
431     if( !saveFile.open( QFile::WriteOnly ) )
432     {
433         KMessageBox::detailedError( this,
434                                     i18n("Sorry, the cover could not be saved."),
435                                     saveFile.errorString() );
436         return;
437     }
438 
439     const QImage &image = item->bigPix();
440     QMimeDatabase db;
441     const QString &ext = db.suffixForFileName( saveUrl.path() ).toLower();
442     bool ok;
443     if( ext == "jpg" || ext == "jpeg" )
444         ok = image.save( &saveFile, "JPG" );
445     else if( ext == "png" )
446         ok = image.save( &saveFile, "PNG" );
447     else
448         ok = image.save( &saveFile );
449 
450     if( !ok )
451     {
452         KMessageBox::detailedError( this,
453                                     i18n("Sorry, the cover could not be saved."),
454                                     saveFile.errorString() );
455         saveFile.remove();
456     }
457 }
458 
slotButtonClicked(int button)459 void CoverFoundDialog::slotButtonClicked( int button )
460 {
461     if( button == QDialog::Accepted )
462     {
463         CoverFoundItem *item = dynamic_cast< CoverFoundItem* >( m_view->currentItem() );
464         if( !item )
465         {
466             reject();
467             return;
468         }
469 
470         bool gotBigPix( true );
471         if( !item->hasBigPix() )
472             gotBigPix = fetchBigPix();
473 
474         if( gotBigPix )
475         {
476             m_image = item->bigPix();
477             accept();
478         }
479     }
480 }
481 
fetchRequestRedirected(QNetworkReply * oldReply,QNetworkReply * newReply)482 void CoverFoundDialog::fetchRequestRedirected( QNetworkReply *oldReply,
483                                                QNetworkReply *newReply )
484 {
485     QUrl oldUrl = oldReply->request().url();
486     QUrl newUrl = newReply->request().url();
487 
488     // Since we were redirected we have to check if the redirect
489     // was for one of our URLs and if the new URL is not handled
490     // already.
491     if( m_urls.contains( oldUrl ) && !m_urls.contains( newUrl ) )
492     {
493         // Get the unit for the old URL.
494         CoverFoundItem *item = m_urls.value( oldUrl );
495 
496         // Add the unit with the new URL and remove the old one.
497         m_urls.insert( newUrl, item );
498         m_urls.remove( oldUrl );
499     }
500 }
501 
handleFetchResult(const QUrl & url,const QByteArray & data,const NetworkAccessManagerProxy::Error & e)502 void CoverFoundDialog::handleFetchResult( const QUrl &url, const QByteArray &data,
503                                           const NetworkAccessManagerProxy::Error &e )
504 {
505     CoverFoundItem *item = m_urls.take( url );
506     QImage image;
507     if( item && e.code == QNetworkReply::NoError && image.loadFromData( data ) )
508     {
509         item->setBigPix( image );
510         m_sideBar->setPixmap( QPixmap::fromImage( image ) );
511         if( m_dialog )
512             m_dialog->accept();
513     }
514     else
515     {
516         QStringList errors;
517         errors << e.description;
518         KMessageBox::errorList( this, i18n("Sorry, the cover image could not be retrieved."), errors );
519         if( m_dialog )
520             m_dialog->reject();
521     }
522 }
523 
fetchBigPix()524 bool CoverFoundDialog::fetchBigPix()
525 {
526     DEBUG_BLOCK
527     CoverFoundItem *item = static_cast< CoverFoundItem* >( m_view->currentItem() );
528     const QUrl url( item->metadata().value( "normalarturl" ) );
529     if( !url.isValid() )
530         return false;
531 
532     QNetworkReply *reply = The::networkAccessManager()->getData( url, this, &CoverFoundDialog::handleFetchResult );
533     m_urls.insert( url, item );
534 
535     if( !m_dialog )
536     {
537         m_dialog = new QProgressDialog( this );
538         m_dialog->setWindowTitle( i18n( "Fetching Large Cover" ) );
539         m_dialog->setLabelText( i18n( "Download Progress" ) );
540         m_dialog->setModal( true );
541         m_dialog->setCancelButton( new QPushButton( i18n( "Cancel" ) ) );
542         m_dialog->setAutoClose( false );
543         m_dialog->setAutoReset( true );
544         m_dialog->setMinimumWidth( 300 );
545         connect( reply, &QNetworkReply::downloadProgress,
546                  this, &CoverFoundDialog::downloadProgressed );
547     }
548     int result = m_dialog->exec();
549     bool success = (result == QDialog::Accepted) && !m_dialog->wasCanceled();
550     The::networkAccessManager()->abortGet( url );
551     if( !success )
552         m_urls.remove( url );
553     m_dialog->deleteLater();
554     return success;
555 }
556 
downloadProgressed(qint64 bytesReceived,qint64 bytesTotal)557 void CoverFoundDialog::downloadProgressed( qint64 bytesReceived, qint64 bytesTotal )
558 {
559     if( m_dialog )
560     {
561         m_dialog->setRange( 0, bytesTotal );
562         m_dialog->setValue( bytesReceived );
563     }
564 }
565 
display()566 void CoverFoundDialog::display()
567 {
568     CoverFoundItem *item = static_cast< CoverFoundItem* >( m_view->currentItem() );
569     const bool success = item->hasBigPix() ? true : fetchBigPix();
570     if( !success )
571         return;
572 
573     const QImage &image = item->hasBigPix() ? item->bigPix() : item->thumb();
574     CoverViewDialog *dlg = new CoverViewDialog( image, this );
575     dlg->show();
576     dlg->raise();
577     dlg->activateWindow();
578 }
579 
processCurrentQuery()580 void CoverFoundDialog::processCurrentQuery()
581 {
582     const QString text = m_search->currentText();
583     processQuery( text );
584 }
585 
processQuery(const QString & input)586 void CoverFoundDialog::processQuery( const QString &input )
587 {
588     const bool inputEmpty( input.isEmpty() );
589     const bool mQueryEmpty( m_query.isEmpty() );
590 
591     QString q;
592     if( inputEmpty && !mQueryEmpty )
593     {
594         q = m_query;
595     }
596     else if( !inputEmpty || !mQueryEmpty )
597     {
598         q = input;
599         if( m_query != input )
600         {
601             m_query = input;
602             m_queryPage = 1;
603         }
604     }
605 
606     if( !q.isEmpty() )
607     {
608         Q_EMIT newCustomQuery( m_album, q, m_queryPage );
609         updateSearchButton( q );
610         m_queryPage++;
611     }
612 }
613 
selectDiscogs()614 void CoverFoundDialog::selectDiscogs()
615 {
616     KConfigGroup config = Amarok::config( "Cover Fetcher" );
617     config.writeEntry( "Interactive Image Source", "Discogs" );
618     m_sortAction->setEnabled( true );
619     m_queryPage = 0;
620     processCurrentQuery();
621     debug() << "Select Discogs as source";
622 }
623 
selectLastFm()624 void CoverFoundDialog::selectLastFm()
625 {
626     KConfigGroup config = Amarok::config( "Cover Fetcher" );
627     config.writeEntry( "Interactive Image Source", "LastFm" );
628     m_sortAction->setEnabled( false );
629     m_queryPage = 0;
630     processCurrentQuery();
631     debug() << "Select Last.fm as source";
632 }
633 
selectGoogle()634 void CoverFoundDialog::selectGoogle()
635 {
636     KConfigGroup config = Amarok::config( "Cover Fetcher" );
637     config.writeEntry( "Interactive Image Source", "Google" );
638     m_sortAction->setEnabled( true );
639     m_queryPage = 0;
640     processCurrentQuery();
641     debug() << "Select Google as source";
642 }
643 
setQueryPage(int page)644 void CoverFoundDialog::setQueryPage( int page )
645 {
646     m_queryPage = page;
647 }
648 
sortingTriggered(bool checked)649 void CoverFoundDialog::sortingTriggered( bool checked )
650 {
651     KConfigGroup config = Amarok::config( "Cover Fetcher" );
652     config.writeEntry( "Sort by Size", checked );
653     m_sortEnabled = checked;
654     m_isSorted = false;
655     if( m_sortEnabled )
656         sortCoversBySize();
657     debug() << "Enable sorting by size:" << checked;
658 }
659 
sortCoversBySize()660 void CoverFoundDialog::sortCoversBySize()
661 {
662     DEBUG_BLOCK
663 
664     m_sortSizes.clear();
665     QList< QListWidgetItem* > viewItems = m_view->findItems( QChar('*'), Qt::MatchWildcard );
666     QMultiMap<int, CoverFoundItem*> sortItems;
667 
668     // get a list of cover items sorted (automatically by qmap) by size
669     foreach( QListWidgetItem *viewItem, viewItems  )
670     {
671         CoverFoundItem *coverItem = dynamic_cast<CoverFoundItem*>( viewItem );
672         const CoverFetch::Metadata &meta = coverItem->metadata();
673         const int itemSize = meta.value( "width" ).toInt() * meta.value( "height" ).toInt();
674         sortItems.insert( itemSize, coverItem );
675         m_sortSizes << itemSize;
676     }
677 
678     // take items from the view and insert into a temp list in the sorted order
679     QList<CoverFoundItem*> coverItems = sortItems.values();
680     QList<CoverFoundItem*> tempItems;
681     for( int i = 0, count = sortItems.count(); i < count; ++i )
682     {
683         CoverFoundItem *item = coverItems.value( i );
684         const int itemRow = m_view->row( item );
685         QListWidgetItem *itemFromRow = m_view->takeItem( itemRow );
686         if( itemFromRow )
687             tempItems << dynamic_cast<CoverFoundItem*>( itemFromRow );
688     }
689 
690     // add the items back to the view in descending order
691     foreach( CoverFoundItem* item, tempItems )
692         m_view->insertItem( 0, item );
693 
694     m_isSorted = true;
695 }
696 
updateSearchButton(const QString & text)697 void CoverFoundDialog::updateSearchButton( const QString &text )
698 {
699     const bool isNewSearch = ( text != m_query ) ? true : false;
700     m_searchButton->setText( isNewSearch ? KStandardGuiItem::find().text() : KStandardGuiItem::cont().text() );
701     m_searchButton->setIcon( isNewSearch ? KStandardGuiItem::find().icon() : KStandardGuiItem::cont().icon() );
702     m_searchButton->setToolTip( isNewSearch ? i18n( "Search" ) : i18n( "Search For More Results" ) );
703 }
704 
updateGui()705 void CoverFoundDialog::updateGui()
706 {
707     updateTitle();
708 
709     if( !m_search->hasFocus() )
710         findChild<QDialogButtonBox*>()->button( QDialogButtonBox::Ok )->setFocus();
711     update();
712 }
713 
updateTitle()714 void CoverFoundDialog::updateTitle()
715 {
716     const int itemCount = m_view->count();
717     const QString caption = ( itemCount == 0 )
718                           ? i18n( "No Images Found" )
719                           : i18np( "1 Image Found", "%1 Images Found", itemCount );
720     setWindowTitle( caption );
721 }
722 
CoverFoundSideBar(const Meta::AlbumPtr & album,QWidget * parent)723 CoverFoundSideBar::CoverFoundSideBar( const Meta::AlbumPtr &album, QWidget *parent )
724     : BoxWidget( true, parent )
725     , m_album( album )
726 {
727     m_cover = new QLabel( this );
728     m_tabs  = new QTabWidget( this );
729     m_notes = new QLabel;
730     QScrollArea *metaArea = new QScrollArea;
731     m_metaTable = new QWidget( metaArea );
732     m_metaTable->setLayout( new QFormLayout );
733     m_metaTable->setMinimumSize( QSize( 150, 200 ) );
734     metaArea->setFrameShape( QFrame::NoFrame );
735     metaArea->setWidget( m_metaTable );
736     m_notes->setAlignment( Qt::AlignLeft | Qt::AlignTop );
737     m_notes->setMargin( 4 );
738     m_notes->setOpenExternalLinks( true );
739     m_notes->setTextFormat( Qt::RichText );
740     m_notes->setTextInteractionFlags( Qt::TextBrowserInteraction );
741     m_notes->setWordWrap( true );
742     m_cover->setAlignment( Qt::AlignCenter );
743     m_tabs->addTab( metaArea, i18n( "Information" ) );
744     m_tabs->addTab( m_notes, i18n( "Notes" ) );
745     setMaximumWidth( 200 );
746     setPixmap( QPixmap::fromImage( m_album->image( 190 ) ) );
747     clear();
748 }
749 
~CoverFoundSideBar()750 CoverFoundSideBar::~CoverFoundSideBar()
751 {
752 }
753 
clear()754 void CoverFoundSideBar::clear()
755 {
756     clearMetaTable();
757     m_notes->clear();
758     m_metadata.clear();
759 }
760 
setPixmap(const QPixmap & pixmap,const CoverFetch::Metadata & metadata)761 void CoverFoundSideBar::setPixmap( const QPixmap &pixmap, const CoverFetch::Metadata &metadata )
762 {
763     m_metadata = metadata;
764     updateNotes();
765     setPixmap( pixmap );
766 }
767 
setPixmap(const QPixmap & pixmap)768 void CoverFoundSideBar::setPixmap( const QPixmap &pixmap )
769 {
770     m_pixmap = pixmap;
771     QPixmap scaledPix = pixmap.scaled( QSize( 190, 190 ), Qt::KeepAspectRatio );
772     QPixmap prettyPix = The::svgHandler()->addBordersToPixmap( scaledPix, 5, QString(), true );
773     m_cover->setPixmap( prettyPix );
774     updateMetaTable();
775 }
776 
updateNotes()777 void CoverFoundSideBar::updateNotes()
778 {
779     bool enableNotes( false );
780     if( m_metadata.contains( "notes" ) )
781     {
782         const QString notes = m_metadata.value( "notes" );
783         if( !notes.isEmpty() )
784         {
785             m_notes->setText( notes );
786             enableNotes = true;
787         }
788         else
789             enableNotes = false;
790     }
791     else
792     {
793         m_notes->clear();
794         enableNotes = false;
795     }
796     m_tabs->setTabEnabled( m_tabs->indexOf( m_notes ), enableNotes );
797 }
798 
updateMetaTable()799 void CoverFoundSideBar::updateMetaTable()
800 {
801     clearMetaTable();
802 
803     QFormLayout *layout = static_cast< QFormLayout* >( m_metaTable->layout() );
804     layout->setSizeConstraint( QLayout::SetMinAndMaxSize );
805 
806     CoverFetch::Metadata::const_iterator mit = m_metadata.constBegin();
807     while( mit != m_metadata.constEnd() )
808     {
809         const QString &value = mit.value();
810         if( !value.isEmpty() )
811         {
812             const QString &tag = mit.key();
813             QString name;
814 
815             #define TAGHAS(s) (tag.compare(QLatin1String(s)) == 0)
816             if( TAGHAS("artist") )        name = i18nc( "@item::intable", "Artist" );
817             else if( TAGHAS("country") )  name = i18nc( "@item::intable", "Country" );
818             else if( TAGHAS("date") )     name = i18nc( "@item::intable", "Date" );
819             else if( TAGHAS("format") )   name = i18nc( "@item::intable File Format", "Format" );
820             else if( TAGHAS("height") )   name = i18nc( "@item::intable Image Height", "Height" );
821             else if( TAGHAS("name") )     name = i18nc( "@item::intable Album Title", "Title" );
822             else if( TAGHAS("type") )     name = i18nc( "@item::intable Release Type", "Type" );
823             else if( TAGHAS("released") ) name = i18nc( "@item::intable Release Date", "Released" );
824             else if( TAGHAS("size") )     name = i18nc( "@item::intable File Size", "Size" );
825             else if( TAGHAS("source") )   name = i18nc( "@item::intable Cover Provider", "Source" );
826             else if( TAGHAS("title") )    name = i18nc( "@item::intable Album Title", "Title" );
827             else if( TAGHAS("width") )    name = i18nc( "@item::intable Image Width", "Width" );
828             #undef TAGHAS
829 
830             if( !name.isEmpty() )
831             {
832                 QLabel *label = new QLabel( value, 0 );
833                 label->setToolTip( value );
834                 layout->addRow( QStringLiteral("<b>%1:</b>").arg(name), label );
835             }
836         }
837         ++mit;
838     }
839 
840     QString refUrl;
841 
842     const QString source = m_metadata.value( "source" );
843     if( source == "Last.fm" || source == "Discogs" )
844     {
845         refUrl = m_metadata.value( "releaseurl" );
846     }
847     else if( source == "Google" )
848     {
849         refUrl = m_metadata.value( "imgrefurl" );
850     }
851 
852     if( !refUrl.isEmpty() )
853     {
854         QFont font;
855         QFontMetrics qfm( font );
856         const QString &toolUrl = refUrl;
857         const QString &tooltip = qfm.elidedText( toolUrl, Qt::ElideMiddle, 350 );
858         const QString &decoded = QUrl::fromPercentEncoding( refUrl.toLocal8Bit() );
859         const QString &url     = QString( "<a href=\"%1\">%2</a>" )
860                                     .arg( decoded,
861                                           i18nc("@item::intable URL", "link") );
862 
863         QLabel *label = new QLabel( url, nullptr );
864         label->setOpenExternalLinks( true );
865         label->setTextInteractionFlags( Qt::TextBrowserInteraction );
866         label->setToolTip( tooltip );
867         layout->addRow( QString( "<b>%1:</b>" ).arg( i18nc("@item::intable", "URL") ), label );
868     }
869 }
870 
clearMetaTable()871 void CoverFoundSideBar::clearMetaTable()
872 {
873     QFormLayout *layout = static_cast< QFormLayout* >( m_metaTable->layout() );
874     int count = layout->count();
875     while( --count >= 0 )
876     {
877         QLayoutItem *child = layout->itemAt( 0 );
878         layout->removeItem( child );
879         delete child->widget();
880         delete child;
881     }
882 }
883 
CoverFoundItem(const QImage & cover,const CoverFetch::Metadata & data,const CoverFetch::ImageSize imageSize,QListWidget * parent)884 CoverFoundItem::CoverFoundItem( const QImage &cover,
885                                 const CoverFetch::Metadata &data,
886                                 const CoverFetch::ImageSize imageSize,
887                                 QListWidget *parent )
888     : QListWidgetItem( parent )
889     , m_metadata( data )
890 {
891     switch( imageSize )
892     {
893     default:
894     case CoverFetch::NormalSize:
895         m_bigPix = cover;
896         break;
897     case CoverFetch::ThumbSize:
898         m_thumb = cover;
899         break;
900     }
901 
902     QPixmap scaledPix = QPixmap::fromImage(cover.scaled( QSize( 120, 120 ), Qt::KeepAspectRatio ));
903     QPixmap prettyPix = The::svgHandler()->addBordersToPixmap( scaledPix, 5, QString(), true );
904     setSizeHint( QSize( 140, 150 ) );
905     setIcon( prettyPix );
906     setCaption();
907     setFont( QFontDatabase::systemFont( QFontDatabase::SmallestReadableFont ) );
908     setTextAlignment( Qt::AlignHCenter | Qt::AlignTop );
909 }
910 
~CoverFoundItem()911 CoverFoundItem::~CoverFoundItem()
912 {
913 }
914 
operator ==(const CoverFoundItem & other) const915 bool CoverFoundItem::operator==( const CoverFoundItem &other ) const
916 {
917     return m_metadata == other.m_metadata;
918 }
919 
operator !=(const CoverFoundItem & other) const920 bool CoverFoundItem::operator!=( const CoverFoundItem &other ) const
921 {
922     return !( *this == other );
923 }
924 
setCaption()925 void CoverFoundItem::setCaption()
926 {
927     QStringList captions;
928     const QString &width = m_metadata.value( QLatin1String("width") );
929     const QString &height = m_metadata.value( QLatin1String("height") );
930     if( !width.isEmpty() && !height.isEmpty() )
931         captions << QString( "%1 x %2" ).arg( width, height );
932 
933     int size = m_metadata.value( QLatin1String("size") ).toInt();
934     if( size )
935     {
936         const QString source = m_metadata.value( QLatin1String("source") );
937 
938         captions << ( QString::number( size ) + QLatin1Char('k') );
939     }
940 
941     if( !captions.isEmpty() )
942         setText( captions.join( QLatin1String( " - " ) ) );
943 }
944 
945