1 /*
2  * Strawberry Music Player
3  * This file was part of Clementine.
4  * Copyright 2010, David Sansome <me@davidsansome.com>
5  * Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
6  *
7  * Strawberry is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11  *
12  * Strawberry is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with Strawberry.  If not, see <http://www.gnu.org/licenses/>.
19  *
20  */
21 
22 #include "config.h"
23 
24 #include <QWidget>
25 #include <QDialog>
26 #include <QStandardItemModel>
27 #include <QAbstractItemModel>
28 #include <QStyledItemDelegate>
29 #include <QStyleOptionViewItem>
30 #include <QStandardItem>
31 #include <QList>
32 #include <QVariant>
33 #include <QByteArray>
34 #include <QString>
35 #include <QUrl>
36 #include <QImage>
37 #include <QPixmap>
38 #include <QPainter>
39 #include <QIcon>
40 #include <QFont>
41 #include <QFontMetrics>
42 #include <QColor>
43 #include <QRect>
44 #include <QSize>
45 #include <QDialogButtonBox>
46 #include <QPushButton>
47 #include <QKeySequence>
48 #include <QtEvents>
49 
50 #include "core/application.h"
51 #include "core/utilities.h"
52 #include "core/logging.h"
53 #include "widgets/busyindicator.h"
54 #include "widgets/forcescrollperpixel.h"
55 #include "widgets/groupediconview.h"
56 #include "widgets/qsearchfield.h"
57 #include "albumcoversearcher.h"
58 #include "albumcoverfetcher.h"
59 #include "albumcoverloader.h"
60 #include "albumcoverloaderoptions.h"
61 #include "albumcoverloaderresult.h"
62 #include "albumcoverimageresult.h"
63 #include "ui_albumcoversearcher.h"
64 
65 const int SizeOverlayDelegate::kMargin = 4;
66 const int SizeOverlayDelegate::kPaddingX = 3;
67 const int SizeOverlayDelegate::kPaddingY = 1;
QListWidgetItem(icon,text,parent,type)68 const qreal SizeOverlayDelegate::kBorder = 5.0;
69 const qreal SizeOverlayDelegate::kFontPointSize = 7.5;
70 const int SizeOverlayDelegate::kBorderAlpha = 200;
71 const int SizeOverlayDelegate::kBackgroundAlpha = 175;
72 
73 SizeOverlayDelegate::SizeOverlayDelegate(QObject *parent)
74     : QStyledItemDelegate(parent) {}
75 
76 void SizeOverlayDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &idx) const {
77 
78   QStyledItemDelegate::paint(painter, option, idx);
79 
80   if (!idx.data(AlbumCoverSearcher::Role_ImageFetchFinished).toBool()) {
81     return;
82   }
83 
84   const QSize size = idx.data(AlbumCoverSearcher::Role_ImageSize).toSize();
85   const QString text = Utilities::PrettySize(size);
86 
87   QFont font(option.font);
88   font.setPointSizeF(kFontPointSize);
89   font.setBold(true);
90 
91   const QFontMetrics metrics(font);
backend()92 
93 #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
94   const int text_width = metrics.horizontalAdvance(text);
95 #else
96   const int text_width = metrics.width(text);
97 #endif
98 
99   const QRect icon_rect(option.rect.left(), option.rect.top(), option.rect.width(), option.rect.width());
100 
101   const QRect background_rect(icon_rect.right() - kMargin - text_width - kPaddingX * 2, icon_rect.bottom() - kMargin - metrics.height() - kPaddingY * 2, text_width + kPaddingX * 2, metrics.height() + kPaddingY * 2);
102   const QRect text_rect(background_rect.left() + kPaddingX, background_rect.top() + kPaddingY, text_width, metrics.height());
103 
104   painter->save();
105   painter->setRenderHint(QPainter::Antialiasing);
106   painter->setPen(QColor(0, 0, 0, kBorderAlpha));
107   painter->setBrush(QColor(0, 0, 0, kBackgroundAlpha));
108   painter->drawRoundedRect(background_rect, kBorder, kBorder);
109 
110   painter->setPen(Qt::white);
111   painter->setFont(font);
112   painter->drawText(text_rect, text);
113   painter->restore();
114 
115 }
116 
117 AlbumCoverSearcher::AlbumCoverSearcher(const QIcon &no_cover_icon, Application *app, QWidget *parent)
118     : QDialog(parent),
119       ui_(new Ui_AlbumCoverSearcher),
120       app_(app),
121       model_(new QStandardItemModel(this)),
122       no_cover_icon_(no_cover_icon),
123       fetcher_(nullptr),
124       id_(0) {
125 
126   setWindowModality(Qt::WindowModal);
127   ui_->setupUi(this);
128   ui_->busy->hide();
129 
130   ui_->covers->set_header_text(tr("Covers from %1"));
131   ui_->covers->AddSortSpec(Role_ImageDimensions, Qt::DescendingOrder);
132   ui_->covers->setItemDelegate(new SizeOverlayDelegate(this));
133   ui_->covers->setModel(model_);
134 
ItemAsSong(QListWidgetItem * item)135   options_.get_image_data_ = true;
136   options_.get_image_ = true;
137   options_.scale_output_image_ = false;
138   options_.pad_output_image_ = false;
139   options_.create_thumbnail_ = true;
140   options_.pad_thumbnail_image_ = true;
141   options_.thumbnail_size_ = ui_->covers->iconSize();
142 
143   QObject::connect(app_->album_cover_loader(), &AlbumCoverLoader::AlbumCoverLoaded, this, &AlbumCoverSearcher::AlbumCoverLoaded);
144   QObject::connect(ui_->search, &QPushButton::clicked, this, &AlbumCoverSearcher::Search);
145   QObject::connect(ui_->covers, &GroupedIconView::doubleClicked, this, &AlbumCoverSearcher::CoverDoubleClicked);
146 
147   new ForceScrollPerPixel(ui_->covers, this);
148 
149   ui_->buttonBox->button(QDialogButtonBox::Cancel)->setShortcut(QKeySequence::Close);
150 
151 }
152 
153 AlbumCoverSearcher::~AlbumCoverSearcher() {
154   delete ui_;
155 }
156 
157 void AlbumCoverSearcher::Init(AlbumCoverFetcher *fetcher) {
158 
159   fetcher_ = fetcher;
160   QObject::connect(fetcher_, &AlbumCoverFetcher::SearchFinished, this, &AlbumCoverSearcher::SearchFinished, Qt::QueuedConnection);
161 
162 }
163 
164 AlbumCoverImageResult AlbumCoverSearcher::Exec(const QString &artist, const QString &album) {
165 
166   ui_->artist->setText(artist);
167   ui_->album->setText(album);
168   ui_->artist->setFocus();
169 
170   if (!artist.isEmpty() || !album.isEmpty()) {
171     Search();
172   }
173 
174   if (exec() == QDialog::Rejected) return AlbumCoverImageResult();
175 
176   QModelIndex selected = ui_->covers->currentIndex();
177   if (!selected.isValid() || !selected.data(Role_ImageFetchFinished).toBool())
178     return AlbumCoverImageResult();
179 
180   AlbumCoverImageResult result;
181   result.image_data = selected.data(Role_ImageData).toByteArray();
182   result.image = selected.data(Role_Image).value<QImage>();
183   result.mime_type = Utilities::MimeTypeFromData(result.image_data);
184 
185   return result;
186 
187 }
188 
189 void AlbumCoverSearcher::Search() {
190 
191   model_->clear();
192   cover_loading_tasks_.clear();
193 
194   if (ui_->album->isEnabled()) {
195     id_ = fetcher_->SearchForCovers(ui_->artist->text(), ui_->album->text());
196     ui_->search->setText(tr("Abort"));
197     ui_->busy->show();
198     ui_->artist->setEnabled(false);
199     ui_->album->setEnabled(false);
200     ui_->covers->setEnabled(false);
201   }
202   else {
203     fetcher_->Clear();
204     ui_->search->setText(tr("Search"));
205     ui_->busy->hide();
206     ui_->search->setEnabled(true);
207     ui_->artist->setEnabled(true);
208     ui_->album->setEnabled(true);
209     ui_->covers->setEnabled(true);
210   }
211 
212 }
213 
214 void AlbumCoverSearcher::SearchFinished(const quint64 id, const CoverProviderSearchResults &results) {
215 
216   if (id != id_) return;
217 
218   ui_->search->setEnabled(true);
219   ui_->artist->setEnabled(true);
220   ui_->album->setEnabled(true);
221   ui_->covers->setEnabled(true);
222   ui_->search->setText(tr("Search"));
223   id_ = 0;
224 
225   for (const CoverProviderSearchResult &result : results) {
226 
227     if (result.image_url.isEmpty()) continue;
228 
229     quint64 new_id = app_->album_cover_loader()->LoadImageAsync(options_, result.image_url, QUrl());
230 
231     QStandardItem *item = new QStandardItem;
232     item->setIcon(no_cover_icon_);
233     item->setText(result.artist + " - " + result.album);
234     item->setData(result.image_url, Role_ImageURL);
235     item->setData(new_id, Role_ImageRequestId);
236     item->setData(false, Role_ImageFetchFinished);
237     item->setData(QVariant(Qt::AlignTop | Qt::AlignHCenter), Qt::TextAlignmentRole);
238     item->setData(result.provider, GroupedIconView::Role_Group);
239 
240     model_->appendRow(item);
241 
242     cover_loading_tasks_[new_id] = item;
243   }
244 
245   if (cover_loading_tasks_.isEmpty()) ui_->busy->hide();
246 
247 }
248 
249 void AlbumCoverSearcher::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result) {
250 
251   if (!cover_loading_tasks_.contains(id)) return;
252   QStandardItem *item = cover_loading_tasks_.take(id);
253 
254   if (cover_loading_tasks_.isEmpty()) ui_->busy->hide();
255 
256   if (!result.success || result.album_cover.image_data.isNull() || result.album_cover.image.isNull() || result.image_thumbnail.isNull()) {
257     model_->removeRow(item->row());
258     return;
259   }
260 
261   QPixmap pixmap = QPixmap::fromImage(result.image_thumbnail);
262   if (pixmap.isNull()) {
263     model_->removeRow(item->row());
264     return;
265   }
266 
267   QIcon icon(pixmap);
268 
269   item->setData(true, Role_ImageFetchFinished);
270   item->setData(result.album_cover.image_data, Role_ImageData);
271   item->setData(result.album_cover.image, Role_Image);
272   item->setData(result.album_cover.image.width() * result.album_cover.image.height(), Role_ImageDimensions);
273   item->setData(result.album_cover.image.size(), Role_ImageSize);
274   if (!icon.isNull()) item->setIcon(icon);
275 
276 }
277 
278 void AlbumCoverSearcher::keyPressEvent(QKeyEvent *e) {
279 
280   if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
281     e->ignore();
282     return;
283   }
284 
285   QDialog::keyPressEvent(e);
286 
287 }
288 
289 void AlbumCoverSearcher::CoverDoubleClicked(const QModelIndex &idx) {
290   if (idx.isValid()) accept();
291 }
292