diff --git a/data/data.qrc b/data/data.qrc index 874f7ceb..c96fbe05 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -12,6 +12,7 @@ schema/schema-18.sql schema/device-schema.sql style/strawberry.css + style/artistbio.css style/smartplaylistsearchterm.css html/oauthsuccess.html pictures/strawberry.png diff --git a/data/icons.qrc b/data/icons.qrc index 847f5c96..27806c7f 100644 --- a/data/icons.qrc +++ b/data/icons.qrc @@ -97,6 +97,7 @@ icons/128x128/somafm.png icons/128x128/radioparadise.png icons/128x128/musicbrainz.png + icons/128x128/guitar.png icons/64x64/albums.png icons/64x64/alsa.png icons/64x64/application-exit.png @@ -195,6 +196,7 @@ icons/64x64/somafm.png icons/64x64/radioparadise.png icons/64x64/musicbrainz.png + icons/64x64/guitar.png icons/48x48/albums.png icons/48x48/alsa.png icons/48x48/application-exit.png @@ -297,6 +299,7 @@ icons/48x48/somafm.png icons/48x48/radioparadise.png icons/48x48/musicbrainz.png + icons/48x48/guitar.png icons/32x32/albums.png icons/32x32/alsa.png icons/32x32/application-exit.png @@ -399,6 +402,7 @@ icons/32x32/somafm.png icons/32x32/radioparadise.png icons/32x32/musicbrainz.png + icons/32x32/guitar.png icons/22x22/albums.png icons/22x22/alsa.png icons/22x22/application-exit.png @@ -501,5 +505,6 @@ icons/22x22/somafm.png icons/22x22/radioparadise.png icons/22x22/musicbrainz.png + icons/22x22/guitar.png diff --git a/data/icons/128x128/guitar.png b/data/icons/128x128/guitar.png new file mode 100644 index 00000000..0844bbf9 Binary files /dev/null and b/data/icons/128x128/guitar.png differ diff --git a/data/icons/22x22/guitar.png b/data/icons/22x22/guitar.png new file mode 100644 index 00000000..626cfdfe Binary files /dev/null and b/data/icons/22x22/guitar.png differ diff --git a/data/icons/32x32/guitar.png b/data/icons/32x32/guitar.png new file mode 100644 index 00000000..e854d638 Binary files /dev/null and b/data/icons/32x32/guitar.png differ diff --git a/data/icons/48x48/guitar.png b/data/icons/48x48/guitar.png new file mode 100644 index 00000000..dc5f35cb Binary files /dev/null and b/data/icons/48x48/guitar.png differ diff --git a/data/icons/64x64/guitar.png b/data/icons/64x64/guitar.png new file mode 100644 index 00000000..12d87b76 Binary files /dev/null and b/data/icons/64x64/guitar.png differ diff --git a/data/icons/full/guitar.png b/data/icons/full/guitar.png new file mode 100644 index 00000000..a283de39 Binary files /dev/null and b/data/icons/full/guitar.png differ diff --git a/data/style/artistbio.css b/data/style/artistbio.css new file mode 100644 index 00000000..5ac19618 --- /dev/null +++ b/data/style/artistbio.css @@ -0,0 +1,7 @@ +QScrollArea { + background: qpalette(base); +} + +QTextEdit { + border: 0px; +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 629f1fd7..68398d7d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -39,6 +39,7 @@ set(SOURCES core/scopedtransaction.cpp core/translations.cpp core/systemtrayicon.cpp + core/latch.cpp utilities/strutils.cpp utilities/envutils.cpp @@ -245,6 +246,12 @@ set(SOURCES widgets/loginstatewidget.cpp widgets/ratingwidget.cpp widgets/resizabletextedit.cpp + widgets/widgetfadehelper.cpp + widgets/prettyimageview.cpp + widgets/prettyimage.cpp + widgets/collapsibleinfoheader.cpp + widgets/collapsibleinfopane.cpp + widgets/infotextview.cpp osd/osdbase.cpp osd/osdpretty.cpp @@ -291,6 +298,12 @@ set(SOURCES organize/organizedialog.cpp organize/organizeerrordialog.cpp + artistbio/artistbioview.cpp + artistbio/artistbiofetcher.cpp + artistbio/artistbioprovider.cpp + artistbio/lastfmartistbio.cpp + artistbio/wikipediaartistbio.cpp + ) set(HEADERS @@ -316,6 +329,7 @@ set(HEADERS core/potranslator.h core/mimedata.h core/stylesheetloader.h + core/latch.h engine/enginebase.h engine/devicefinders.h @@ -486,6 +500,13 @@ set(HEADERS widgets/qsearchfield.h widgets/ratingwidget.h widgets/forcescrollperpixel.h + widgets/resizabletextedit.h + widgets/widgetfadehelper.h + widgets/prettyimage.h + widgets/prettyimageview.h + widgets/collapsibleinfoheader.h + widgets/collapsibleinfopane.h + widgets/infotextview.h osd/osdbase.h osd/osdpretty.h @@ -527,6 +548,12 @@ set(HEADERS organize/organizedialog.h organize/organizeerrordialog.h + artistbio/artistbiofetcher.h + artistbio/artistbioprovider.h + artistbio/artistbioview.h + artistbio/lastfmartistbio.h + artistbio/wikipediaartistbio.h + ) set(UI diff --git a/src/artistbio/artistbiofetcher.cpp b/src/artistbio/artistbiofetcher.cpp new file mode 100644 index 00000000..f30fe2bc --- /dev/null +++ b/src/artistbio/artistbiofetcher.cpp @@ -0,0 +1,126 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include +#include +#include + +#include "artistbiofetcher.h" +#include "artistbioprovider.h" +#include "core/logging.h" + +ArtistBioFetcher::ArtistBioFetcher(QObject *parent) + : QObject(parent), + timeout_duration_(kDefaultTimeoutDuration), + next_id_(1) {} + +void ArtistBioFetcher::AddProvider(ArtistBioProvider *provider) { + + providers_ << provider; + connect(provider, SIGNAL(ImageReady(int, QUrl)), SLOT(ImageReady(int, QUrl)), Qt::QueuedConnection); + connect(provider, SIGNAL(InfoReady(int, CollapsibleInfoPane::Data)), SLOT(InfoReady(int, CollapsibleInfoPane::Data)), Qt::QueuedConnection); + connect(provider, SIGNAL(Finished(int)), SLOT(ProviderFinished(int)), Qt::QueuedConnection); + +} + +ArtistBioFetcher::~ArtistBioFetcher() { + + while (!providers_.isEmpty()) { + ArtistBioProvider *provider = providers_.takeFirst(); + provider->deleteLater(); + } + +} + +int ArtistBioFetcher::FetchInfo(const Song &metadata) { + + const int id = next_id_++; + results_[id] = Result(); + timeout_timers_[id] = new QTimer(this); + timeout_timers_[id]->setSingleShot(true); + timeout_timers_[id]->setInterval(timeout_duration_); + timeout_timers_[id]->start(); + + connect(timeout_timers_[id], &QTimer::timeout, [this, id]() { Timeout(id); }); + + for (ArtistBioProvider *provider : providers_) { + if (provider->is_enabled()) { + waiting_for_[id].append(provider); + provider->Start(id, metadata); + } + } + return id; + +} + +void ArtistBioFetcher::ImageReady(const int id, const QUrl &url) { + + if (!results_.contains(id)) return; + results_[id].images_ << url; + +} + +void ArtistBioFetcher::InfoReady(const int id, const CollapsibleInfoPane::Data &data) { + + if (!results_.contains(id)) return; + results_[id].info_ << data; + + if (!waiting_for_.contains(id)) return; + emit InfoResultReady(id, data); + +} + +void ArtistBioFetcher::ProviderFinished(const int id) { + + if (!results_.contains(id)) return; + if (!waiting_for_.contains(id)) return; + + ArtistBioProvider *provider = qobject_cast(sender()); + if (!waiting_for_[id].contains(provider)) return; + + waiting_for_[id].removeAll(provider); + if (waiting_for_[id].isEmpty()) { + Result result = results_.take(id); + emit ResultReady(id, result); + waiting_for_.remove(id); + delete timeout_timers_.take(id); + } + +} + +void ArtistBioFetcher::Timeout(const int id) { + + if (!results_.contains(id)) return; + if (!waiting_for_.contains(id)) return; + + // Emit the results that we have already + emit ResultReady(id, results_.take(id)); + + // Cancel any providers that we're still waiting for + for (ArtistBioProvider *provider : waiting_for_[id]) { + qLog(Info) << "Request timed out from info provider" << provider->name(); + provider->Cancel(id); + } + waiting_for_.remove(id); + + // Remove the timer + delete timeout_timers_.take(id); + +} diff --git a/src/artistbio/artistbiofetcher.h b/src/artistbio/artistbiofetcher.h new file mode 100644 index 00000000..da56dca1 --- /dev/null +++ b/src/artistbio/artistbiofetcher.h @@ -0,0 +1,75 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef ARTISTBIOFETCHER_H +#define ARTISTBIOFETCHER_H + +#include +#include +#include +#include + +#include "widgets/collapsibleinfopane.h" +#include "core/song.h" + +class QTimer; +class ArtistBioProvider; + +class ArtistBioFetcher : public QObject { + Q_OBJECT + + public: + explicit ArtistBioFetcher(QObject *parent = nullptr); + ~ArtistBioFetcher() override; + + struct Result { + QList images_; + QList info_; + }; + + static const int kDefaultTimeoutDuration = 25000; + + void AddProvider(ArtistBioProvider *provider); + int FetchInfo(const Song &metadata); + + QList providers() const { return providers_; } + + signals: + void InfoResultReady(int, CollapsibleInfoPane::Data); + void ResultReady(int, ArtistBioFetcher::Result); + + private slots: + void ImageReady(const int id, const QUrl &url); + void InfoReady(const int id, const CollapsibleInfoPane::Data &data); + void ProviderFinished(const int id); + void Timeout(const int id); + + private: + QList providers_; + + QMap results_; + QMap> waiting_for_; + QMap timeout_timers_; + + int timeout_duration_; + int next_id_; +}; + +#endif // ARTISTBIOFETCHER_H diff --git a/src/artistbio/artistbioprovider.cpp b/src/artistbio/artistbioprovider.cpp new file mode 100644 index 00000000..a9db1041 --- /dev/null +++ b/src/artistbio/artistbioprovider.cpp @@ -0,0 +1,25 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "artistbioprovider.h" + +ArtistBioProvider::ArtistBioProvider() : enabled_(true) {} + +QString ArtistBioProvider::name() const { return metaObject()->className(); } diff --git a/src/artistbio/artistbioprovider.h b/src/artistbio/artistbioprovider.h new file mode 100644 index 00000000..f7dd7f41 --- /dev/null +++ b/src/artistbio/artistbioprovider.h @@ -0,0 +1,53 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef ARTISTBIOPROVIDER_H +#define ARTISTBIOPROVIDER_H + +#include +#include + +#include "widgets/collapsibleinfopane.h" +#include "core/song.h" + +class ArtistBioProvider : public QObject { + Q_OBJECT + + public: + explicit ArtistBioProvider(); + + virtual void Start(const int id, const Song &song) = 0; + virtual void Cancel(const int) {} + + virtual QString name() const; + + bool is_enabled() const { return enabled_; } + void set_enabled(bool enabled) { enabled_ = enabled; } + + signals: + void ImageReady(int, QUrl); + void InfoReady(int, CollapsibleInfoPane::Data); + void Finished(int); + + private: + bool enabled_; +}; + +#endif // ARTISTBIOPROVIDER_H diff --git a/src/artistbio/artistbioview.cpp b/src/artistbio/artistbioview.cpp new file mode 100644 index 00000000..cc941777 --- /dev/null +++ b/src/artistbio/artistbioview.cpp @@ -0,0 +1,287 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/song.h" +#include "core/networkaccessmanager.h" +#include "widgets/prettyimageview.h" +#include "widgets/widgetfadehelper.h" +#include "artistbiofetcher.h" +#include "lastfmartistbio.h" +#include "wikipediaartistbio.h" + +#include "artistbioview.h" + +const char *ArtistBioView::kSettingsGroup = "ArtistBio"; + +ArtistBioView::ArtistBioView(QWidget *parent) + : QWidget(parent), + network_(new NetworkAccessManager(this)), + fetcher_(new ArtistBioFetcher(this)), + current_request_id_(-1), + container_(new QVBoxLayout), + section_container_(nullptr), + fader_(new WidgetFadeHelper(this, 1000)), + dirty_(false) { + + // Add the top-level scroll area + QScrollArea *scrollarea = new QScrollArea(this); + setLayout(new QVBoxLayout); + layout()->setContentsMargins(0, 0, 0, 0); + layout()->addWidget(scrollarea); + + // Add a container widget to the scroll area + QWidget *container_widget = new QWidget; + container_widget->setLayout(container_); + container_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); + container_widget->setBackgroundRole(QPalette::Base); + container_->setSizeConstraint(QLayout::SetMinAndMaxSize); + container_->setContentsMargins(0, 0, 0, 0); + container_->setSpacing(6); + scrollarea->setWidget(container_widget); + scrollarea->setWidgetResizable(true); + + // Add a spacer to the bottom of the container + container_->addStretch(); + + // Set stylesheet + QFile stylesheet(":/style/artistbio.css"); + if (stylesheet.open(QIODevice::ReadOnly)) { + setStyleSheet(QString::fromLatin1(stylesheet.readAll())); + stylesheet.close(); + } + + fetcher_->AddProvider(new LastFMArtistBio); + fetcher_->AddProvider(new WikipediaArtistBio); + + connect(fetcher_, SIGNAL(ResultReady(int, ArtistBioFetcher::Result)), SLOT(ResultReady(int, ArtistBioFetcher::Result))); + connect(fetcher_, SIGNAL(InfoResultReady(int, CollapsibleInfoPane::Data)), SLOT(InfoResultReady(int, CollapsibleInfoPane::Data))); + +} + +ArtistBioView::~ArtistBioView() {} + +void ArtistBioView::showEvent(QShowEvent *e) { + + if (dirty_) { + MaybeUpdate(queued_metadata_); + dirty_ = false; + } + + QWidget::showEvent(e); + +} + +void ArtistBioView::ReloadSettings() { + + for (CollapsibleInfoPane *pane : sections_) { + QWidget *contents = pane->data().contents_; + if (!contents) continue; + + QMetaObject::invokeMethod(contents, "ReloadSettings"); + } + +} + +bool ArtistBioView::NeedsUpdate(const Song &old_metadata, const Song &new_metadata) const { + + if (new_metadata.artist().isEmpty()) return false; + + return old_metadata.artist() != new_metadata.artist(); + +} + +void ArtistBioView::InfoResultReady(const int id, const CollapsibleInfoPane::Data &_data) { + + if (id != current_request_id_) return; + + AddSection(new CollapsibleInfoPane(_data, this)); + CollapseSections(); + +} + +void ArtistBioView::ResultReady(const int id, const ArtistBioFetcher::Result &result) { + + if (id != current_request_id_) return; + + if (!result.images_.isEmpty()) { + // Image view goes at the top + PrettyImageView *image_view = new PrettyImageView(network_, this); + AddWidget(image_view); + + for (const QUrl& url : result.images_) { + image_view->AddImage(url); + } + } + + CollapseSections(); + +} + +void ArtistBioView::Clear() { + + fader_->StartFade(); + + qDeleteAll(widgets_); + widgets_.clear(); + if (section_container_) { + container_->removeWidget(section_container_); + delete section_container_; + } + sections_.clear(); + + // Container for collapsible sections goes below + section_container_ = new QWidget; + section_container_->setLayout(new QVBoxLayout); + section_container_->layout()->setContentsMargins(0, 0, 0, 0); + section_container_->layout()->setSpacing(1); + section_container_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); + container_->insertWidget(0, section_container_); + +} + +void ArtistBioView::AddSection(CollapsibleInfoPane *section) { + + int i = 0; + for (; i < sections_.count(); ++i) { + if (section->data() < sections_[i]->data()) break; + } + + ConnectWidget(section->data().contents_); + + sections_.insert(i, section); + qobject_cast(section_container_->layout())->insertWidget(i, section); + section->show(); + +} + +void ArtistBioView::AddWidget(QWidget *widget) { + + ConnectWidget(widget); + + container_->insertWidget(container_->count() - 2, widget); + widgets_ << widget; + +} + +void ArtistBioView::SongChanged(const Song &metadata) { + + if (isVisible()) { + MaybeUpdate(metadata); + dirty_ = false; + } + else { + queued_metadata_ = metadata; + dirty_ = true; + } + +} + +void ArtistBioView::SongFinished() { dirty_ = false; } + +void ArtistBioView::MaybeUpdate(const Song &metadata) { + + if (old_metadata_.is_valid()) { + if (!NeedsUpdate(old_metadata_, metadata)) { + return; + } + } + + Update(metadata); + old_metadata_ = metadata; + +} + +void ArtistBioView::Update(const Song &metadata) { + + current_request_id_ = fetcher_->FetchInfo(metadata); + + // Do this after the new pane has been shown otherwise it'll just grab a black rectangle. + Clear(); + QTimer::singleShot(0, fader_, SLOT(StartBlur())); + +} + +void ArtistBioView::CollapseSections() { + + QSettings s; + s.beginGroup(kSettingsGroup); + + // Sections are already sorted by type and relevance, so the algorithm we use to determine which ones to show by default is: + // * In the absence of any user preference, show the first (highest relevance section of each type and hide the rest) + // * If one or more sections in a type have been explicitly hidden/shown by the user before then hide all sections in that type and show only the ones that are explicitly shown. + + QMultiMap types_; + QSet has_user_preference_; + for (CollapsibleInfoPane *pane : sections_) { + const CollapsibleInfoPane::Data::Type type = pane->data().type_; + types_.insert(type, pane); + + QVariant preference = s.value(pane->data().id_); + if (preference.isValid()) { + has_user_preference_.insert(type); + if (preference.toBool()) { + pane->Expand(); + } + } + } + + for (CollapsibleInfoPane::Data::Type type : types_.keys()) { + if (!has_user_preference_.contains(type)) { + // Expand the first one + types_.values(type).last()->Expand(); + } + } + + for (CollapsibleInfoPane *pane : sections_) { + connect(pane, SIGNAL(Toggled(bool)), SLOT(SectionToggled(bool))); + } + +} + +void ArtistBioView::SectionToggled(const bool value) { + + CollapsibleInfoPane *pane = qobject_cast(sender()); + if (!pane || !sections_.contains(pane)) return; + + QSettings s; + s.beginGroup(kSettingsGroup); + s.setValue(pane->data().id_, value); + s.endGroup(); + +} + +void ArtistBioView::ConnectWidget(QWidget *widget) { + + const QMetaObject *m = widget->metaObject(); + + if (m->indexOfSignal("ShowSettingsDialog()") != -1) { + connect(widget, SIGNAL(ShowSettingsDialog()), SIGNAL(ShowSettingsDialog())); + } + +} diff --git a/src/artistbio/artistbioview.h b/src/artistbio/artistbioview.h new file mode 100644 index 00000000..90b09373 --- /dev/null +++ b/src/artistbio/artistbioview.h @@ -0,0 +1,104 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef ARTISTBIOVIEW_H +#define ARTISTBIOVIEW_H + +#include +#include +#include + +#include "core/song.h" +#include "widgets/collapsibleinfopane.h" +#include "widgets/widgetfadehelper.h" +#include "widgets/collapsibleinfopane.h" +#include "playlist/playlistitem.h" +#include "smartplaylists/playlistgenerator_fwd.h" +#include "artistbiofetcher.h" + +class QNetworkAccessManager; +class QTimeLine; +class QVBoxLayout; +class QScrollArea; +class QShowEvent; + +class PrettyImageView; +class CollapsibleInfoPane; +class WidgetFadeHelper; + +class ArtistBioView : public QWidget { + Q_OBJECT + + public: + explicit ArtistBioView(QWidget *parent = nullptr); + ~ArtistBioView() override; + + static const char *kSettingsGroup; + + public slots: + void SongChanged(const Song& metadata); + void SongFinished(); + virtual void ReloadSettings(); + + signals: + void ShowSettingsDialog(); + + protected: + void showEvent(QShowEvent *e) override; + + void Update(const Song &metadata); + void AddWidget(QWidget *widget); + void AddSection(CollapsibleInfoPane *section); + void Clear(); + void CollapseSections(); + + bool NeedsUpdate(const Song& old_metadata, const Song &new_metadata) const; + + protected slots: + void ResultReady(const int id, const ArtistBioFetcher::Result &result); + void InfoResultReady(const int id, const CollapsibleInfoPane::Data &data); + + protected: + QNetworkAccessManager *network_; + ArtistBioFetcher *fetcher_; + int current_request_id_; + + private: + void MaybeUpdate(const Song &metadata); + void ConnectWidget(QWidget *widget); + + private slots: + void SectionToggled(const bool value); + + private: + QVBoxLayout *container_; + QList widgets_; + + QWidget *section_container_; + QList sections_; + + WidgetFadeHelper *fader_; + + Song queued_metadata_; + Song old_metadata_; + bool dirty_; +}; + +#endif // ARTISTBIOVIEW_H diff --git a/src/artistbio/lastfmartistbio.cpp b/src/artistbio/lastfmartistbio.cpp new file mode 100644 index 00000000..91fa7c12 --- /dev/null +++ b/src/artistbio/lastfmartistbio.cpp @@ -0,0 +1,209 @@ +/* + * Strawberry Music Player + * Copyright 2020, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/networkaccessmanager.h" +#include "core/song.h" +#include "core/logging.h" +#include "core/iconloader.h" + +#include "lastfmartistbio.h" +#include "widgets/infotextview.h" +#include "scrobbler/scrobblingapi20.h" +#include "scrobbler/lastfmscrobbler.h" + +LastFMArtistBio::LastFMArtistBio() : ArtistBioProvider(), network_(new NetworkAccessManager(this)) {} + +LastFMArtistBio::~LastFMArtistBio() { + + while (!replies_.isEmpty()) { + QNetworkReply *reply = replies_.takeFirst(); + disconnect(reply, nullptr, this, nullptr); + reply->abort(); + reply->deleteLater(); + } + +} + +void LastFMArtistBio::Start(const int id, const Song &song) { + + ParamList params = ParamList() + << Param("api_key", ScrobblingAPI20::kApiKey) + << Param("lang", QLocale().name().left(2).toLower()) + << Param("format", "json") + << Param("method", "artist.getinfo") + << Param("artist", song.artist()); + + std::sort(params.begin(), params.end()); + + QUrlQuery url_query; + for (const Param ¶m : params) { + url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second)); + } + + QUrl url(LastFMScrobbler::kApiUrl); + url.setQuery(url_query); + QNetworkRequest req(url); +#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0) + req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); +#else + req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); +#endif + req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + + QNetworkReply *reply = network_->get(req); + replies_ << reply; + connect(reply, &QNetworkReply::finished, [=] { RequestFinished(reply, id); }); + + qLog(Debug) << "Sending request" << url_query.toString(QUrl::FullyDecoded); + +} + +QByteArray LastFMArtistBio::GetReplyData(QNetworkReply *reply) { + + QByteArray data; + + if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { + data = reply->readAll(); + } + else { + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { + // This is a network error, there is nothing more to do. + Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + } + else { + QString error; + // See if there is Json data containing "error" and "message" - then use that instead. + data = reply->readAll(); + QJsonParseError json_error; + QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); + int error_code = -1; + if (json_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) { + QJsonObject json_obj = json_doc.object(); + if (json_obj.contains("error") && json_obj.contains("message")) { + error_code = json_obj["error"].toInt(); + QString error_message = json_obj["message"].toString(); + error = QString("%1 (%2)").arg(error_message).arg(error_code); + } + } + if (error.isEmpty()) { + if (reply->error() != QNetworkReply::NoError) { + error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else { + error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); + } + } + Error(error); + } + return QByteArray(); + } + + return data; + +} + +QJsonObject LastFMArtistBio::ExtractJsonObj(const QByteArray &data) { + + if (data.isEmpty()) return QJsonObject(); + + QJsonParseError error; + QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); + + if (error.error != QJsonParseError::NoError) { + Error("Reply from server missing Json data.", data); + return QJsonObject(); + } + if (json_doc.isEmpty()) { + Error("Received empty Json document.", json_doc); + return QJsonObject(); + } + if (!json_doc.isObject()) { + Error("Json document is not an object.", json_doc); + return QJsonObject(); + } + QJsonObject json_obj = json_doc.object(); + if (json_obj.isEmpty()) { + Error("Received empty Json object.", json_doc); + return QJsonObject(); + } + + return json_obj; + +} + +void LastFMArtistBio::RequestFinished(QNetworkReply *reply, const int id) { + + if (!replies_.contains(reply)) return; + replies_.removeAll(reply); + disconnect(reply, nullptr, this, nullptr); + reply->deleteLater(); + + QJsonObject json_obj = ExtractJsonObj(GetReplyData(reply)); + + QString title; + QString text; + if (!json_obj.isEmpty() && json_obj.contains("artist") && json_obj["artist"].isObject()) { + json_obj = json_obj["artist"].toObject(); + if (json_obj.contains("bio") && json_obj["bio"].isObject()) { + title = json_obj["name"].toString(); + QJsonObject obj_bio = json_obj["bio"].toObject(); + if (obj_bio.contains("content")) { + text = obj_bio["content"].toString(); + } + } + } + + CollapsibleInfoPane::Data info_data; + info_data.id_ = title; + info_data.title_ = tr("Biography"); + info_data.type_ = CollapsibleInfoPane::Data::Type_Biography; + info_data.icon_ = IconLoader::Load("scrobble"); + InfoTextView *editor = new InfoTextView; + editor->SetHtml(text); + info_data.contents_ = editor; + emit InfoReady(id, info_data); + emit Finished(id); + +} + +void LastFMArtistBio::Error(const QString &error, const QVariant &debug) { + + qLog(Error) << error; + if (debug.isValid()) qLog(Debug) << debug; + +} diff --git a/src/artistbio/lastfmartistbio.h b/src/artistbio/lastfmartistbio.h new file mode 100644 index 00000000..d8867d22 --- /dev/null +++ b/src/artistbio/lastfmartistbio.h @@ -0,0 +1,66 @@ +/* + * Strawberry Music Player + * Copyright 2020, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef LASTFMARTISTBIO_H +#define LASTFMARTISTBIO_H + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#include "core/song.h" + +#include "artistbioprovider.h" + +class NetworkAccessManager; +class QNetworkReply; + +class LastFMArtistBio : public ArtistBioProvider { + Q_OBJECT + + public: + explicit LastFMArtistBio(); + ~LastFMArtistBio(); + + void Start(const int id, const Song &song) override; + + private: + typedef QPair Param; + typedef QList ParamList; + + QNetworkReply *CreateRequest(const ParamList &request_params); + QByteArray GetReplyData(QNetworkReply *reply); + QJsonObject ExtractJsonObj(const QByteArray &data); + void Error(const QString &error, const QVariant &debug = QVariant()); + + private slots: + void RequestFinished(QNetworkReply *reply, const int id); + + private: + NetworkAccessManager *network_; + QList replies_; + +}; + +#endif // LASTFMARTISTBIO_H diff --git a/src/artistbio/wikipediaartistbio.cpp b/src/artistbio/wikipediaartistbio.cpp new file mode 100644 index 00000000..78fe38f6 --- /dev/null +++ b/src/artistbio/wikipediaartistbio.cpp @@ -0,0 +1,316 @@ +/* + * Strawberry Music Player + * Copyright 2020, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/logging.h" +#include "core/networkaccessmanager.h" +#include "core/iconloader.h" +#include "core/latch.h" + +#include "widgets/infotextview.h" +#include "wikipediaartistbio.h" + +const char *WikipediaArtistBio::kApiUrl = "https://en.wikipedia.org/w/api.php"; +const int WikipediaArtistBio::kMinimumImageSize = 400; + +WikipediaArtistBio::WikipediaArtistBio() : ArtistBioProvider(), network_(new NetworkAccessManager(this)) {} + +WikipediaArtistBio::~WikipediaArtistBio() { + + while (!replies_.isEmpty()) { + QNetworkReply *reply = replies_.takeFirst(); + disconnect(reply, nullptr, this, nullptr); + if (reply->isRunning()) reply->abort(); + reply->deleteLater(); + } + +} + +QNetworkReply *WikipediaArtistBio::CreateRequest(QList ¶ms) { + + params << Param("format", "json"); + params << Param("action", "query"); + + QUrlQuery url_query; + for (const Param ¶m : params) { + url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second)); + } + + QUrl url(kApiUrl); + url.setQuery(url_query); + QNetworkRequest req(url); +#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0) + req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); +#else + req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); +#endif + req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + + QNetworkReply *reply = network_->get(req); + connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(HandleSSLErrors(QList))); + replies_ << reply; + + return reply; + +} + +QByteArray WikipediaArtistBio::GetReplyData(QNetworkReply *reply) { + + QByteArray data; + + if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { + data = reply->readAll(); + } + else { + if (reply->error() == QNetworkReply::NoError) { + qLog(Error) << "Wikipedia artist biography error: Received HTTP code" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + } + else { + qLog(Error) << "Wikipedia artist biography error:" << reply->error() << reply->errorString(); + } + } + + return data; + +} + +QJsonObject WikipediaArtistBio::ExtractJsonObj(const QByteArray &data) { + + if (data.isEmpty()) return QJsonObject(); + + QJsonParseError json_error; + QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); + + if (json_error.error != QJsonParseError::NoError) { + qLog(Error) << "Wikipedia artist biography error: Failed to parse json data:" << json_error.errorString(); + return QJsonObject(); + } + + if (json_doc.isEmpty()) { + qLog(Error) << "Wikipedia artist biography error: Received empty Json document."; + return QJsonObject(); + } + + if (!json_doc.isObject()) { + qLog(Error) << "Wikipedia artist biography error: Json document is not an object."; + return QJsonObject(); + } + + QJsonObject json_obj = json_doc.object(); + if (json_obj.isEmpty()) { + qLog(Error) << "Wikipedia artist biography error: Received empty Json object."; + return QJsonObject(); + } + + return json_obj; + +} + +void WikipediaArtistBio::HandleSSLErrors(QList) {} + +void WikipediaArtistBio::Start(const int id, const Song &metadata) { + + if (metadata.artist().isEmpty()) { + emit Finished(id); + return; + } + + CountdownLatch *latch = new CountdownLatch; + connect(latch, &CountdownLatch::Done, [this, id, latch](){ + latch->deleteLater(); + emit Finished(id); + }); + + GetImageTitles(id, metadata.artist(), latch); + //GetArticle(id, metadata.artist(), latch); + +} + +void WikipediaArtistBio::GetArticle(const int id, const QString &artist, CountdownLatch *latch) { + + latch->Wait(); + + ParamList params = ParamList() << Param("titles", artist) + << Param("prop", "extracts"); + + QNetworkReply *reply = CreateRequest(params); + connect(reply, &QNetworkReply::finished, [this, reply, id, latch]() { GetArticleReply(reply, id, latch); }); + +} + +void WikipediaArtistBio::GetArticleReply(QNetworkReply *reply, const int id, CountdownLatch *latch) { + + reply->deleteLater(); + replies_.removeAll(reply); + + QJsonObject json_obj = ExtractJsonObj(GetReplyData(reply)); + + QString title; + QString text; + if (!json_obj.isEmpty() && json_obj.contains("query") && json_obj["query"].isObject()) { + json_obj = json_obj["query"].toObject(); + if (json_obj.contains("pages") && json_obj["pages"].isObject()) { + QJsonObject value_pages = json_obj["pages"].toObject(); + for (const QJsonValue value_page : value_pages) { + if (!value_page.isObject()) continue; + QJsonObject obj_page = value_page.toObject(); + if (!obj_page.contains("title") || !obj_page.contains("extract")) continue; + title = obj_page["title"].toString(); + text = obj_page["extract"].toString(); + } + } + } + + CollapsibleInfoPane::Data info_data; + info_data.id_ = title; + info_data.title_ = tr("Biography"); + info_data.type_ = CollapsibleInfoPane::Data::Type_Biography; + info_data.icon_ = IconLoader::Load("wikipedia"); + InfoTextView *editor = new InfoTextView; + editor->SetHtml(text); + info_data.contents_ = editor; + emit InfoReady(id, info_data); + + latch->CountDown(); + +} + +void WikipediaArtistBio::GetImageTitles(const int id, const QString &artist, CountdownLatch *latch) { + + latch->Wait(); + + ParamList params = ParamList() << Param("titles", artist) + << Param("prop", "images") + << Param("imlimit", "25"); + + QNetworkReply *reply = CreateRequest(params); + connect(reply, &QNetworkReply::finished, [this, reply, id, latch]() { GetImageTitlesFinished(reply, id, latch); }); + +} + +void WikipediaArtistBio::GetImageTitlesFinished(QNetworkReply *reply, const int id, CountdownLatch *latch) { + + reply->deleteLater(); + replies_.removeAll(reply); + + QJsonObject json_obj = ExtractJsonObj(GetReplyData(reply)); + + QString title; + QStringList titles; + if (!json_obj.isEmpty() && json_obj.contains("query") && json_obj["query"].isObject()) { + json_obj = json_obj["query"].toObject(); + if (json_obj.contains("pages") && json_obj["pages"].isObject()) { + QJsonObject value_pages = json_obj["pages"].toObject(); + for (const QJsonValue value_page : value_pages) { + if (!value_page.isObject()) continue; + QJsonObject obj_page = value_page.toObject(); + if (!obj_page.contains("title") || !obj_page.contains("images") || !obj_page["images"].isArray()) continue; + title = obj_page["title"].toString(); + QJsonArray array_images = obj_page["images"].toArray(); + for (const QJsonValue value_image : array_images) { + if (!value_image.isObject()) continue; + QJsonObject obj_image = value_image.toObject(); + if (!obj_image.contains("title")) continue; + QString filename = obj_image["title"].toString(); + if (filename.endsWith(".jpg", Qt::CaseInsensitive) || filename.endsWith(".png", Qt::CaseInsensitive)) { + titles << filename; + } + } + } + } + } + + for (const QString &image_title : titles) { + GetImage(id, image_title, latch); + } + + latch->CountDown(); + +} + +void WikipediaArtistBio::GetImage(const int id, const QString &title, CountdownLatch *latch) { + + latch->Wait(); + + ParamList params2 = ParamList() << Param("titles", title) + << Param("prop", "imageinfo") + << Param("iiprop", "url|size"); + + QNetworkReply *reply = CreateRequest(params2); + connect(reply, &QNetworkReply::finished, [this, reply, id, latch]() { GetImageFinished(reply, id, latch); }); + +} + +void WikipediaArtistBio::GetImageFinished(QNetworkReply *reply, const int id, CountdownLatch *latch) { + + reply->deleteLater(); + replies_.removeAll(reply); + + QJsonObject json_obj = ExtractJsonObj(GetReplyData(reply)); + + if (!json_obj.isEmpty()) { + QList urls = ExtractImageUrls(json_obj); + for (const QUrl &url : urls) { + emit ImageReady(id, url); + } + } + + latch->CountDown(); + +} + +QList WikipediaArtistBio::ExtractImageUrls(QJsonObject json_obj) { + + QList urls; + if (json_obj.contains("query") && json_obj["query"].isObject()) { + json_obj = json_obj["query"].toObject(); + if (json_obj.contains("pages") && json_obj["pages"].isObject()) { + QJsonObject value_pages = json_obj["pages"].toObject(); + for (const QJsonValue value_page : value_pages) { + if (!value_page.isObject()) continue; + QJsonObject obj_page = value_page.toObject(); + if (!obj_page.contains("title") || !obj_page.contains("imageinfo") || !obj_page["imageinfo"].isArray()) continue; + QJsonArray array_images = obj_page["imageinfo"].toArray(); + for (const QJsonValue value_image : array_images) { + if (!value_image.isObject()) continue; + QJsonObject obj_image = value_image.toObject(); + if (!obj_image.contains("url") || !obj_image.contains("width") || !obj_image.contains("height")) continue; + QUrl url(obj_image["url"].toString()); + const int width = obj_image["width"].toInt(); + const int height = obj_image["height"].toInt(); + if (!url.isValid() || width < kMinimumImageSize || height < kMinimumImageSize) continue; + urls << url; + } + } + } + } + return urls; + +} diff --git a/src/artistbio/wikipediaartistbio.h b/src/artistbio/wikipediaartistbio.h new file mode 100644 index 00000000..e0f9b60b --- /dev/null +++ b/src/artistbio/wikipediaartistbio.h @@ -0,0 +1,72 @@ +/* + * Strawberry Music Player + * Copyright 2020, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef WIKIPEDIAARTISTBIO_H +#define WIKIPEDIAARTISTBIO_H + +#include +#include +#include +#include +#include +#include + +#include "artistbioprovider.h" + +class QNetworkReply; +class CountdownLatch; +class NetworkAccessManager; + +class WikipediaArtistBio : public ArtistBioProvider { + Q_OBJECT + + public: + explicit WikipediaArtistBio(); + ~WikipediaArtistBio(); + + void Start(const int id, const Song &song) override; + + private: + typedef QPair Param; + typedef QList ParamList; + + QNetworkReply *CreateRequest(QList ¶ms); + QByteArray GetReplyData(QNetworkReply *reply); + QJsonObject ExtractJsonObj(const QByteArray &data); + void GetArticle(const int id, const QString &artist, CountdownLatch* latch); + void GetImageTitles(const int id, const QString &artist, CountdownLatch* latch); + void GetImage(const int id, const QString &title, CountdownLatch *latch); + QList ExtractImageUrls(QJsonObject json_obj); + + private slots: + void HandleSSLErrors(QList ssl_errors); + void GetArticleReply(QNetworkReply *reply, const int id, CountdownLatch *latch); + void GetImageTitlesFinished(QNetworkReply *reply, const int id, CountdownLatch *latch); + void GetImageFinished(QNetworkReply *reply, const int id, CountdownLatch *latch); + + private: + static const char *kApiUrl; + static const int kMinimumImageSize; + + NetworkAccessManager *network_; + QList replies_; + +}; + +#endif // WIKIPEDIAARTISTBIO_H diff --git a/src/core/latch.cpp b/src/core/latch.cpp new file mode 100644 index 00000000..ac62762f --- /dev/null +++ b/src/core/latch.cpp @@ -0,0 +1,39 @@ +/* This file is part of Clementine. + Copyright 2016, John Maguire + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include + +#include "latch.h" + +CountdownLatch::CountdownLatch() : count_(0) {} + +void CountdownLatch::Wait() { + + QMutexLocker l(&mutex_); + ++count_; + +} + +void CountdownLatch::CountDown() { + + QMutexLocker l(&mutex_); + Q_ASSERT(count_ > 0); + --count_; + if (count_ == 0) { + emit Done(); + } + +} diff --git a/src/core/latch.h b/src/core/latch.h new file mode 100644 index 00000000..6e90df92 --- /dev/null +++ b/src/core/latch.h @@ -0,0 +1,39 @@ +/* This file is part of Clementine. + Copyright 2016, John Maguire + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#ifndef LATCH_H +#define LATCH_H + +#include +#include + +class CountdownLatch : public QObject { + Q_OBJECT + + public: + explicit CountdownLatch(); + void Wait(); + void CountDown(); + + signals: + void Done(); + + private: + QMutex mutex_; + int count_; +}; + +#endif // LATCH_H diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index 0195f2c0..893764d6 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -204,6 +204,8 @@ #include "smartplaylists/smartplaylistsviewcontainer.h" +#include "artistbio/artistbioview.h" + #ifdef Q_OS_WIN # include "windows7thumbbar.h" #endif @@ -218,6 +220,7 @@ using std::make_unique; using std::make_shared; + using namespace std::chrono_literals; const char *MainWindow::kSettingsGroup = "MainWindow"; @@ -308,6 +311,7 @@ MainWindow::MainWindow(Application *app, SharedPtr tray_icon, OS qobuz_view_(new InternetTabsView(app_, app->internet_services()->ServiceBySource(Song::Source::Qobuz), QobuzSettingsPage::kSettingsGroup, SettingsDialog::Page::Qobuz, this)), #endif radio_view_(new RadioViewContainer(this)), + artistbio_view_(new ArtistBioView(this)), lastfm_import_dialog_(new LastFMImportDialog(app_->lastfm_import(), this)), collection_show_all_(nullptr), collection_show_duplicates_(nullptr), @@ -389,6 +393,7 @@ MainWindow::MainWindow(Application *app, SharedPtr tray_icon, OS #ifdef HAVE_QOBUZ ui_->tabs->AddTab(qobuz_view_, "qobuz", IconLoader::Load("qobuz", true, 0, 32), tr("Qobuz")); #endif + ui_->tabs->AddTab(artistbio_view_, "artistbio", IconLoader::Load("guitar"), tr("Artist biography")); // Add the playing widget to the fancy tab widget ui_->tabs->addBottomWidget(ui_->widget_playing); @@ -907,6 +912,10 @@ MainWindow::MainWindow(Application *app, SharedPtr tray_icon, OS QObject::connect(&*app_->lastfm_import(), &LastFMImport::UpdateTotal, lastfm_import_dialog_, &LastFMImportDialog::UpdateTotal); QObject::connect(&*app_->lastfm_import(), &LastFMImport::UpdateProgress, lastfm_import_dialog_, &LastFMImportDialog::UpdateProgress); + connect(app_->playlist_manager(), SIGNAL(CurrentSongChanged(Song)), artistbio_view_, SLOT(SongChanged(Song))); + connect(app_->player(), SIGNAL(PlaylistFinished()), artistbio_view_, SLOT(SongFinished())); + connect(app_->player(), SIGNAL(Stopped()), artistbio_view_, SLOT(SongFinished())); + // Load settings qLog(Debug) << "Loading settings"; settings_.beginGroup(kSettingsGroup); @@ -1148,6 +1157,16 @@ void MainWindow::ReloadSettings() { album_cover_choice_controller_->search_cover_auto_action()->setChecked(settings_.value("search_for_cover_auto", true).toBool()); + s.beginGroup(BehaviourSettingsPage::kSettingsGroup); + bool artistbio = s.value("artistbio", false).toBool(); + s.endGroup(); + if (artistbio) { + ui_->tabs->EnableTab(artistbio_view_); + } + else { + ui_->tabs->DisableTab(artistbio_view_); + } + #ifdef HAVE_SUBSONIC s.beginGroup(SubsonicSettingsPage::kSettingsGroup); bool enable_subsonic = s.value("enabled", false).toBool(); diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index 6053f92e..8823bb78 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -99,6 +99,7 @@ class Windows7ThumbBar; class AddStreamDialog; class LastFMImportDialog; class RadioViewContainer; +class ArtistBioView; class MainWindow : public QMainWindow, public PlatformInterface { Q_OBJECT @@ -343,6 +344,8 @@ class MainWindow : public QMainWindow, public PlatformInterface { RadioViewContainer *radio_view_; + ArtistBioView* artistbio_view_; + LastFMImportDialog *lastfm_import_dialog_; QAction *collection_show_all_; diff --git a/src/core/metatypes.cpp b/src/core/metatypes.cpp index a9be594c..93c02fcf 100644 --- a/src/core/metatypes.cpp +++ b/src/core/metatypes.cpp @@ -71,6 +71,7 @@ #include "smartplaylists/playlistgenerator_fwd.h" #include "radios/radiochannel.h" +#include "widgets/collapsibleinfopane.h" #ifdef HAVE_LIBMTP # include "device/mtpconnection.h" @@ -153,6 +154,8 @@ void RegisterMetaTypes() { qRegisterMetaType("RadioChannel"); qRegisterMetaType("RadioChannelList"); + qRegisterMetaType("CollapsibleInfoPane::Data"); + #ifdef HAVE_LIBMTP qRegisterMetaType("MtpConnection*"); #endif diff --git a/src/settings/behavioursettingspage.cpp b/src/settings/behavioursettingspage.cpp index bbf5fd92..99369af5 100644 --- a/src/settings/behavioursettingspage.cpp +++ b/src/settings/behavioursettingspage.cpp @@ -160,6 +160,7 @@ void BehaviourSettingsPage::Load() { ui_->checkbox_resumeplayback->setChecked(s.value("resumeplayback", false).toBool()); ui_->checkbox_playingwidget->setChecked(s.value("playing_widget", true).toBool()); + ui_->checkbox_artistbio->setChecked(s.value("artistbio", false).toBool()); #ifndef Q_OS_MACOS const StartupBehaviour startup_behaviour = static_cast(s.value("startupbehaviour", static_cast(StartupBehaviour::Remember)).toInt()); @@ -224,6 +225,7 @@ void BehaviourSettingsPage::Save() { s.setValue("trayicon_progress", ui_->checkbox_trayicon_progress->isChecked()); s.setValue("resumeplayback", ui_->checkbox_resumeplayback->isChecked()); s.setValue("playing_widget", ui_->checkbox_playingwidget->isChecked()); + s.setValue("artistbio", ui_->checkbox_artistbio->isChecked()); StartupBehaviour startup_behaviour = StartupBehaviour::Remember; if (ui_->radiobutton_remember->isChecked()) startup_behaviour = StartupBehaviour::Remember; diff --git a/src/settings/behavioursettingspage.ui b/src/settings/behavioursettingspage.ui index 480af77e..2a1aeda2 100644 --- a/src/settings/behavioursettingspage.ui +++ b/src/settings/behavioursettingspage.ui @@ -58,6 +58,13 @@ + + + + Artist biography + + + diff --git a/src/widgets/collapsibleinfoheader.cpp b/src/widgets/collapsibleinfoheader.cpp new file mode 100644 index 00000000..f141d244 --- /dev/null +++ b/src/widgets/collapsibleinfoheader.cpp @@ -0,0 +1,174 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "collapsibleinfoheader.h" + +const int CollapsibleInfoHeader::kHeight = 20; +const int CollapsibleInfoHeader::kIconSize = 16; + +CollapsibleInfoHeader::CollapsibleInfoHeader(QWidget* parent) + : QWidget(parent), + expanded_(false), + hovering_(false), + animation_(new QPropertyAnimation(this, "opacity", this)), + opacity_(0.0) { + + setMinimumHeight(kHeight); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + setCursor(QCursor(Qt::PointingHandCursor)); + +} + +void CollapsibleInfoHeader::SetTitle(const QString &title) { + title_ = title; + update(); +} + +void CollapsibleInfoHeader::SetIcon(const QIcon &icon) { + icon_ = icon; + update(); +} + +void CollapsibleInfoHeader::SetExpanded(const bool expanded) { + + expanded_ = expanded; + + emit ExpandedToggled(expanded); + if (expanded) + emit Expanded(); + else + emit Collapsed(); +} + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +void CollapsibleInfoHeader::enterEvent(QEnterEvent*) { +#else +void CollapsibleInfoHeader::enterEvent(QEvent*) { +#endif + + hovering_ = true; + if (!expanded_) { + animation_->stop(); + animation_->setEndValue(1.0); + animation_->setDuration(80); + animation_->start(); + } + +} + +void CollapsibleInfoHeader::leaveEvent(QEvent*) { + + hovering_ = false; + if (!expanded_) { + animation_->stop(); + animation_->setEndValue(0.0); + animation_->setDuration(160); + animation_->start(); + } + +} + +void CollapsibleInfoHeader::set_opacity(const float opacity) { + opacity_ = opacity; + update(); +} + +void CollapsibleInfoHeader::paintEvent(QPaintEvent*) { + + QPainter p(this); + + QColor active_text_color(palette().color(QPalette::Active, QPalette::HighlightedText)); + QColor inactive_text_color(palette().color(QPalette::Active, QPalette::Text)); + QColor text_color; + if (expanded_) { + text_color = active_text_color; + } + else { + p.setOpacity(0.4 + opacity_ * 0.6); + text_color = QColor(active_text_color.red() * opacity_ + inactive_text_color.red() * (1.0 - opacity_), active_text_color.green() * opacity_ + inactive_text_color.green() * (1.0 - opacity_), active_text_color.blue() * opacity_ + inactive_text_color.blue() * (1.0 - opacity_)); + } + + QRect indicator_rect(0, 0, height(), height()); + QRect icon_rect(height() + 2, (kHeight - kIconSize) / 2, kIconSize, kIconSize); + QRect text_rect(rect()); + text_rect.setLeft(icon_rect.right() + 4); + + // Draw the background + QColor highlight(palette().color(QPalette::Active, QPalette::Highlight)); + const QColor bg_color_1(highlight.lighter(120)); + const QColor bg_color_2(highlight.darker(120)); + const QColor bg_border(palette().color(QPalette::Dark)); + QLinearGradient bg_brush(rect().topLeft(), rect().bottomLeft()); + bg_brush.setColorAt(0.0, bg_color_1); + bg_brush.setColorAt(0.5, bg_color_1); + bg_brush.setColorAt(0.5, bg_color_2); + bg_brush.setColorAt(1.0, bg_color_2); + + p.setPen(Qt::NoPen); + p.fillRect(rect(), bg_brush); + + p.setPen(bg_border); + p.drawLine(rect().topLeft(), rect().topRight()); + p.drawLine(rect().bottomLeft(), rect().bottomRight()); + + // Draw the expand/collapse indicator + QStyleOption opt; + opt.initFrom(this); + opt.rect = indicator_rect; + opt.state |= QStyle::State_Children; + if (expanded_) opt.state |= QStyle::State_Open; + if (hovering_) opt.state |= QStyle::State_Active; + + // Have to use the application's style here because using the widget's style + // will trigger QStyleSheetStyle's recursion guard (I don't know why). + QApplication::style()->drawPrimitive(QStyle::PE_IndicatorBranch, &opt, &p, this); + + // Draw the icon + p.drawPixmap(icon_rect, icon_.pixmap(kIconSize)); + + // Draw the title text + QFont bold_font(font()); + bold_font.setBold(true); + p.setFont(bold_font); + + p.setPen(text_color); + p.drawText(text_rect, Qt::AlignLeft | Qt::AlignVCenter, title_); + +} + +void CollapsibleInfoHeader::mouseReleaseEvent(QMouseEvent*) { + SetExpanded(!expanded_); +} diff --git a/src/widgets/collapsibleinfoheader.h b/src/widgets/collapsibleinfoheader.h new file mode 100644 index 00000000..e421f149 --- /dev/null +++ b/src/widgets/collapsibleinfoheader.h @@ -0,0 +1,82 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef COLLAPSIBLEINFOHEADER_H +#define COLLAPSIBLEINFOHEADER_H + +#include +#include +#include + +class QPropertyAnimation; +class QEnterEvent; +class QEvent; +class QPaintEvent; +class QMouseEvent; + +class CollapsibleInfoHeader : public QWidget { + Q_OBJECT + Q_PROPERTY(float opacity READ opacity WRITE set_opacity) + + public: + CollapsibleInfoHeader(QWidget *parent = nullptr); + + static const int kHeight; + static const int kIconSize; + + bool expanded() const { return expanded_; } + bool hovering() const { return hovering_; } + const QString &title() const { return title_; } + const QIcon &icon() const { return icon_; } + + float opacity() const { return opacity_; } + void set_opacity(const float opacity); + + public slots: + void SetExpanded(const bool expanded); + void SetTitle(const QString &title); + void SetIcon(const QIcon &icon); + + signals: + void Expanded(); + void Collapsed(); + void ExpandedToggled(const bool expanded); + + protected: +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + void enterEvent(QEnterEvent*) override; +#else + void enterEvent(QEvent*) override; +#endif + void leaveEvent(QEvent*) override; + void paintEvent(QPaintEvent*) override; + void mouseReleaseEvent(QMouseEvent*) override; + + private: + bool expanded_; + bool hovering_; + QString title_; + QIcon icon_; + + QPropertyAnimation *animation_; + float opacity_; +}; + +#endif // COLLAPSIBLEINFOHEADER_H diff --git a/src/widgets/collapsibleinfopane.cpp b/src/widgets/collapsibleinfopane.cpp new file mode 100644 index 00000000..8fce0fbb --- /dev/null +++ b/src/widgets/collapsibleinfopane.cpp @@ -0,0 +1,64 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "collapsibleinfoheader.h" +#include "collapsibleinfopane.h" + +#include + +CollapsibleInfoPane::CollapsibleInfoPane(const Data &data, QWidget *parent) + : QWidget(parent), data_(data), header_(new CollapsibleInfoHeader(this)) { + + QVBoxLayout* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(3); + layout->setSizeConstraint(QLayout::SetMinAndMaxSize); + setLayout(layout); + + layout->addWidget(header_); + layout->addWidget(data.contents_); + data.contents_->hide(); + + header_->SetTitle(data.title_); + header_->SetIcon(data.icon_); + + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); + + connect(header_, SIGNAL(ExpandedToggled(bool)), SLOT(ExpandedToggled(bool))); + connect(header_, SIGNAL(ExpandedToggled(bool)), SIGNAL(Toggled(bool))); + +} + +void CollapsibleInfoPane::Collapse() { header_->SetExpanded(false); } + +void CollapsibleInfoPane::Expand() { header_->SetExpanded(true); } + +void CollapsibleInfoPane::ExpandedToggled(bool expanded) { + data_.contents_->setVisible(expanded); +} + +bool CollapsibleInfoPane::Data::operator<(const CollapsibleInfoPane::Data &other) const { + + const int my_score = (TypeCount - type_) * 1000 + relevance_; + const int other_score = (TypeCount - other.type_) * 1000 + other.relevance_; + + return my_score > other_score; + +} diff --git a/src/widgets/collapsibleinfopane.h b/src/widgets/collapsibleinfopane.h new file mode 100644 index 00000000..a5e8e1ab --- /dev/null +++ b/src/widgets/collapsibleinfopane.h @@ -0,0 +1,72 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef COLLAPSIBLEINFOPANE_H +#define COLLAPSIBLEINFOPANE_H + +#include +#include + +class CollapsibleInfoHeader; + +class CollapsibleInfoPane : public QWidget { + Q_OBJECT + + public: + struct Data { + explicit Data() : type_(Type_Biography), relevance_(0) {} + + bool operator<(const Data& other) const; + + enum Type { + Type_Biography, + TypeCount + }; + + QString id_; + QString title_; + QIcon icon_; + Type type_; + int relevance_; + + QWidget *contents_; + QObject *content_object_; + }; + + CollapsibleInfoPane(const Data &data, QWidget* parent = nullptr); + + const Data &data() const { return data_; } + + public slots: + void Expand(); + void Collapse(); + + signals: + void Toggled(const bool expanded); + + private slots: + void ExpandedToggled(const bool expanded); + + private: + Data data_; + CollapsibleInfoHeader *header_; +}; + +#endif // COLLAPSIBLEINFOPANE_H diff --git a/src/widgets/infotextview.cpp b/src/widgets/infotextview.cpp new file mode 100644 index 00000000..d75d5d0b --- /dev/null +++ b/src/widgets/infotextview.cpp @@ -0,0 +1,89 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include +#include +#include +#include +#include +#include + +#include "core/logging.h" +#include "infotextview.h" + +InfoTextView::InfoTextView(QWidget *parent) : QTextBrowser(parent), last_width_(-1), recursion_filter_(false) { + + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setOpenExternalLinks(true); + +} + +void InfoTextView::resizeEvent(QResizeEvent *e) { + + const int w = qMax(100, width()); + if (w == last_width_) return; + last_width_ = w; + + document()->setTextWidth(w); + setMinimumHeight(document()->size().height()); + + QTextBrowser::resizeEvent(e); + +} + +QSize InfoTextView::sizeHint() const { return minimumSize(); } + +void InfoTextView::wheelEvent(QWheelEvent *e) { e->ignore(); } + +void InfoTextView::SetHtml(const QString &html) { + + QString copy(html.trimmed()); + + // Simplify newlines + copy.replace(QRegularExpression("\\r\\n?"), "\n"); + + // Convert two or more newlines to

, convert single newlines to
+ copy.replace(QRegularExpression("([^>])([\\t ]*\\n){2,}"), "\\1

"); + copy.replace(QRegularExpression("([^>])[\\t ]*\\n"), "\\1
"); + + // Strip any newlines from the end + copy.replace(QRegularExpression("((<\\s*br\\s*/?\\s*>)|(<\\s*/?\\s*p\\s*/?\\s*>))+$"), ""); + + setHtml(copy); + +} + +// Prevents QTextDocument from trying to load remote images before they are ready. +QVariant InfoTextView::loadResource(int type, const QUrl &name) { + + if (recursion_filter_) { + recursion_filter_ = false; + return QVariant(); + } + recursion_filter_ = true; + if (type == QTextDocument::ImageResource && name.scheme() == "http") { + if (document()->resource(type, name).isNull()) { + return QVariant(); + } + } + return QTextBrowser::loadResource(type, name); + +} diff --git a/src/widgets/infotextview.h b/src/widgets/infotextview.h new file mode 100644 index 00000000..da91543d --- /dev/null +++ b/src/widgets/infotextview.h @@ -0,0 +1,52 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef INFOTEXTVIEW_H +#define INFOTEXTVIEW_H + +#include +#include +#include + +class QResizeEvent; +class QWheelEvent; + +class InfoTextView : public QTextBrowser { + Q_OBJECT + + public: + explicit InfoTextView(QWidget *parent = nullptr); + + QSize sizeHint() const override; + + public slots: + void SetHtml(const QString &html); + + protected: + void resizeEvent(QResizeEvent *e) override; + void wheelEvent(QWheelEvent *e) override; + QVariant loadResource(int type, const QUrl &name) override; + + private: + int last_width_; + bool recursion_filter_; +}; + +#endif // INFOTEXTVIEW_H diff --git a/src/widgets/prettyimage.cpp b/src/widgets/prettyimage.cpp new file mode 100644 index 00000000..24edc47f --- /dev/null +++ b/src/widgets/prettyimage.cpp @@ -0,0 +1,257 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/logging.h" +#include "core/networkaccessmanager.h" +#include "core/iconloader.h" + +#include "prettyimage.h" + +const int PrettyImage::kTotalHeight = 200; +const int PrettyImage::kReflectionHeight = 40; +const int PrettyImage::kImageHeight = PrettyImage::kTotalHeight - PrettyImage::kReflectionHeight; + +const int PrettyImage::kMaxImageWidth = 300; + +const char *PrettyImage::kSettingsGroup = "PrettyImageView"; + +PrettyImage::PrettyImage(const QUrl &url, QNetworkAccessManager *network, QWidget *parent) + : QWidget(parent), + network_(network), + state_(State_WaitingForLazyLoad), + url_(url), + menu_(nullptr) { + + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + LazyLoad(); + +} + +void PrettyImage::LazyLoad() { + + if (state_ != State_WaitingForLazyLoad) return; + + // Start fetching the image + QNetworkReply *reply = network_->get(QNetworkRequest(url_)); + state_ = State_Fetching; + QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { ImageFetched(reply); }); + +} + +QSize PrettyImage::image_size() const { + + if (state_ != State_Finished) return QSize(kImageHeight * 1.6, kImageHeight); + + QSize ret = image_.size(); + ret.scale(kMaxImageWidth, kImageHeight, Qt::KeepAspectRatio); + return ret; + +} + +QSize PrettyImage::sizeHint() const { + return QSize(image_size().width(), kTotalHeight); +} + +void PrettyImage::ImageFetched(QNetworkReply *reply) { + + reply->deleteLater(); + + QImage image = QImage::fromData(reply->readAll()); + if (image.isNull()) { + qLog(Debug) << "Image failed to load" << reply->request().url() << reply->error(); + deleteLater(); + } + else { + state_ = State_CreatingThumbnail; + image_ = image; + (void)QtConcurrent::run([=]{ ImageScaled(image_.scaled(image_size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); }); + } + +} + +void PrettyImage::ImageScaled(QImage image) { + + thumbnail_ = QPixmap::fromImage(image); + state_ = State_Finished; + + updateGeometry(); + update(); + emit Loaded(); + +} + +void PrettyImage::paintEvent(QPaintEvent*) { + + // Draw at the bottom of our area + QRect image_rect(QPoint(0, 0), image_size()); + image_rect.moveBottom(kImageHeight); + + QPainter p(this); + + // Draw the main image + DrawThumbnail(&p, image_rect); + + // Draw the reflection + // Figure out where to draw it + QRect reflection_rect(image_rect); + reflection_rect.moveTop(image_rect.bottom()); + + // Create the reflected pixmap + QImage reflection(reflection_rect.size(), QImage::Format_ARGB32_Premultiplied); + reflection.fill(palette().color(QPalette::Base).rgba()); + QPainter reflection_painter(&reflection); + + // Set up the transformation + QTransform transform; + transform.scale(1.0, -1.0); + transform.translate(0.0, -reflection_rect.height()); + reflection_painter.setTransform(transform); + + QRect fade_rect(reflection.rect().bottomLeft() - QPoint(0, kReflectionHeight), reflection.rect().bottomRight()); + + // Draw the reflection into the buffer + DrawThumbnail(&reflection_painter, reflection.rect()); + + // Make it fade out towards the bottom + QLinearGradient fade_gradient(fade_rect.topLeft(), fade_rect.bottomLeft()); + fade_gradient.setColorAt(0.0, QColor(0, 0, 0, 0)); + fade_gradient.setColorAt(1.0, QColor(0, 0, 0, 128)); + + reflection_painter.setCompositionMode(QPainter::CompositionMode_DestinationIn); + reflection_painter.fillRect(fade_rect, fade_gradient); + + reflection_painter.end(); + + // Draw the reflection on the image + p.drawImage(reflection_rect, reflection); + +} + +void PrettyImage::DrawThumbnail(QPainter *p, const QRect &rect) { + + switch (state_) { + case State_WaitingForLazyLoad: + case State_Fetching: + case State_CreatingThumbnail: + p->setPen(palette().color(QPalette::Disabled, QPalette::Text)); + p->drawText(rect, Qt::AlignHCenter | Qt::AlignBottom, tr("Loading...")); + break; + + case State_Finished: + p->drawPixmap(rect, thumbnail_); + break; + } + +} + +void PrettyImage::contextMenuEvent(QContextMenuEvent *e) { + + if (e->pos().y() >= kImageHeight) return; + + if (!menu_) { + menu_ = new QMenu(this); + menu_->addAction(IconLoader::Load("zoom-in"), tr("Show fullsize..."), this, SLOT(ShowFullsize())); + menu_->addAction(IconLoader::Load("document-save"), tr("Save image") + "...", this, SLOT(SaveAs())); + } + + menu_->popup(e->globalPos()); + +} + +void PrettyImage::ShowFullsize() { + + // Create the window + QScrollArea *pwindow = new QScrollArea; + pwindow->setAttribute(Qt::WA_DeleteOnClose, true); + pwindow->setWindowTitle(tr("%1 image viewer").arg("Strawberry")); + + // Work out how large to make the window, based on the size of the screen +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + QScreen *screen = QWidget::screen(); +#else + QScreen *screen = (window() && window()->windowHandle() ? window()->windowHandle()->screen() : QGuiApplication::primaryScreen()); +#endif + if (screen) { + QRect desktop_rect(screen->availableGeometry()); + QSize window_size(qMin(desktop_rect.width() - 20, image_.width()), qMin(desktop_rect.height() - 20, image_.height())); + pwindow->resize(window_size); + } + + // Create the label that displays the image + QLabel *label = new QLabel(pwindow); + label->setPixmap(QPixmap::fromImage(image_)); + + // Show the label in the window + pwindow->setWidget(label); + pwindow->setFrameShape(QFrame::NoFrame); + pwindow->show(); + +} + +void PrettyImage::SaveAs() { + + QString filename = QFileInfo(url_.path()).fileName(); + + if (filename.isEmpty()) filename = "artwork.jpg"; + + QSettings s; + s.beginGroup(kSettingsGroup); + QString last_save_dir = s.value("last_save_dir", QDir::homePath()).toString(); + + QString path = last_save_dir.isEmpty() ? QDir::homePath() : last_save_dir; + QFileInfo path_info(path); + if (path_info.isDir()) { + path += "/" + filename; + } + else { + path = path_info.path() + "/" + filename; + } + + filename = QFileDialog::getSaveFileName(this, tr("Save image"), path); + if (filename.isEmpty()) return; + + image_.save(filename); + + s.setValue("last_save_dir", last_save_dir); + + s.endGroup(); + +} diff --git a/src/widgets/prettyimage.h b/src/widgets/prettyimage.h new file mode 100644 index 00000000..ee0f4d06 --- /dev/null +++ b/src/widgets/prettyimage.h @@ -0,0 +1,92 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef PRETTYIMAGE_H +#define PRETTYIMAGE_H + +#include +#include +#include +#include +#include +#include + +class QMenu; +class QNetworkAccessManager; +class QNetworkReply; +class QContextMenuEvent; +class QPaintEvent; + +class PrettyImage : public QWidget { + Q_OBJECT + + public: + PrettyImage(const QUrl &url, QNetworkAccessManager *network, QWidget *parent = nullptr); + + static const int kTotalHeight; + static const int kReflectionHeight; + static const int kImageHeight; + + static const int kMaxImageWidth; + + static const char *kSettingsGroup; + + QSize sizeHint() const override; + QSize image_size() const; + +signals: + void Loaded(); + + public slots: + void LazyLoad(); + void SaveAs(); + void ShowFullsize(); + + protected: + void contextMenuEvent(QContextMenuEvent*) override; + void paintEvent(QPaintEvent*) override; + + private slots: + void ImageFetched(QNetworkReply *reply); + void ImageScaled(QImage image); + + private: + enum State { + State_WaitingForLazyLoad, + State_Fetching, + State_CreatingThumbnail, + State_Finished, + }; + + void DrawThumbnail(QPainter *p, const QRect &rect); + + private: + QNetworkAccessManager *network_; + State state_; + QUrl url_; + + QImage image_; + QPixmap thumbnail_; + + QMenu *menu_; + QString last_save_dir_; +}; + +#endif // PRETTYIMAGE_H diff --git a/src/widgets/prettyimageview.cpp b/src/widgets/prettyimageview.cpp new file mode 100644 index 00000000..e59e2139 --- /dev/null +++ b/src/widgets/prettyimageview.cpp @@ -0,0 +1,189 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/networkaccessmanager.h" + +#include "prettyimage.h" +#include "prettyimageview.h" + +PrettyImageView::PrettyImageView(QNetworkAccessManager *network, QWidget* parent) + : QScrollArea(parent), + network_(network), + container_(new QWidget(this)), + layout_(new QHBoxLayout(container_)), + current_index_(-1), + scroll_animation_(new QPropertyAnimation(horizontalScrollBar(), "value", this)), + recursion_filter_(false) { + + setWidget(container_); + setWidgetResizable(true); + setMinimumHeight(PrettyImage::kTotalHeight + 10); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + setFrameShape(QFrame::NoFrame); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + scroll_animation_->setDuration(250); + scroll_animation_->setEasingCurve(QEasingCurve::InOutCubic); + connect(horizontalScrollBar(), SIGNAL(sliderReleased()), SLOT(ScrollBarReleased())); + connect(horizontalScrollBar(), SIGNAL(actionTriggered(int)), SLOT(ScrollBarAction(int))); + + layout_->setSizeConstraint(QLayout::SetMinAndMaxSize); + layout_->setContentsMargins(6, 6, 6, 6); + layout_->setSpacing(6); + layout_->addSpacing(200); + layout_->addSpacing(200); + + container_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); + +} + +bool PrettyImageView::eventFilter(QObject *obj, QEvent *event) { + + // Work around infinite recursion in QScrollArea resizes. + if (recursion_filter_) { + return false; + } + recursion_filter_ = true; + bool ret = QScrollArea::eventFilter(obj, event); + recursion_filter_ = false; + return ret; + +} + +void PrettyImageView::AddImage(const QUrl &url) { + + PrettyImage *image = new PrettyImage(url, network_, container_); + connect(image, SIGNAL(destroyed()), SLOT(ScrollToCurrent())); + connect(image, SIGNAL(Loaded()), SLOT(ScrollToCurrent())); + + layout_->insertWidget(layout_->count() - 1, image); + if (current_index_ == -1) ScrollTo(0); + +} + +void PrettyImageView::mouseReleaseEvent(QMouseEvent *e) { + + // Find the image that was clicked on + QWidget *widget = container_->childAt(container_->mapFrom(this, e->pos())); + if (!widget) return; + + // Get the index of that image + const int index = layout_->indexOf(widget) - 1; + if (index == -1) return; + + if (index == current_index_) { + // Show the image fullsize + PrettyImage* pretty_image = qobject_cast(widget); + if (pretty_image) { + pretty_image->ShowFullsize(); + } + } + else { + // Scroll to the image + ScrollTo(index); + } + +} + +void PrettyImageView::ScrollTo(const int index, const bool smooth) { + + current_index_ = qBound(0, index, layout_->count() - 3); + const int layout_index = current_index_ + 1; + + const QWidget *target_widget = layout_->itemAt(layout_index)->widget(); + if (!target_widget) return; + + const int current_x = horizontalScrollBar()->value(); + const int target_x = target_widget->geometry().center().x() - width() / 2; + + if (current_x == target_x) return; + + if (smooth) { + scroll_animation_->setStartValue(current_x); + scroll_animation_->setEndValue(target_x); + scroll_animation_->start(); + } + else { + scroll_animation_->stop(); + horizontalScrollBar()->setValue(target_x); + } + +} + +void PrettyImageView::ScrollToCurrent() { ScrollTo(current_index_); } + +void PrettyImageView::ScrollBarReleased() { + + // Find the nearest widget to where the scroll bar was released + const int current_x = horizontalScrollBar()->value() + width() / 2; + int layout_index = 1; + for (; layout_index < layout_->count() - 1; ++layout_index) { + const QWidget *widget = layout_->itemAt(layout_index)->widget(); + if (widget && widget->geometry().right() > current_x) { + break; + } + } + + ScrollTo(layout_index - 1); + +} + +void PrettyImageView::ScrollBarAction(const int action) { + + switch (action) { + case QAbstractSlider::SliderSingleStepAdd: + case QAbstractSlider::SliderPageStepAdd: + ScrollTo(current_index_ + 1); + break; + + case QAbstractSlider::SliderSingleStepSub: + case QAbstractSlider::SliderPageStepSub: + ScrollTo(current_index_ - 1); + break; + } + +} + +void PrettyImageView::resizeEvent(QResizeEvent *e) { + + QScrollArea::resizeEvent(e); + ScrollTo(current_index_, false); + +} + +void PrettyImageView::wheelEvent(QWheelEvent *e) { + + const int d = e->angleDelta().x() > 0 ? -1 : 1; + ScrollTo(current_index_ + d, true); + +} diff --git a/src/widgets/prettyimageview.h b/src/widgets/prettyimageview.h new file mode 100644 index 00000000..77e702b2 --- /dev/null +++ b/src/widgets/prettyimageview.h @@ -0,0 +1,74 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef PRETTYIMAGEVIEW_H +#define PRETTYIMAGEVIEW_H + +#include +#include +#include + +class QNetworkAccessManager; +class QNetworkReply; +class QMenu; +class QHBoxLayout; +class QPropertyAnimation; +class QTimeLine; +class QMouseEvent; +class QResizeEvent; +class QWheelEvent; + +class PrettyImageView : public QScrollArea { + Q_OBJECT + + public: + PrettyImageView(QNetworkAccessManager *network, QWidget *parent = nullptr); + + static const char* kSettingsGroup; + + public slots: + void AddImage(const QUrl& url); + + protected: + void mouseReleaseEvent(QMouseEvent*) override; + void resizeEvent(QResizeEvent *e) override; + void wheelEvent(QWheelEvent *e) override; + + private slots: + void ScrollBarReleased(); + void ScrollBarAction(const int action); + void ScrollTo(const int index, const bool smooth = true); + void ScrollToCurrent(); + + private: + bool eventFilter(QObject*, QEvent*) override; + + QNetworkAccessManager *network_; + + QWidget *container_; + QHBoxLayout *layout_; + + int current_index_; + QPropertyAnimation *scroll_animation_; + + bool recursion_filter_; +}; + +#endif // PRETTYIMAGEVIEW_H diff --git a/src/widgets/widgetfadehelper.cpp b/src/widgets/widgetfadehelper.cpp new file mode 100644 index 00000000..612a3bd7 --- /dev/null +++ b/src/widgets/widgetfadehelper.cpp @@ -0,0 +1,185 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "widgetfadehelper.h" +#include "core/qt_blurimage.h" + +const int WidgetFadeHelper::kLoadingPadding = 9; +const int WidgetFadeHelper::kLoadingBorderRadius = 10; + +WidgetFadeHelper::WidgetFadeHelper(QWidget *parent, const int msec) + : QWidget(parent), + parent_(parent), + blur_timeline_(new QTimeLine(msec, this)), + fade_timeline_(new QTimeLine(msec, this)) { + + parent->installEventFilter(this); + + connect(blur_timeline_, SIGNAL(valueChanged(qreal)), SLOT(update())); + connect(fade_timeline_, SIGNAL(valueChanged(qreal)), SLOT(update())); + connect(fade_timeline_, SIGNAL(finished()), SLOT(FadeFinished())); + + hide(); + +} + +bool WidgetFadeHelper::eventFilter(QObject *obj, QEvent *event) { + + // We're only interested in our parent's resize events + if (obj != parent_ || event->type() != QEvent::Resize) return false; + + // Don't care if we're hidden + if (!isVisible()) return false; + + QResizeEvent *re = static_cast(event); + if (re->oldSize() == re->size()) { + // Ignore phoney resize events + return false; + } + + // Get a new capture of the parent + hide(); + CaptureParent(); + show(); + return false; + +} + +void WidgetFadeHelper::StartBlur() { + + CaptureParent(); + + // Cover the parent + raise(); + show(); + + // Start the timeline + blur_timeline_->stop(); + blur_timeline_->start(); + + setAttribute(Qt::WA_TransparentForMouseEvents, false); + +} + +void WidgetFadeHelper::CaptureParent() { + + // Take a "screenshot" of the window + original_pixmap_ = parent_->grab(); + QImage original_image = original_pixmap_.toImage(); + + // Blur it + QImage blurred(original_image.size(), QImage::Format_ARGB32_Premultiplied); + blurred.fill(Qt::transparent); + + QPainter blur_painter(&blurred); + blur_painter.save(); + qt_blurImage(&blur_painter, original_image, 10.0, true, false); + blur_painter.restore(); + + // Draw some loading text over the top + QFont loading_font(font()); + loading_font.setBold(true); + QFontMetrics loading_font_metrics(loading_font); + + const QString loading_text = tr("Loading..."); +#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)) + const QSize loading_size(kLoadingPadding * 2 + loading_font_metrics.horizontalAdvance(loading_text), kLoadingPadding * 2 + loading_font_metrics.height()); +#else + const QSize loading_size(kLoadingPadding * 2 + loading_font_metrics.width(loading_text), kLoadingPadding * 2 + loading_font_metrics.height()); +#endif + const QRect loading_rect((blurred.width() - loading_size.width()) / 2, 100, loading_size.width(), loading_size.height()); + + blur_painter.setRenderHint(QPainter::Antialiasing); + + blur_painter.translate(0.5, 0.5); + blur_painter.setPen(QColor(200, 200, 200, 255)); + blur_painter.setBrush(QColor(200, 200, 200, 192)); + blur_painter.drawRoundedRect(loading_rect, kLoadingBorderRadius, kLoadingBorderRadius); + + blur_painter.setPen(palette().brush(QPalette::Text).color()); + blur_painter.setFont(loading_font); + blur_painter.drawText(loading_rect.translated(-1, -1), Qt::AlignCenter, loading_text); + blur_painter.translate(-0.5, -0.5); + + blur_painter.end(); + + blurred_pixmap_ = QPixmap::fromImage(blurred); + + resize(parent_->size()); + +} + +void WidgetFadeHelper::StartFade() { + + if (blur_timeline_->state() == QTimeLine::Running) { + // Blur timeline is still running, so we need render the current state + // into a new pixmap. + QPixmap pixmap(original_pixmap_); + QPainter painter(&pixmap); + painter.setOpacity(blur_timeline_->currentValue()); + painter.drawPixmap(0, 0, blurred_pixmap_); + painter.end(); + blurred_pixmap_ = pixmap; + } + blur_timeline_->stop(); + original_pixmap_ = QPixmap(); + + // Start the timeline + fade_timeline_->stop(); + fade_timeline_->start(); + + setAttribute(Qt::WA_TransparentForMouseEvents, true); + +} + +void WidgetFadeHelper::paintEvent(QPaintEvent*) { + + QPainter p(this); + + if (fade_timeline_->state() != QTimeLine::Running) { + // We're fading in the blur + p.drawPixmap(0, 0, original_pixmap_); + p.setOpacity(blur_timeline_->currentValue()); + } + else { + // Fading out the blur into the new image + p.setOpacity(1.0 - fade_timeline_->currentValue()); + } + + p.drawPixmap(0, 0, blurred_pixmap_); + +} + +void WidgetFadeHelper::FadeFinished() { + + hide(); + original_pixmap_ = QPixmap(); + blurred_pixmap_ = QPixmap(); + +} diff --git a/src/widgets/widgetfadehelper.h b/src/widgets/widgetfadehelper.h new file mode 100644 index 00000000..879ced40 --- /dev/null +++ b/src/widgets/widgetfadehelper.h @@ -0,0 +1,63 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef WIDGETFADEHELPER_H +#define WIDGETFADEHELPER_H + +#include +#include + +class QTimeLine; +class QPaintEvent; +class QEvent; + +class WidgetFadeHelper : public QWidget { + Q_OBJECT + + public: + WidgetFadeHelper(QWidget *parent, const int msec = 500); + + public slots: + void StartBlur(); + void StartFade(); + + protected: + void paintEvent(QPaintEvent*) override; + bool eventFilter(QObject *obj, QEvent *event) override; + + private slots: + void FadeFinished(); + + private: + void CaptureParent(); + + private: + static const int kLoadingPadding; + static const int kLoadingBorderRadius; + + QWidget* parent_; + QTimeLine* blur_timeline_; + QTimeLine* fade_timeline_; + + QPixmap original_pixmap_; + QPixmap blurred_pixmap_; +}; + +#endif // WIDGETFADEHELPER_H