diff --git a/src/enum/column.hpp b/src/enum/column.hpp index b83bcaaa..539e0456 100644 --- a/src/enum/column.hpp +++ b/src/enum/column.hpp @@ -8,4 +8,5 @@ enum class Column: int Album = 3, Length = 4, Added = 5, + Liked = 6, }; diff --git a/src/enum/datarole.hpp b/src/enum/datarole.hpp index 3f9c2384..ff83e424 100644 --- a/src/enum/datarole.hpp +++ b/src/enum/datarole.hpp @@ -11,4 +11,5 @@ enum class DataRole: int Length = 0x106, // 262 DefaultIndex = 0x107, // 263 ShowId = 0x108, // 264 + Liked = 0x109, // 265 }; diff --git a/src/list/tracks.cpp b/src/list/tracks.cpp index c9e8f410..c43e9945 100644 --- a/src/list/tracks.cpp +++ b/src/list/tracks.cpp @@ -28,8 +28,15 @@ List::Tracks::Tracks(lib::spt::api &spotify, lib::settings &settings, lib::cache setAllColumnsShowFocus(true); setColumnCount(columnCount); setHeaderLabels({ - settings.general.track_numbers == lib::spotify_context::all ? "#" : "", - "Title", "Artist", "Album", "Length", "Added" + settings.general.track_numbers == lib::spotify_context::all + ? QStringLiteral("#") + : QString(), + QStringLiteral("Title"), + QStringLiteral("Artist"), + QStringLiteral("Album"), + QStringLiteral("Length"), + QStringLiteral("Added"), + QStringLiteral("Liked"), }); header()->setSectionsMovable(false); header()->setSortIndicator(settings.general.song_header_sort_by + 1, Qt::AscendingOrder); @@ -42,6 +49,10 @@ List::Tracks::Tracks(lib::spt::api &spotify, lib::settings &settings, lib::cache header()->setSectionHidden(value + 1, true); } + // Clicking icons + QTreeWidget::connect(this, &QTreeWidget::itemClicked, + this, &List::Tracks::onItemClicked); + // Play tracks on click or enter/special key QTreeWidget::connect(this, &QTreeWidget::itemDoubleClicked, this, &List::Tracks::onDoubleClicked); @@ -56,6 +67,10 @@ List::Tracks::Tracks(lib::spt::api &spotify, lib::settings &settings, lib::cache QLabel::connect(header(), &QWidget::customContextMenuRequested, this, &List::Tracks::onHeaderMenu); + // Selecting track + QTreeWidget::connect(this, &QTreeWidget::currentItemChanged, + this, &List::Tracks::onCurrentItemChanged); + QShortcut::connect(new QShortcut(Shortcut::newPlaylist(), this), &QShortcut::activated, this, &List::Tracks::onNewPlaylist); @@ -102,6 +117,38 @@ void List::Tracks::onMenu(const QPoint &pos) songMenu->popup(mapToGlobal(pos)); } +void List::Tracks::onItemClicked(QTreeWidgetItem *item, int column) +{ + if (column == static_cast(Column::Liked)) + { + const auto &trackData = item->data(0, static_cast(DataRole::Track)); + const auto &track = trackData.value(); + + const auto &likedData = item->data(0, static_cast(DataRole::Liked)); + const auto &isLiked = likedData.toBool(); + + const auto callback = [item, isLiked, column](const std::string &response) + { + if (response.empty()) + { + item->setData(0, static_cast(DataRole::Liked), !isLiked); + item->setIcon(column, Icon::get(isLiked + ? QStringLiteral("non-starred-symbolic") + : QStringLiteral("starred-symbolic"))); + } + }; + + if (isLiked) + { + spotify.remove_saved_tracks({track.id}, callback); + } + else + { + spotify.add_saved_tracks({track.id}, callback); + } + } +} + void List::Tracks::onDoubleClicked(QTreeWidgetItem *item, int /*column*/) { if (item->isDisabled()) @@ -151,8 +198,13 @@ void List::Tracks::onHeaderMenu(const QPoint &pos) auto *menu = new QMenu(header()); auto *showHeaders = menu->addMenu(Icon::get("visibility"), "Columns to show"); auto *sortBy = menu->addMenu(Icon::get("view-sort-ascending"), "Default sorting"); - QStringList headerTitles({ - "Title", "Artist", "Album", "Length", "Added" + const QStringList headerTitles({ + QStringLiteral("Title"), + QStringLiteral("Artist"), + QStringLiteral("Album"), + QStringLiteral("Length"), + QStringLiteral("Added"), + QStringLiteral("Liked"), }); const auto &headers = this->settings.general.hidden_song_headers; @@ -205,6 +257,31 @@ void List::Tracks::onHeaderMenuTriggered(QAction *action) settings.save(); } +void List::Tracks::onCurrentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous) +{ + if (previous != nullptr) + { + const auto &likedData = previous->data(0, static_cast(DataRole::Liked)); + const auto isLiked = likedData.toBool(); + if (!isLiked) + { + previous->setIcon(static_cast(Column::Liked), QIcon()); + } + } + + if (current == nullptr) + { + return; + } + + const auto &likedData = current->data(0, static_cast(DataRole::Liked)); + const auto isLiked = likedData.toBool(); + + current->setIcon(static_cast(Column::Liked), isLiked + ? Icon::get(QStringLiteral("starred-symbolic")) + : Icon::get(QStringLiteral("non-starred-symbolic"))); +} + void List::Tracks::onNewPlaylist() { const auto trackIds = getSelectedTrackIds(); @@ -300,8 +377,9 @@ void List::Tracks::resizeHeaders(const QSize &newSize) constexpr int addedSize = 140; constexpr int minSize = 60; constexpr int columnCount = 7; + constexpr int likedSize = 40; - auto size = (newSize.width() - indexSize - lengthSize - addedSize) / columnCount; + auto size = (newSize.width() - indexSize - lengthSize - addedSize - likedSize) / columnCount; if (size < minSize) { @@ -314,6 +392,7 @@ void List::Tracks::resizeHeaders(const QSize &newSize) header()->resizeSection(static_cast(Column::Album), size * 2); header()->resizeSection(static_cast(Column::Length), lengthSize); header()->resizeSection(static_cast(Column::Added), addedSize); + header()->resizeSection(static_cast(Column::Liked), likedSize); } void List::Tracks::updateResizeMode(lib::resize_mode mode) @@ -414,7 +493,7 @@ void List::Tracks::load(const std::vector &tracks, QString::fromStdString(track.album.name), QString::fromStdString(lib::format::time(track.duration)), getAddedText(added), - }, track, emptyIcon, index, QString::fromStdString(addedAt)); + }, track, emptyIcon, index, QString::fromStdString(addedAt), false); if (!anyHasDate && !added.empty()) { @@ -440,6 +519,26 @@ void List::Tracks::load(const std::vector &tracks, header()->setSectionHidden(static_cast(Column::Added), !anyHasDate || lib::set::contains(settings.general.hidden_song_headers, static_cast(Column::Added))); + + getLikedTracks([this](const std::vector &likedTracks) + { + for (const auto &likedTrack: likedTracks) + { + auto trackItem = trackItems.find(likedTrack.id); + if (trackItem == trackItems.end()) + { + continue; + } + + auto *item = dynamic_cast(trackItem->second); + if (item == nullptr) + { + continue; + } + + item->setLiked(true); + } + }); } void List::Tracks::load(const std::vector &tracks) @@ -571,3 +670,19 @@ auto List::Tracks::getCurrent() -> const spt::Current & auto *mainWindow = MainWindow::find(parentWidget()); return mainWindow->getCurrent(); } + +void List::Tracks::getLikedTracks(const std::function &)> &callback) +{ + const auto cachedTracks = cache.get_tracks("liked_tracks"); + if (!cachedTracks.empty()) + { + callback(cachedTracks); + return; + } + + spotify.saved_tracks([this, callback](const std::vector &tracks) + { + callback(tracks); + cache.set_tracks("liked_tracks", tracks); + }); +} diff --git a/src/list/tracks.hpp b/src/list/tracks.hpp index 2507154b..fb93c3df 100644 --- a/src/list/tracks.hpp +++ b/src/list/tracks.hpp @@ -78,11 +78,16 @@ namespace List auto getSelectedTrackIds() const -> std::vector; void resizeHeaders(const QSize &newSize); + void getLikedTracks(const std::function &)> &callback); + void onMenu(const QPoint &pos); + void onItemClicked(QTreeWidgetItem *item, int column); void onDoubleClicked(QTreeWidgetItem *item, int column); void onHeaderMenu(const QPoint &pos); void onHeaderMenuTriggered(QAction *action); + static void onCurrentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous); + void onNewPlaylist(); void onDelete(); void onPlaySelectedRow(); diff --git a/src/listitem/track.cpp b/src/listitem/track.cpp index d293c19c..e7f54482 100644 --- a/src/listitem/track.cpp +++ b/src/listitem/track.cpp @@ -1,13 +1,14 @@ #include "listitem/track.hpp" +#include "util/icon.hpp" ListItem::Track::Track(const QStringList &strings, const lib::spt::track &track, const QIcon &icon, int index) - : Track(strings, track, icon, index, {}) + : Track(strings, track, icon, index, {}, false) { } ListItem::Track::Track(const QStringList &strings, const lib::spt::track &track, - const QIcon &icon, int index, const QString &addedAt) + const QIcon &icon, int index, const QString &addedAt, bool isLiked) : QTreeWidgetItem(strings) { setIcon(0, icon); @@ -22,6 +23,8 @@ ListItem::Track::Track(const QStringList &strings, const lib::spt::track &track, setData(0, static_cast(DataRole::AddedDate), addedDate); setData(0, static_cast(DataRole::Length), track.duration); + setLiked(isLiked); + if (track.is_local || !track.is_playable) { setDisabled(true); @@ -97,3 +100,13 @@ auto ListItem::Track::removePrefix(const QString &str) -> QString ? str.right(str.length() - 4) : str; } + +void ListItem::Track::setLiked(bool isLiked) +{ + const auto icon = isLiked + ? Icon::get(QStringLiteral("starred-symbolic")) + : QIcon(); + + setIcon(static_cast(Column::Liked), icon); + setData(0, static_cast(DataRole::Liked), isLiked); +} diff --git a/src/listitem/track.hpp b/src/listitem/track.hpp index 2afc89ec..a847b2de 100644 --- a/src/listitem/track.hpp +++ b/src/listitem/track.hpp @@ -19,7 +19,9 @@ namespace ListItem const QIcon &icon, int index); Track(const QStringList &strings, const lib::spt::track &track, - const QIcon &icon, int index, const QString &addedAt); + const QIcon &icon, int index, const QString &addedAt, bool isLiked); + + void setLiked(bool isLiked); private: auto operator<(const QTreeWidgetItem &item) const -> bool override;