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