Compare commits

..
Author SHA1 Message Date
Dimitris Kardarakos 0ea2d757d7 Fix action naming and icon 2022-01-02 15:13:02 +02:00
Dimitris Kardarakos 93d7164229 Improve search page item accessibility
On mobile, interaction with the search page swipe list items result in a set of inconveniences:
- Accidentally play a track - triggered by item click- while trying to click the swipe handler, and vice versa
- Swipe list item actions do not offer a text description; you have to try them to see what they offer
- The styling of the swipe list item on mobile, in many cases, makes hard to trigger the actions
- When more than one pages exist in the stack, gestures for swiping an item and navigating between pages overlap

An alternative pattern is offered:
- The search results are presented as simple list items
- When the user clicks on an item, the item is expanded; a new row is displayed, displaying the actions available for this item.
- When the user clicks on an expanded item, the item's actions are hiding
2022-01-02 12:54:38 +00:00
28 changed files with 229 additions and 253 deletions

No files matched your search

-1
View File
@@ -6,4 +6,3 @@
*.kdev4
CMakeLists.txt.user
flatpak/flatpak-builder-tools/
build/
-21
View File
@@ -2,24 +2,3 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: AudioTube
Upstream-Contact: Jonah Brüchert <[email protected]>
Source: https://invent.kde.org/plasma-mobile/audiotube
# Sample paragraph, commented out:
#
# Files: ...
# Copyright: $YEAR $NAME <$CONTACT>
# License: ...
# Comment: ...
Files: README.md
Copyright: 2021 Jonah Brüchert <[email protected]>
License: CC0-1.0
Files: org.kde.audiotube.desktop
org.kde.audiotube.json
Copyright: 2021 Jonah Brüchert <[email protected]>
License: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
Files: logo.png
org.kde.audiotube.svg
Copyright: 2021 Mathis Brüchert <[email protected]>
License: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+2 -2
View File
@@ -8,11 +8,11 @@ project(audiotube)
include(FeatureSummary)
set(PROJECT_VERSION "22.02")
set(PROJECT_VERSION "21.12")
set(QT5_MIN_VERSION 5.15)
set(KF5_MIN_VERSION 5.81)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE)
+1 -1
View File
@@ -6,7 +6,7 @@ AudioTube is a client for YouTube Music.
It depends on Qt, Kirigami, KI18n, KCoreAddons and KCrash in addition to a few python packages:
* ytmusicapi
* yt_dlp
* youtube_dl
YouTube uses webp images for the cover art, so you will need to install qtimageformats.
+3
View File
@@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2021 Jonah Brüchert <[email protected]>
SPDX-License-Identifier: CC0-1.0
+2 -2
View File
@@ -7,8 +7,8 @@
"sources": [
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/8e/10/7b43c0a779d4c6331088d64af7e4a2da27b47817f558e97ebbadfc41869f/yt_dlp-2022.1.21-py2.py3-none-any.whl",
"sha256": "182903b78a4645edd504dcd9f2603f76f9fdaf8bd279fd45d01992f7637b6d8a"
"url": "https://files.pythonhosted.org/packages/27/7e/5bf52a334a0638639b57b7699e6a3adaaaa8e7bd6f7ad33e62a9e2be7bb7/yt_dlp-2021.12.27-py2.py3-none-any.whl",
"sha256": "bb46898a175d149c9c6bb2846056590d297aa4eafad8487c3e1315d2c6090896"
},
{
"type": "file",
+11 -11
View File
@@ -7,8 +7,13 @@
"sources": [
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/33/d9/d18f3827dfd54acc48f29337059197514ad656ffc88e02a51e0457c5117a/ytmusicapi-0.20.0-py3-none-any.whl",
"sha256": "abdb90426baa14a7929ec810d65c1df228661b61d580678fc48c087909a9aea2"
"url": "https://files.pythonhosted.org/packages/87/5a/ef90afb6dbef7a2cf8418c050f2e9e32be449f97ddad085e41e568bf4a15/ytmusicapi-0.19.5-py3-none-any.whl",
"sha256": "23ed35df2b7977e3e7662c0ac478864fb23d025ba5de8f2c269cabf2903b8d25"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/92/96/144f70b972a9c0eabbd4391ef93ccd49d0f2747f4f6a2a2738e99e5adc65/requests-2.26.0-py2.py3-none-any.whl",
"sha256": "6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"
},
{
"type": "file",
@@ -17,13 +22,8 @@
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/84/3e/1037abe6498e65d645ce7a22d3402605d49a3b2c7f20c3abb027760da4f0/charset_normalizer-2.0.10-py3-none-any.whl",
"sha256": "cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/2d/61/08076519c80041bc0ffa1a8af0cbd3bf3e2b62af10435d269a9d0f40564d/requests-2.27.1-py2.py3-none-any.whl",
"sha256": "f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"
"url": "https://files.pythonhosted.org/packages/47/84/b06f6729fac8108c5fa3e13cde19b0b3de66ba5538c325496dbe39f5ff8e/charset_normalizer-2.0.9-py3-none-any.whl",
"sha256": "1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721"
},
{
"type": "file",
@@ -32,8 +32,8 @@
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/4e/b8/f5a25b22e803f0578e668daa33ba3701bb37858ec80e08a150bd7d2cf1b1/urllib3-1.26.8-py2.py3-none-any.whl",
"sha256": "000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"
"url": "https://files.pythonhosted.org/packages/af/f4/524415c0744552cce7d8bf3669af78e8a069514405ea4fcbd0cc44733744/urllib3-1.26.7-py2.py3-none-any.whl",
"sha256": "c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"
}
]
}
+3
View File
@@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2021 Mathis Brüchert <[email protected]>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
-15
View File
@@ -7,7 +7,6 @@ SPDX-License-Identifier: CC0-1.0
<component type="desktop-application">
<id>org.kde.audiotube</id>
<name>AudioTube</name>
<name xml:lang="ar">تيوب الصوتي</name>
<name xml:lang="ca">AudioTube</name>
<name xml:lang="ca-valencia">AudioTube</name>
<name xml:lang="cs">AudioTube</name>
@@ -25,14 +24,12 @@ SPDX-License-Identifier: CC0-1.0
<name xml:lang="pl">AudioTube</name>
<name xml:lang="pt">AudioTube</name>
<name xml:lang="pt-BR">AudioTube</name>
<name xml:lang="ro">AudioTube</name>
<name xml:lang="sk">AudioTube</name>
<name xml:lang="sl">AudioTube</name>
<name xml:lang="sv">AudioTube</name>
<name xml:lang="uk">AudioTube</name>
<name xml:lang="x-test">xxAudioTubexx</name>
<summary>Convergent YouTube Music client</summary>
<summary xml:lang="ar">عميل موسيقى يوتيوب متعدد المنصات</summary>
<summary xml:lang="ca">Client convergent de música de YouTube</summary>
<summary xml:lang="ca-valencia">Client convergent de música de YouTube</summary>
<summary xml:lang="de">Konvergentes YouTube-Musikprogramm</summary>
@@ -48,7 +45,6 @@ SPDX-License-Identifier: CC0-1.0
<summary xml:lang="pl">Adaptacyjny program do YouTube Music</summary>
<summary xml:lang="pt">Cliente de música do YouTube convergente</summary>
<summary xml:lang="pt-BR">Um cliente convergente para o YouTube Music</summary>
<summary xml:lang="ro">Client convergent de muzică pentru YouTube</summary>
<summary xml:lang="sl">Konvergentni odjemalec glasbe YouTube</summary>
<summary xml:lang="sv">Konvergent YouTube-musikprogram</summary>
<summary xml:lang="uk">Зручний музичний клієнт YouTube</summary>
@@ -56,7 +52,6 @@ SPDX-License-Identifier: CC0-1.0
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-2.0+</project_license>
<developer_name>KDE Community</developer_name>
<developer_name xml:lang="ar">مجتمع كدي</developer_name>
<developer_name xml:lang="ca">La comunitat KDE</developer_name>
<developer_name xml:lang="ca-valencia">La comunitat KDE</developer_name>
<developer_name xml:lang="cs">Komunita KDE</developer_name>
@@ -74,7 +69,6 @@ SPDX-License-Identifier: CC0-1.0
<developer_name xml:lang="pl">Społeczność KDE</developer_name>
<developer_name xml:lang="pt">Comunidade do KDE</developer_name>
<developer_name xml:lang="pt-BR">A Comunidade KDE</developer_name>
<developer_name xml:lang="ro">Comunitatea KDE</developer_name>
<developer_name xml:lang="sk">KDE komunita</developer_name>
<developer_name xml:lang="sl">Skupnost KDE</developer_name>
<developer_name xml:lang="sv">KDE-gemenskapen</developer_name>
@@ -83,14 +77,6 @@ SPDX-License-Identifier: CC0-1.0
<content_rating type="oars-1.0"/>
<url type="homepage">https://apps.kde.org/en/audiotube</url>
<releases>
<release version="22.02" date="2022-02-09">
<url>https://www.plasma-mobile.org/2022/02/09/plasma-mobile-gear-22-02/</url>
<description>
<ul>
<li>Update the version of ytmusicapi</li>
</ul>
</description>
</release>
<release version="21.12" date="2021-12-07">
<url>https://www.plasma-mobile.org/2021/12/07/plasma-mobile-gear-21-12/</url>
</release>
@@ -114,7 +100,6 @@ SPDX-License-Identifier: CC0-1.0
</recommends>
<description>
<p>AudioTube can search YouTube Music, list albums and artists, play automatically generated playlists, albums and allows to put your own playlist together.</p>
<p xml:lang="ar">يمكن لتيوب الصوتي البحث في موسيقى يوتيوب، وعرض الألبومات والفنانين، وتشغيل القوائم والألبومات المولدة تلقائيًا، ويسمح لك بوضع قائمة التشغيل الخاصة بك معًا.</p>
<p xml:lang="ca">L'AudioTube pot cercar música a YouTube, llistar àlbums i artistes, reproduir llistes de reproducció generades automàticament, àlbums i permet crear la vostra pròpia llista de reproducció.</p>
<p xml:lang="ca-valencia">L'AudioTube pot cercar música a YouTube, llistar àlbums i artistes, reproduir llistes de reproducció generades automàticament, àlbums i permet crear la vostra pròpia llista de reproducció.</p>
<p xml:lang="de">AudioTube kann Musik auf YouTube durchsuchen, Alben und Interpreten auflisten, automatisch generierte Wiedergabelisten und Alben abspielen und erlaubt es, eine eigene Wiedergabeliste zusammenzustellen.</p>
-2
View File
@@ -1,6 +1,5 @@
[Desktop Entry]
Name=AudioTube
Name[ar]=تيوب الصوتي
Name[ca]=AudioTube
Name[ca@valencia]=AudioTube
Name[cs]=AudioTube
@@ -25,7 +24,6 @@ Name[sv]=AudioTube
Name[uk]=AudioTube
Name[x-test]=xxAudioTubexx
Comment=YouTube Music client
Comment[ar]=عميل يوتيوب ميوزيك
Comment[ca]=Client de música de YouTube
Comment[ca@valencia]=Client de música de YouTube
Comment[cs]=Hudební klient YouTube
+3
View File
@@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2021 Jonah Brüchert <[email protected]>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+1 -1
View File
@@ -28,7 +28,7 @@
"config-opts": ["-DPYBIND11_FINDPYTHON=ON", "-DPYBIND11_TEST=OFF"],
"buildsystem": "cmake-ninja",
"builddir": true,
"sources": [ { "type": "git", "url": "https://github.com/pybind/pybind11", "tag": "v2.9.1" } ]
"sources": [ { "type": "git", "url": "https://github.com/pybind/pybind11", "tag": "v2.6.2" } ]
},
{
"name": "org.kde.audiotube",
+3
View File
@@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2021 Jonah Brüchert <[email protected]>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+3
View File
@@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2021 Mathis Brüchert <[email protected]>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+1 -1
View File
@@ -3,5 +3,5 @@
#
# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
ytmusicapi==0.20.0
ytmusicapi==0.19.5
yt-dlp
+3 -3
View File
@@ -9,11 +9,11 @@
AlbumModel::AlbumModel(QObject *parent)
: AbstractYTMusicModel(parent)
{
connect(this, &AlbumModel::browseIdChanged, this, [this] {
connect(this, &AlbumModel::browseIdChanged, this, [=] {
setLoading(true);
YTMusicThread::instance()->fetchAlbum(m_browseId);
});
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchAlbumFinished, this, [this](const album::Album &album) {
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchAlbumFinished, this, [=](const album::Album &album) {
setLoading(false);
beginResetModel();
@@ -25,7 +25,7 @@ AlbumModel::AlbumModel(QObject *parent)
Q_EMIT thumbnailUrlChanged();
Q_EMIT playlistIdChanged();
});
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [this] {
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [=] {
setLoading(false);
});
}
+36 -28
View File
@@ -8,9 +8,8 @@
ArtistModel::ArtistModel(QObject *parent)
: AbstractYTMusicModel(parent)
, m_view(albums, singles, songs, videos)
{
connect(this, &ArtistModel::channelIdChanged, this, [this] {
connect(this, &ArtistModel::channelIdChanged, this, [=] {
if (m_channelId.isEmpty()) {
return;
}
@@ -19,37 +18,44 @@ ArtistModel::ArtistModel(QObject *parent)
YTMusicThread::instance()->fetchArtist(m_channelId);
});
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchArtistFinished, this, [this](const artist::Artist &artist) {
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchArtistFinished, this, [=](const artist::Artist &artist) {
setLoading(false);
beginResetModel();
m_artist = artist;
std::sort(m_artist.thumbnails.begin(), m_artist.thumbnails.end());
albums = m_artist.albums ? m_artist.albums->results : std::vector<artist::Artist::Album>();
singles = m_artist.singles ? m_artist.singles->results : std::vector<artist::Artist::Single>();
songs = m_artist.songs ? m_artist.songs->results : std::vector<artist::Artist::Song>();
videos = m_artist.videos ? m_artist.videos->results : std::vector<artist::Artist::Video>();
auto albums = m_artist.albums ? m_artist.albums->results : std::vector<artist::Artist::Album>();
auto singles = m_artist.singles ? m_artist.singles->results : std::vector<artist::Artist::Single>();
auto songs = m_artist.songs ? m_artist.songs->results : std::vector<artist::Artist::Song>();
auto videos = m_artist.videos ? m_artist.videos->results : std::vector<artist::Artist::Video>();
// std::span can't know if the data pointer underneath it was changed, so re-create
m_view = MultiIterableView(albums, singles, songs, videos);
m_view = std::optional(MultiIterableView(std::move(albums),
std::move(singles),
std::move(songs),
std::move(videos)));
endResetModel();
Q_EMIT titleChanged();
Q_EMIT thumbnailUrlChanged();
});
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [this] {
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [=] {
setLoading(false);
});
}
int ArtistModel::rowCount(const QModelIndex &parent) const
{
return parent.isValid() ? 0 : m_view.size();
return parent.isValid() ? 0 : m_view ? int(m_view->size()) : 0;
}
QVariant ArtistModel::data(const QModelIndex &index, int role) const
{
if (!m_view) {
return {};
}
switch (role) {
case Title:
return QString::fromStdString(std::visit([&](auto&& item) {
@@ -65,7 +71,7 @@ QVariant ArtistModel::data(const QModelIndex &index, int role) const
}
Q_UNREACHABLE();
}, m_view[index.row()]));
}, m_view.value()[index.row()]));
case Type:
return std::visit([&](auto&& item) {
using T = std::decay_t<decltype(item)>;
@@ -80,7 +86,7 @@ QVariant ArtistModel::data(const QModelIndex &index, int role) const
}
Q_UNREACHABLE();
}, m_view[index.row()]);
}, m_view.value()[index.row()]);
case Artists:
return QVariant::fromValue(std::vector<meta::Artist> {
{
@@ -102,7 +108,7 @@ QVariant ArtistModel::data(const QModelIndex &index, int role) const
}
Q_UNREACHABLE();
}, m_view[index.row()]);
}, m_view.value()[index.row()]);
}
Q_UNREACHABLE();
@@ -147,18 +153,20 @@ QUrl ArtistModel::thumbnailUrl() const
void ArtistModel::triggerItem(int row)
{
std::visit([&](auto&& item) {
using T = std::decay_t<decltype(item)>;
if constexpr(std::is_same_v<T, artist::Artist::Album>) {
Q_EMIT openAlbum(QString::fromStdString(item.browse_id));
} else if constexpr(std::is_same_v<T, artist::Artist::Single>) {
Q_EMIT openAlbum(QString::fromStdString(item.browse_id));
} else if constexpr(std::is_same_v<T, artist::Artist::Song>) {
Q_EMIT openSong(QString::fromStdString(item.video_id));
} else if constexpr(std::is_same_v<T, artist::Artist::Video>) {
Q_EMIT openVideo(QString::fromStdString(item.video_id), QString::fromStdString(item.title));
} else {
Q_UNREACHABLE();
}
}, m_view[row]);
if (m_view) {
std::visit([&](auto&& item) {
using T = std::decay_t<decltype(item)>;
if constexpr(std::is_same_v<T, artist::Artist::Album>) {
Q_EMIT openAlbum(QString::fromStdString(item.browse_id));
} else if constexpr(std::is_same_v<T, artist::Artist::Single>) {
Q_EMIT openAlbum(QString::fromStdString(item.browse_id));
} else if constexpr(std::is_same_v<T, artist::Artist::Song>) {
Q_EMIT openSong(QString::fromStdString(item.video_id));
} else if constexpr(std::is_same_v<T, artist::Artist::Video>) {
Q_EMIT openVideo(QString::fromStdString(item.video_id), QString::fromStdString(item.title));
} else {
Q_UNREACHABLE();
}
}, m_view.value()[row]);
}
}
+2 -8
View File
@@ -66,13 +66,7 @@ private:
QString m_channelId;
artist::Artist m_artist;
std::vector<artist::Artist::Album> albums;
std::vector<artist::Artist::Single> singles;
std::vector<artist::Artist::Song> songs;
std::vector<artist::Artist::Video> videos;
MultiIterableView<
std::optional<MultiIterableView<
artist::Artist::Album, artist::Artist::Single, artist::Artist::Song, artist::Artist::Video
> m_view;
>> m_view;
};
+9 -9
View File
@@ -51,7 +51,7 @@ AsyncYTMusic::AsyncYTMusic(QObject *parent)
//
void AsyncYTMusic::search(const QString &query)
{
invokeAndCatchOnThread([=, this]() {
invokeAndCatchOnThread([=]() {
Q_EMIT searchFinished(m_ytm->search(query.toStdString()));
});
}
@@ -61,7 +61,7 @@ void AsyncYTMusic::search(const QString &query)
//
void AsyncYTMusic::fetchArtist(const QString &channelId)
{
invokeAndCatchOnThread([=, this]() {
invokeAndCatchOnThread([=]() {
Q_EMIT fetchArtistFinished(m_ytm->get_artist(channelId.toStdString()));
});
}
@@ -71,7 +71,7 @@ void AsyncYTMusic::fetchArtist(const QString &channelId)
//
void AsyncYTMusic::fetchAlbum(const QString &browseId)
{
invokeAndCatchOnThread([=, this]() {
invokeAndCatchOnThread([=]() {
Q_EMIT fetchAlbumFinished(m_ytm->get_album(browseId.toStdString()));
});
}
@@ -81,7 +81,7 @@ void AsyncYTMusic::fetchAlbum(const QString &browseId)
//
void AsyncYTMusic::fetchSong(const QString &videoId)
{
invokeAndCatchOnThread([=, this]() {
invokeAndCatchOnThread([=]() {
auto maybeSong = m_ytm->get_song(videoId.toStdString());
if (maybeSong.has_value()) {
Q_EMIT fetchSongFinished(*maybeSong);
@@ -93,7 +93,7 @@ void AsyncYTMusic::fetchSong(const QString &videoId)
// fetchPlaylist
//
void AsyncYTMusic::fetchPlaylist(const QString &playlistId) {
invokeAndCatchOnThread([=, this]() {
invokeAndCatchOnThread([=]() {
Q_EMIT fetchPlaylistFinished(m_ytm->get_playlist(playlistId.toStdString()));
});
}
@@ -103,7 +103,7 @@ void AsyncYTMusic::fetchPlaylist(const QString &playlistId) {
//
void AsyncYTMusic::fetchArtistAlbums(const QString &channelId, const QString &params)
{
invokeAndCatchOnThread([=, this]() {
invokeAndCatchOnThread([=]() {
Q_EMIT fetchArtistAlbumsFinished(m_ytm->get_artist_albums(channelId.toStdString(), params.toStdString()));
});
}
@@ -113,7 +113,7 @@ void AsyncYTMusic::fetchArtistAlbums(const QString &channelId, const QString &pa
//
void AsyncYTMusic::extractVideoInfo(const QString &videoId)
{
invokeAndCatchOnThread([=, this]() {
invokeAndCatchOnThread([=]() {
Q_EMIT extractVideoInfoFinished(m_ytm->extract_video_info(videoId.toStdString()));
});
}
@@ -123,7 +123,7 @@ void AsyncYTMusic::extractVideoInfo(const QString &videoId)
//
void AsyncYTMusic::fetchWatchPlaylist(const std::optional<QString> &videoId, const std::optional<QString> &playlistId)
{
invokeAndCatchOnThread([=, this]() {
invokeAndCatchOnThread([=]() {
Q_EMIT fetchWatchPlaylistFinished(m_ytm->get_watch_playlist(
map_optional<std::string>(videoId, &QString::toStdString),
map_optional<std::string>(playlistId, &QString::toStdString)
@@ -132,7 +132,7 @@ void AsyncYTMusic::fetchWatchPlaylist(const std::optional<QString> &videoId, con
}
void AsyncYTMusic::invokeAndCatchOnThread(const std::function<void()> &fun) {
QMetaObject::invokeMethod(this, [=, this]() {
QMetaObject::invokeMethod(this, [=]() {
try {
fun();
} catch (const py::error_already_set &err) {
+60 -42
View File
@@ -75,8 +75,9 @@ Kirigami.ScrollablePage {
}
}
}
delegate: Kirigami.SwipeListItem {
delegate: Kirigami.AbstractListItem {
id: delegateItem
property bool showActionButtons: false
required property int index
required property string title
required property int type
@@ -84,55 +85,72 @@ Kirigami.ScrollablePage {
required property var artists
required property string radioPlaylistId
RowLayout {
Layout.fillHeight: true
Kirigami.Icon {
Layout.fillHeight: true
source: {
switch (type) {
case SearchModel.Artist:
return "view-media-artist"
case SearchModel.Album:
return "media-album-cover"
case SearchModel.Playlist:
return "view-media-playlist"
case SearchModel.Song:
return "emblem-music-symbolic"
case SearchModel.Video:
return "emblem-videos-symbolic"
ColumnLayout {
RowLayout {
Layout.fillWidth: true
Kirigami.Icon {
Layout.fillHeight: true
source: {
switch (type) {
case SearchModel.Artist:
return "view-media-artist"
case SearchModel.Album:
return "media-album-cover"
case SearchModel.Playlist:
return "view-media-playlist"
case SearchModel.Song:
return "emblem-music-symbolic"
case SearchModel.Video:
return "emblem-videos-symbolic"
}
}
}
Controls.Label {
Layout.fillWidth: true
text: title
elide: Qt.ElideRight
}
}
Controls.Label {
Kirigami.Separator {
visible: showActionButtons
Layout.fillWidth: true
text: title
elide: Qt.ElideRight
}
Kirigami.ActionToolBar {
Layout.fillWidth: true
alignment: Qt.AlignLeft
visible: showActionButtons
actions: [
Kirigami.Action {
icon.name: (delegateItem.type === SearchModel.Song || delegateItem.type == SearchModel.Video) ? "media-playback-start" : "project-open"
text: (delegateItem.type === SearchModel.Song || delegateItem.type == SearchModel.Video) ? i18n("Play") : i18n("Open")
onTriggered: searchModel.triggerItem(index)
} ,
Kirigami.Action {
icon.name: "go-next"
text: i18n("Play next")
visible: delegateItem.type === SearchModel.Song || delegateItem.type == SearchModel.Video
onTriggered: UserPlaylistModel.playNext(delegateItem.videoId, delegateItem.title, delegateItem.artists)
},
Kirigami.Action {
icon.name: "media-playlist-append"
text: i18n("Add to Playlist")
visible: delegateItem.type === SearchModel.Song || delegateItem.type == SearchModel.Video
onTriggered: UserPlaylistModel.append(delegateItem.videoId, delegateItem.title, delegateItem.artists)
},
Kirigami.Action {
icon.name: "radio"
text: i18n("Radio")
visible: delegateItem.type === SearchModel.Artist && delegateItem.radioPlaylistId
onTriggered: playPlaylist(delegateItem.radioPlaylistId)
}
]
}
}
actions: [
Kirigami.Action {
icon.name: "go-next"
text: i18n("Play next")
visible: delegateItem.type === SearchModel.Song || delegateItem.type == SearchModel.Video
onTriggered: UserPlaylistModel.playNext(delegateItem.videoId, delegateItem.title, delegateItem.artists)
},
Kirigami.Action {
icon.name: "media-playlist-append"
text: i18n("Add to Playlist")
visible: delegateItem.type === SearchModel.Song || delegateItem.type == SearchModel.Video
onTriggered: UserPlaylistModel.append(delegateItem.videoId, delegateItem.title, delegateItem.artists)
},
Kirigami.Action {
icon.name: "radio"
text: i18n("Radio")
visible: delegateItem.type === SearchModel.Artist && delegateItem.radioPlaylistId
onTriggered: playPlaylist(delegateItem.radioPlaylistId)
}
]
onClicked: searchModel.triggerItem(index)
onClicked: showActionButtons = !showActionButtons
}
Controls.BusyIndicator {
anchors.centerIn: parent
+55 -52
View File
@@ -6,68 +6,71 @@
#include <variant>
#include <tuple>
#include <span>
#include <vector>
#include <QtGlobal>
template <typename Tuple, typename Func, std::size_t i>
inline constexpr void iterate_impl(Tuple &tup, Func fun)
{
if constexpr(i >= std::tuple_size_v<std::decay_t<decltype(tup)>>) {
return;
} else {
fun(std::get<i>(tup));
return iterate_impl<Tuple, Func, i + 1>(tup, fun);
}
}
// MultiIterableView is usually used on a small number of vectors.
// The number is already known, so we can convert this into an if else if else etc tree
// instead of recursively calling the function
#ifdef __GNUC__
#define REMOVE_RECURSION __attribute__((always_inline))
#else
#define REMOVE_RECURSION
#endif
template <typename Tuple, typename Func>
inline constexpr void iterate_tuple(Tuple &tup, Func fun)
{
iterate_impl<Tuple, Func, 0>(tup, fun);
}
/// MultiIterableView is a view over multiple contigous memory arrays, like std::vector or std::array.
/// The elements of the arrays do not need to be of the same type.
template <typename... Arguments>
class MultiIterableView {
public:
explicit MultiIterableView(std::span<Arguments>... lists)
: m_vectors(std::forward_as_tuple(lists...))
{
private:
template<typename T, size_t i, size_t len>
REMOVE_RECURSION inline size_t count(const T &tuple, int initial_size = 0) const {
if constexpr(i < len) {
return count<T, i + 1, len>(tuple, initial_size + std::get<i>(tuple).size());
} else {
return initial_size;
}
}
explicit MultiIterableView(std::vector<Arguments> &...lists)
: MultiIterableView(std::span{lists}...)
{
}
[[nodiscard]] constexpr size_t size() const {
size_t s = 0;
iterate_tuple(m_vectors, [&](auto &vec) {
s += vec.size();
});
return s;
}
[[nodiscard]] constexpr std::variant<Arguments...> operator[](size_t i) const {
size_t s = 0;
std::variant<Arguments...> out;
iterate_tuple(m_vectors, [&](auto &vec) {
if (i >= s && i < s + vec.size()) {
out = vec[i - s];
template<typename T, size_t tuple_i, size_t tuple_len>
REMOVE_RECURSION inline std::variant<Arguments...> get_item(const T &tuple, size_t i) const {
if constexpr(tuple_i < tuple_len) {
if (i < std::get<tuple_i>(tuple).size()) {
return std::get<tuple_i>(tuple)[i];
} else {
return get_item<T, tuple_i + 1, tuple_len>(tuple, i - std::get<tuple_i>(tuple).size());
}
s += vec.size();
});
return out;
} else {
Q_UNREACHABLE();
}
}
[[nodiscard]] constexpr bool empty() const {
bool empty = true;
iterate_tuple(m_vectors, [&](auto &elem) {
empty &= elem.empty();
});
return empty;
template<typename T, size_t i, size_t len>
REMOVE_RECURSION inline bool everything_empty(const T &tuple, bool initial_state = true) const {
if constexpr(i < len) {
return everything_empty<T, i + 1, len>(tuple, initial_state && std::get<i>(tuple).empty());
} else {
return initial_state;
}
}
public:
explicit MultiIterableView(std::vector<Arguments> &&...lists)
: m_vectors(std::make_tuple(lists...))
{
}
size_t size() const {
constexpr size_t nvecs = std::tuple_size_v<decltype(m_vectors)>;
return count<decltype(m_vectors), 0, nvecs>(m_vectors);
}
std::variant<Arguments...> operator[](size_t i) const {
constexpr size_t nvecs = std::tuple_size_v<decltype(m_vectors)>;
return get_item<decltype(m_vectors), 0, nvecs>(m_vectors, i);
}
bool empty() const {
constexpr size_t nvecs = std::tuple_size_v<decltype(m_vectors)>;
return everything_empty<decltype(m_vectors), 0, nvecs>(m_vectors);
}
private:
std::tuple<std::span<Arguments>...> m_vectors;
std::tuple<std::vector<Arguments>...> m_vectors;
};
+2 -2
View File
@@ -13,11 +13,11 @@
PlaylistModel::PlaylistModel(QObject *parent)
: AbstractYTMusicModel(parent)
{
connect(this, &PlaylistModel::playlistIdChanged, this, [=, this] {
connect(this, &PlaylistModel::playlistIdChanged, this, [=] {
setLoading(true);
YTMusicThread::instance()->fetchPlaylist(m_playlistId);
});
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchPlaylistFinished, this, [this](const playlist::Playlist &playlist) {
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchPlaylistFinished, this, [=](const playlist::Playlist &playlist) {
setLoading(false);
beginResetModel();
m_playlist = playlist;
+4 -5
View File
@@ -15,12 +15,11 @@ namespace PlaylistUtils {
QString artistsToString(const std::vector<meta::Artist> &artists)
{
if (!artists.empty()) {
QString string = QString::fromStdString(artists.front().name);
std::for_each(artists.begin() + 1, artists.end(),
[&string](const meta::Artist &artist) {
string.append(QStringLiteral(", %1").arg(QString::fromStdString(artist.name)));
return std::accumulate(artists.begin() + 1, artists.end(),
QString::fromStdString(artists.front().name),
[](QString &string, const meta::Artist &artist) {
return string.append(QStringLiteral(", %1").arg(QString::fromStdString(artist.name)));
});
return string;
}
return {};
+3 -28
View File
@@ -7,7 +7,7 @@
SearchModel::SearchModel(QObject *parent)
: AbstractYTMusicModel(parent)
{
connect(this, &SearchModel::searchQueryChanged, this, [this] {
connect(this, &SearchModel::searchQueryChanged, this, [=] {
if (m_searchQuery.isEmpty()) {
beginResetModel();
m_searchResults.clear();
@@ -18,17 +18,13 @@ SearchModel::SearchModel(QObject *parent)
setLoading(true);
YTMusicThread::instance()->search(m_searchQuery);
});
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::searchFinished, this, [this](const std::vector<search::SearchResultItem> &results) {
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::searchFinished, this, [=](const std::vector<search::SearchResultItem> &results) {
beginResetModel();
setLoading(false);
m_searchResults = results;
std::sort(m_searchResults.begin(), m_searchResults.end(),
[](search::SearchResultItem const & a, search::SearchResultItem const & b) {
return SearchModel::itemType(a) < SearchModel::itemType(b);
});
endResetModel();
});
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [this] {
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [=] {
setLoading(false);
});
}
@@ -169,24 +165,3 @@ void SearchModel::triggerItem(int row)
}
}, m_searchResults.at(row));
}
int SearchModel::itemType(search::SearchResultItem const &item)
{
return std::visit([&](auto &&arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr(std::is_same_v<T, search::Album>) {
return SearchModel::Type::Album;
} else if constexpr(std::is_same_v<T, search::Artist>) {
return SearchModel::Type::Artist;
} else if constexpr(std::is_same_v<T, search::Playlist>) {
return SearchModel::Type::Playlist;
} else if constexpr(std::is_same_v<T, search::Song>) {
return SearchModel::Type::Song;
} else if constexpr(std::is_same_v<T, search::Video>) {
return SearchModel::Type::Video;
} else {
Q_UNREACHABLE();
}
}, item);
}
-1
View File
@@ -55,5 +55,4 @@ public:
private:
QString m_searchQuery;
std::vector<search::SearchResultItem> m_searchResults;
static int itemType(search::SearchResultItem const &item);
};
+9 -9
View File
@@ -15,7 +15,7 @@
UserPlaylistModel::UserPlaylistModel(QObject *parent)
: AbstractYTMusicModel(parent)
{
connect(this, &UserPlaylistModel::initialVideoIdChanged, this, [this] {
connect(this, &UserPlaylistModel::initialVideoIdChanged, this, [=] {
if (m_initialVideoId.isEmpty()) {
return;
}
@@ -23,7 +23,7 @@ UserPlaylistModel::UserPlaylistModel(QObject *parent)
setLoading(true);
YTMusicThread::instance()->fetchWatchPlaylist(m_initialVideoId);
});
connect(this, &UserPlaylistModel::playlistIdChanged, this, [this] {
connect(this, &UserPlaylistModel::playlistIdChanged, this, [=] {
if (m_playlistId.isEmpty()) {
return;
}
@@ -31,7 +31,7 @@ UserPlaylistModel::UserPlaylistModel(QObject *parent)
setLoading(true);
YTMusicThread::instance()->fetchWatchPlaylist(std::nullopt, m_playlistId);
});
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchWatchPlaylistFinished, this, [this](const watch::Playlist &playlist) {
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::fetchWatchPlaylistFinished, this, [=](const watch::Playlist &playlist) {
setLoading(false);
beginResetModel();
@@ -49,7 +49,7 @@ UserPlaylistModel::UserPlaylistModel(QObject *parent)
setCurrentVideoId({});
}
});
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [this] {
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [=] {
setLoading(false);
});
}
@@ -101,7 +101,7 @@ void UserPlaylistModel::setInitialVideoId(const QString &videoId)
QString UserPlaylistModel::nextVideoId() const
{
auto currentTrack = std::find_if(m_playlist.tracks.begin(), m_playlist.tracks.end(),
[this](const watch::Playlist::Track &track) {
[=](const watch::Playlist::Track &track) {
return track.video_id == m_currentVideoId.toStdString();
});
@@ -129,7 +129,7 @@ void UserPlaylistModel::setCurrentVideoId(const QString &videoId)
bool UserPlaylistModel::canSkip() const
{
const auto currentTrackIt = std::find_if(m_playlist.tracks.begin(), m_playlist.tracks.end(),
[this](const watch::Playlist::Track &track) {
[=](const watch::Playlist::Track &track) {
return track.video_id == m_currentVideoId.toStdString();
});
@@ -157,7 +157,7 @@ void UserPlaylistModel::skipTo(const QString &videoId)
void UserPlaylistModel::playNext(const QString &videoId, const QString &title, const std::vector<meta::Artist> &artists)
{
const auto currentIt = std::find_if(m_playlist.tracks.begin(), m_playlist.tracks.end(),
[this](const watch::Playlist::Track &track) {
[=](const watch::Playlist::Track &track) {
return track.video_id == m_currentVideoId.toStdString();
});
@@ -239,7 +239,7 @@ void UserPlaylistModel::shufflePlaylist()
// Only shuffle playlist after current track
if (!m_currentVideoId.isEmpty()) {
const auto currentIt = std::find_if(m_playlist.tracks.begin(), m_playlist.tracks.end(),
[this](const watch::Playlist::Track &track) {
[=](const watch::Playlist::Track &track) {
return track.video_id == m_currentVideoId.toStdString();
});
@@ -257,7 +257,7 @@ void UserPlaylistModel::emitCurrentVideoChanged(const QString &oldVideoId)
return track.video_id == oldVideoId.toStdString();
});
const auto currentVideoIt = std::find_if(m_playlist.tracks.begin(), m_playlist.tracks.end(),
[this](const watch::Playlist::Track &track) {
[=](const watch::Playlist::Track &track) {
return track.video_id == m_currentVideoId.toStdString();
});
+12 -8
View File
@@ -9,7 +9,7 @@
VideoInfoExtractor::VideoInfoExtractor(QObject *parent)
: QObject(parent)
{
connect(this, &VideoInfoExtractor::videoIdChanged, this, [this] {
connect(this, &VideoInfoExtractor::videoIdChanged, this, [=] {
if (m_videoId.isEmpty()) {
m_videoInfo = {};
Q_EMIT audioUrlChanged();
@@ -23,7 +23,7 @@ VideoInfoExtractor::VideoInfoExtractor(QObject *parent)
YTMusicThread::instance()->extractVideoInfo(QString::fromStdString(m_videoId.toStdString()));
});
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::extractVideoInfoFinished, this, [this](const video_info::VideoInfo &info) {
connect(&YTMusicThread::instance().get(), &AsyncYTMusic::extractVideoInfoFinished, this, [=](const video_info::VideoInfo &info) {
m_videoInfo = info;
setLoading(false);
Q_EMIT audioUrlChanged();
@@ -36,7 +36,7 @@ VideoInfoExtractor::VideoInfoExtractor(QObject *parent)
QUrl VideoInfoExtractor::audioUrl() const
{
if (m_videoInfo.formats.empty()) {
return {};
return QUrl();
}
std::vector<video_info::Format> audioFormats;
@@ -48,13 +48,17 @@ QUrl VideoInfoExtractor::audioUrl() const
});
if (audioFormats.empty()) {
return {};
return QUrl();
}
std::sort(audioFormats.begin(), audioFormats.end(),
[](const video_info::Format &a, const video_info::Format &b) {
return a.quality > b.quality;
});
struct {
bool operator()(const video_info::Format &a, const video_info::Format &b) {
return a.quality > b.quality;
}
} qualitySort;
std::sort(audioFormats.begin(), audioFormats.end(), qualitySort);
return QUrl(QString::fromStdString(audioFormats.front().url));
}
+1 -1
View File
@@ -21,7 +21,7 @@ void pyPrintPretty(py::handle obj) {
#define UNEXPORT __attribute__ ((visibility("hidden")))
constexpr auto TESTED_YTMUSICAPI_VERSION = "0.20.0";
constexpr auto TESTED_YTMUSICAPI_VERSION = "0.19.5";
struct UNEXPORT YTMusicPrivate {
py::scoped_interpreter guard {};