Rename "Internet" to "Streaming"

This commit is contained in:
Jonas Kvinge
2024-06-12 22:23:05 +02:00
parent 718af984ab
commit 299415a889
49 changed files with 615 additions and 615 deletions

View File

@@ -0,0 +1,474 @@
/*
* Strawberry Music Player
* This code was part of Clementine
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <utility>
#include <QWidget>
#include <QTreeView>
#include <QSortFilterProxyModel>
#include <QAbstractItemView>
#include <QItemSelectionModel>
#include <QVariant>
#include <QString>
#include <QPainter>
#include <QRect>
#include <QFont>
#include <QFontMetrics>
#include <QMimeData>
#include <QMenu>
#include <QtEvents>
#include "core/shared_ptr.h"
#include "core/application.h"
#include "core/iconloader.h"
#include "core/mimedata.h"
#include "collection/collectionbackend.h"
#include "collection/collectionmodel.h"
#include "collection/collectionfilterwidget.h"
#include "collection/collectionitem.h"
#include "collection/collectionitemdelegate.h"
#include "streamingcollectionview.h"
StreamingCollectionView::StreamingCollectionView(QWidget *parent)
: AutoExpandingTreeView(parent),
app_(nullptr),
collection_backend_(nullptr),
collection_model_(nullptr),
filter_(nullptr),
favorite_(false),
total_song_count_(0),
total_artist_count_(0),
total_album_count_(0),
nomusic_(QStringLiteral(":/pictures/nomusic.png")),
context_menu_(nullptr),
load_(nullptr),
add_to_playlist_(nullptr),
add_to_playlist_enqueue_(nullptr),
add_to_playlist_enqueue_next_(nullptr),
open_in_new_playlist_(nullptr),
remove_songs_(nullptr),
is_in_keyboard_search_(false) {
setItemDelegate(new CollectionItemDelegate(this));
setAttribute(Qt::WA_MacShowFocusRect, false);
setHeaderHidden(true);
setAllColumnsShowFocus(true);
setDragEnabled(true);
setDragDropMode(QAbstractItemView::DragOnly);
setSelectionMode(QAbstractItemView::ExtendedSelection);
SetAutoOpen(false);
setStyleSheet(QStringLiteral("QTreeView::item{padding-top:1px;}"));
}
void StreamingCollectionView::Init(Application *app, SharedPtr<CollectionBackend> collection_backend, CollectionModel *collection_model, const bool favorite) {
app_ = app;
collection_backend_ = collection_backend;
collection_model_ = collection_model;
favorite_ = favorite;
ReloadSettings();
}
void StreamingCollectionView::SetFilter(CollectionFilterWidget *filter) {
filter_ = filter;
}
void StreamingCollectionView::ReloadSettings() {
if (collection_model_) collection_model_->ReloadSettings();
if (filter_) filter_->ReloadSettings();
}
void StreamingCollectionView::SaveFocus() {
const QModelIndex current = currentIndex();
const QVariant role_type = model()->data(current, CollectionModel::Role_Type);
if (!role_type.isValid()) {
return;
}
const CollectionItem::Type item_type = role_type.value<CollectionItem::Type>();
if (item_type != CollectionItem::Type::Song && item_type != CollectionItem::Type::Container && item_type != CollectionItem::Type::Divider) {
return;
}
last_selected_path_.clear();
last_selected_song_ = Song();
last_selected_container_ = QString();
switch (item_type) {
case CollectionItem::Type::Song:{
QModelIndex idx = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(current);
SongList songs = collection_model_->GetChildSongs(idx);
if (!songs.isEmpty()) {
last_selected_song_ = songs.last();
}
break;
}
case CollectionItem::Type::Container:
case CollectionItem::Type::Divider:{
QString text = model()->data(current, CollectionModel::Role_SortText).toString();
last_selected_container_ = text;
break;
}
default:
return;
}
SaveContainerPath(current);
}
void StreamingCollectionView::SaveContainerPath(const QModelIndex &child) {
const QModelIndex current = model()->parent(child);
const QVariant role_type = model()->data(current, CollectionModel::Role_Type);
if (!role_type.isValid()) {
return;
}
const CollectionItem::Type item_type = role_type.value<CollectionItem::Type>();
if (item_type != CollectionItem::Type::Container && item_type != CollectionItem::Type::Divider) {
return;
}
QString text = model()->data(current, CollectionModel::Role_SortText).toString();
last_selected_path_ << text;
SaveContainerPath(current);
}
void StreamingCollectionView::RestoreFocus() {
if (last_selected_container_.isEmpty() && last_selected_song_.url().isEmpty()) {
return;
}
RestoreLevelFocus();
}
bool StreamingCollectionView::RestoreLevelFocus(const QModelIndex &parent) {
if (model()->canFetchMore(parent)) {
model()->fetchMore(parent);
}
const int rows = model()->rowCount(parent);
for (int i = 0; i < rows; i++) {
const QModelIndex current = model()->index(i, 0, parent);
if (!current.isValid()) continue;
const QVariant role_type = model()->data(current, CollectionModel::Role_Type);
if (!role_type.isValid()) continue;
const CollectionItem::Type item_type = role_type.value<CollectionItem::Type>();
switch (item_type) {
case CollectionItem::Type::Root:
case CollectionItem::Type::LoadingIndicator:
break;
case CollectionItem::Type::Song:
if (!last_selected_song_.url().isEmpty()) {
QModelIndex idx = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(current);
const SongList songs = collection_model_->GetChildSongs(idx);
for (const Song &song : songs) {
if (song == last_selected_song_) {
setCurrentIndex(current);
return true;
}
}
}
break;
case CollectionItem::Type::Container:
case CollectionItem::Type::Divider:{
QString text = model()->data(current, CollectionModel::Role_SortText).toString();
if (!last_selected_container_.isEmpty() && last_selected_container_ == text) {
expand(current);
setCurrentIndex(current);
return true;
}
if (last_selected_path_.contains(text)) {
expand(current);
// If a selected container or song were not found, we've got into a wrong subtree (happens with "unknown" all the time)
if (!RestoreLevelFocus(current)) {
collapse(current);
}
else {
return true;
}
}
break;
}
}
}
return false;
}
void StreamingCollectionView::TotalSongCountUpdated(int count) {
int old = total_song_count_;
total_song_count_ = count;
if (old != total_song_count_) update();
if (total_song_count_ == 0) {
setCursor(Qt::PointingHandCursor);
}
else {
unsetCursor();
}
emit TotalSongCountUpdated_();
}
void StreamingCollectionView::TotalArtistCountUpdated(int count) {
int old = total_artist_count_;
total_artist_count_ = count;
if (old != total_artist_count_) update();
if (total_artist_count_ == 0) {
setCursor(Qt::PointingHandCursor);
}
else {
unsetCursor();
}
emit TotalArtistCountUpdated_();
}
void StreamingCollectionView::TotalAlbumCountUpdated(int count) {
int old = total_album_count_;
total_album_count_ = count;
if (old != total_album_count_) update();
if (total_album_count_ == 0) {
setCursor(Qt::PointingHandCursor);
}
else {
unsetCursor();
}
emit TotalAlbumCountUpdated_();
}
void StreamingCollectionView::paintEvent(QPaintEvent *event) {
if (total_song_count_ == 0) {
QPainter p(viewport());
QRect rect(viewport()->rect());
// Draw the confused strawberry
QRect image_rect((rect.width() - nomusic_.width()) / 2, 50, nomusic_.width(), nomusic_.height());
p.drawPixmap(image_rect, nomusic_);
// Draw the title text
QFont bold_font;
bold_font.setBold(true);
p.setFont(bold_font);
QFontMetrics metrics(bold_font);
QRect title_rect(0, image_rect.bottom() + 20, rect.width(), metrics.height());
p.drawText(title_rect, Qt::AlignHCenter, tr("The streaming collection is empty!"));
// Draw the other text
p.setFont(QFont());
QRect text_rect(0, title_rect.bottom() + 5, rect.width(), metrics.height());
p.drawText(text_rect, Qt::AlignHCenter, tr("Click here to retrieve music"));
}
else {
QTreeView::paintEvent(event);
}
}
void StreamingCollectionView::mouseReleaseEvent(QMouseEvent *e) {
QTreeView::mouseReleaseEvent(e);
if (total_song_count_ == 0) {
emit GetSongs();
}
}
void StreamingCollectionView::contextMenuEvent(QContextMenuEvent *e) {
if (!context_menu_) {
context_menu_ = new QMenu(this);
add_to_playlist_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("media-playback-start")), tr("Append to current playlist"), this, &StreamingCollectionView::AddToPlaylist);
load_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("media-playback-start")), tr("Replace current playlist"), this, &StreamingCollectionView::Load);
open_in_new_playlist_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("document-new")), tr("Open in new playlist"), this, &StreamingCollectionView::OpenInNewPlaylist);
context_menu_->addSeparator();
add_to_playlist_enqueue_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("go-next")), tr("Queue track"), this, &StreamingCollectionView::AddToPlaylistEnqueue);
add_to_playlist_enqueue_next_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("go-next")), tr("Queue to play next"), this, &StreamingCollectionView::AddToPlaylistEnqueueNext);
context_menu_->addSeparator();
if (favorite_) {
remove_songs_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("edit-delete")), tr("Remove from favorites"), this, &StreamingCollectionView::RemoveSelectedSongs);
context_menu_->addSeparator();
}
if (filter_) context_menu_->addMenu(filter_->menu());
}
context_menu_index_ = indexAt(e->pos());
if (!context_menu_index_.isValid()) return;
context_menu_index_ = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(context_menu_index_);
QModelIndexList selected_indexes = qobject_cast<QSortFilterProxyModel*>(model())->mapSelectionToSource(selectionModel()->selection()).indexes();
qint64 songs_selected = selected_indexes.count();
// In all modes
load_->setEnabled(songs_selected > 0);
add_to_playlist_->setEnabled(songs_selected > 0);
open_in_new_playlist_->setEnabled(songs_selected > 0);
add_to_playlist_enqueue_->setEnabled(songs_selected > 0);
if (remove_songs_) remove_songs_->setEnabled(songs_selected > 0);
context_menu_->popup(e->globalPos());
}
void StreamingCollectionView::Load() {
QMimeData *q_mimedata = model()->mimeData(selectedIndexes());
if (MimeData *mimedata = qobject_cast<MimeData*>(q_mimedata)) {
mimedata->clear_first_ = true;
}
emit AddToPlaylistSignal(q_mimedata);
}
void StreamingCollectionView::AddToPlaylist() {
emit AddToPlaylistSignal(model()->mimeData(selectedIndexes()));
}
void StreamingCollectionView::AddToPlaylistEnqueue() {
QMimeData *q_mimedata = model()->mimeData(selectedIndexes());
if (MimeData *mimedata = qobject_cast<MimeData*>(q_mimedata)) {
mimedata->enqueue_now_ = true;
}
emit AddToPlaylistSignal(q_mimedata);
}
void StreamingCollectionView::AddToPlaylistEnqueueNext() {
QMimeData *q_mimedata = model()->mimeData(selectedIndexes());
if (MimeData *mimedata = qobject_cast<MimeData*>(q_mimedata)) {
mimedata->enqueue_next_now_ = true;
}
emit AddToPlaylistSignal(q_mimedata);
}
void StreamingCollectionView::OpenInNewPlaylist() {
QMimeData *q_mimedata = model()->mimeData(selectedIndexes());
if (MimeData *mimedata = qobject_cast<MimeData*>(q_mimedata)) {
mimedata->open_in_new_playlist_ = true;
}
emit AddToPlaylistSignal(q_mimedata);
}
void StreamingCollectionView::RemoveSelectedSongs() {
emit RemoveSongs(GetSelectedSongs());
}
void StreamingCollectionView::keyboardSearch(const QString &search) {
is_in_keyboard_search_ = true;
QTreeView::keyboardSearch(search);
is_in_keyboard_search_ = false;
}
void StreamingCollectionView::scrollTo(const QModelIndex &idx, ScrollHint hint) {
if (is_in_keyboard_search_) {
QTreeView::scrollTo(idx, QAbstractItemView::PositionAtTop);
}
else {
QTreeView::scrollTo(idx, hint);
}
}
SongList StreamingCollectionView::GetSelectedSongs() const {
QModelIndexList selected_indexes = qobject_cast<QSortFilterProxyModel*>(model())->mapSelectionToSource(selectionModel()->selection()).indexes();
return collection_model_->GetChildSongs(selected_indexes);
}
void StreamingCollectionView::FilterReturnPressed() {
if (!currentIndex().isValid()) {
// Pick the first thing that isn't a divider
for (int row = 0; row < model()->rowCount(); ++row) {
QModelIndex idx = model()->index(row, 0);
const QVariant role_type = idx.data(CollectionModel::Role::Role_Type);
if (!role_type.isValid()) continue;
const CollectionItem::Type item_type = role_type.value<CollectionItem::Type>();
if (item_type != CollectionItem::Type::Divider) {
setCurrentIndex(idx);
break;
}
}
}
if (!currentIndex().isValid()) return;
emit doubleClicked(currentIndex());
}
int StreamingCollectionView::TotalSongs() const {
return total_song_count_;
}
int StreamingCollectionView::TotalArtists() const {
return total_artist_count_;
}
int StreamingCollectionView::TotalAlbums() const {
return total_album_count_;
}

View File

@@ -0,0 +1,143 @@
/*
* Strawberry Music Player
* This code was part of Clementine
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef STREAMINGCOLLECTIONVIEW_H
#define STREAMINGCOLLECTIONVIEW_H
#include "config.h"
#include <QObject>
#include <QAbstractItemModel>
#include <QAbstractItemView>
#include <QSet>
#include <QMap>
#include <QString>
#include <QPixmap>
#include "core/shared_ptr.h"
#include "core/song.h"
#include "widgets/autoexpandingtreeview.h"
class QWidget;
class QMenu;
class QAction;
class QContextMenuEvent;
class QMouseEvent;
class QPaintEvent;
class Application;
class CollectionBackend;
class CollectionModel;
class CollectionFilterWidget;
class StreamingCollectionView : public AutoExpandingTreeView {
Q_OBJECT
public:
explicit StreamingCollectionView(QWidget *parent = nullptr);
void Init(Application *app, SharedPtr<CollectionBackend> collection_backend, CollectionModel *collection_model, const bool favorite = false);
// Returns Songs currently selected in the collection view.
// Please note that the selection is recursive meaning that if for example an album is selected this will return all of it's songs.
SongList GetSelectedSongs() const;
void SetApplication(Application *app);
void SetFilter(CollectionFilterWidget *filter);
// QTreeView
void keyboardSearch(const QString &search) override;
void scrollTo(const QModelIndex &idx, ScrollHint hint = EnsureVisible) override;
int TotalSongs() const;
int TotalArtists() const;
int TotalAlbums() const;
public slots:
void TotalSongCountUpdated(int count);
void TotalArtistCountUpdated(int count);
void TotalAlbumCountUpdated(int count);
void ReloadSettings();
void FilterReturnPressed();
void SaveFocus();
void RestoreFocus();
signals:
void GetSongs();
void TotalSongCountUpdated_();
void TotalArtistCountUpdated_();
void TotalAlbumCountUpdated_();
void Error(const QString &error);
void RemoveSongs(const SongList &songs);
protected:
// QWidget
void paintEvent(QPaintEvent *event) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void contextMenuEvent(QContextMenuEvent *e) override;
private slots:
void Load();
void AddToPlaylist();
void AddToPlaylistEnqueue();
void AddToPlaylistEnqueueNext();
void OpenInNewPlaylist();
void RemoveSelectedSongs();
private:
void RecheckIsEmpty();
bool RestoreLevelFocus(const QModelIndex &parent = QModelIndex());
void SaveContainerPath(const QModelIndex &child);
private:
Application *app_;
SharedPtr<CollectionBackend> collection_backend_;
CollectionModel *collection_model_;
CollectionFilterWidget *filter_;
bool favorite_;
int total_song_count_;
int total_artist_count_;
int total_album_count_;
QPixmap nomusic_;
QMenu *context_menu_;
QModelIndex context_menu_index_;
QAction *load_;
QAction *add_to_playlist_;
QAction *add_to_playlist_enqueue_;
QAction *add_to_playlist_enqueue_next_;
QAction *open_in_new_playlist_;
QAction *remove_songs_;
bool is_in_keyboard_search_;
// Save focus
Song last_selected_song_;
QString last_selected_container_;
QSet<QString> last_selected_path_;
};
#endif // STREAMINGCOLLECTIONVIEW_H

View File

@@ -0,0 +1,68 @@
/*
* Strawberry Music Player
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QtGlobal>
#include <QWidget>
#include <QProgressBar>
#include <QKeyEvent>
#include <QContextMenuEvent>
#include "collection/collectionfilterwidget.h"
#include "streamingcollectionview.h"
#include "streamingcollectionviewcontainer.h"
#include "ui_streamingcollectionviewcontainer.h"
StreamingCollectionViewContainer::StreamingCollectionViewContainer(QWidget *parent)
: QWidget(parent),
ui_(new Ui_StreamingCollectionViewContainer) {
ui_->setupUi(this);
ui_->view->SetFilter(ui_->filter_widget);
QObject::connect(ui_->filter_widget, &CollectionFilterWidget::UpPressed, ui_->view, &StreamingCollectionView::UpAndFocus);
QObject::connect(ui_->filter_widget, &CollectionFilterWidget::DownPressed, ui_->view, &StreamingCollectionView::DownAndFocus);
QObject::connect(ui_->filter_widget, &CollectionFilterWidget::ReturnPressed, ui_->view, &StreamingCollectionView::FilterReturnPressed);
QObject::connect(ui_->view, &StreamingCollectionView::FocusOnFilterSignal, ui_->filter_widget, &CollectionFilterWidget::FocusOnFilter);
ui_->progressbar->hide();
ReloadSettings();
}
StreamingCollectionViewContainer::~StreamingCollectionViewContainer() { delete ui_; }
void StreamingCollectionViewContainer::ReloadSettings() const {
ui_->filter_widget->ReloadSettings();
ui_->view->ReloadSettings();
}
bool StreamingCollectionViewContainer::SearchFieldHasFocus() const {
return ui_->filter_widget->SearchFieldHasFocus();
}
void StreamingCollectionViewContainer::FocusSearchField() {
ui_->filter_widget->FocusSearchField();
}
void StreamingCollectionViewContainer::contextMenuEvent(QContextMenuEvent *e) { Q_UNUSED(e); }

View File

@@ -0,0 +1,69 @@
/*
* Strawberry Music Player
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef STREAMINGCOLLECTIONVIEWCONTAINER_H
#define STREAMINGCOLLECTIONVIEWCONTAINER_H
#include "config.h"
#include <QObject>
#include <QWidget>
#include <QString>
#include "streamingcollectionview.h"
#include "ui_streamingcollectionviewcontainer.h"
class QStackedWidget;
class QPushButton;
class QLabel;
class QProgressBar;
class QContextMenuEvent;
class CollectionFilterWidget;
class StreamingCollectionViewContainer : public QWidget {
Q_OBJECT
public:
explicit StreamingCollectionViewContainer(QWidget *parent = nullptr);
~StreamingCollectionViewContainer() override;
void ReloadSettings() const;
bool SearchFieldHasFocus() const;
void FocusSearchField();
QStackedWidget *stacked() const { return ui_->stacked; }
QWidget *help_page() const { return ui_->help_page; }
QWidget *streamingcollection_page() const { return ui_->streamingcollection_page; }
StreamingCollectionView *view() const { return ui_->view; }
CollectionFilterWidget *filter_widget() const { return ui_->filter_widget; }
QPushButton *button_refresh() const { return ui_->refresh; }
QPushButton *button_close() const { return ui_->close; }
QPushButton *button_abort() const { return ui_->abort; }
QLabel *status() const { return ui_->status; }
QProgressBar *progressbar() const { return ui_->progressbar; }
private slots:
void contextMenuEvent(QContextMenuEvent *e) override;
private:
Ui_StreamingCollectionViewContainer *ui_;
};
#endif // STREAMINGCOLLECTIONVIEWCONTAINER_H

View File

@@ -0,0 +1,165 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>StreamingCollectionViewContainer</class>
<widget class="QWidget" name="StreamingCollectionViewContainer">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>300</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QStackedWidget" name="stacked">
<widget class="QWidget" name="help_page">
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QLabel" name="status">
<property name="enabled">
<bool>true</bool>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>40</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressbar">
<property name="enabled">
<bool>true</bool>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="layout_buttons">
<item>
<spacer name="spacer_buttons">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="close">
<property name="text">
<string>Close</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="abort">
<property name="text">
<string>Abort</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="streamingcollection_page">
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="CollectionFilterWidget" name="filter_widget" native="true"/>
</item>
<item>
<widget class="QPushButton" name="refresh">
<property name="text">
<string>Refresh catalogue</string>
</property>
</widget>
</item>
<item>
<widget class="StreamingCollectionView" name="view"/>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>CollectionFilterWidget</class>
<extends>QWidget</extends>
<header>collection/collectionfilterwidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>StreamingCollectionView</class>
<extends>QTreeView</extends>
<header location="global">streaming/streamingcollectionview.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,38 @@
/*
* Strawberry Music Player
* This code was part of Clementine (GlobalSearch)
* Copyright 2012, David Sansome <me@davidsansome.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include <QStyleOptionViewItem>
#include <QPainter>
#include "streamingsearchitemdelegate.h"
#include "streamingsearchview.h"
StreamingSearchItemDelegate::StreamingSearchItemDelegate(StreamingSearchView *view)
: CollectionItemDelegate(view),
view_(view) {}
void StreamingSearchItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &idx) const {
// Tell the view we painted this item, so it can lazy load some art.
const_cast<StreamingSearchView*>(view_)->LazyLoadAlbumCover(idx);
CollectionItemDelegate::paint(painter, option, idx);
}

View File

@@ -0,0 +1,45 @@
/*
* Strawberry Music Player
* This code was part of Clementine (GlobalSearch)
* Copyright 2012, David Sansome <me@davidsansome.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef STREAMINGSEARCHITEMDELEGATE_H
#define STREAMINGSEARCHITEMDELEGATE_H
#include <QStyleOptionViewItem>
#include <QStyleOption>
#include "collection/collectionitemdelegate.h"
class QPainter;
class QModelIndex;
class StreamingSearchView;
class StreamingSearchItemDelegate : public CollectionItemDelegate {
Q_OBJECT
public:
explicit StreamingSearchItemDelegate(StreamingSearchView *view);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &idx) const override;
private:
StreamingSearchView *view_;
};
#endif // STREAMINGSEARCHITEMDELEGATE_H

View File

@@ -0,0 +1,412 @@
/*
* Strawberry Music Player
* This code was part of Clementine (GlobalSearch)
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QStandardItemModel>
#include <QSortFilterProxyModel>
#include <QMimeData>
#include <QList>
#include <QSet>
#include <QVariant>
#include <QString>
#include <QSize>
#include "core/mimedata.h"
#include "core/iconloader.h"
#include "streamsongmimedata.h"
#include "streamingservice.h"
#include "streamingsearchmodel.h"
#include "streamingsearchview.h"
StreamingSearchModel::StreamingSearchModel(StreamingServicePtr service, QObject *parent)
: QStandardItemModel(parent),
service_(service),
proxy_(nullptr),
use_pretty_covers_(true),
artist_icon_(IconLoader::Load(QStringLiteral("folder-sound"))),
album_icon_(IconLoader::Load(QStringLiteral("cdcase"))) {
group_by_[0] = CollectionModel::GroupBy::AlbumArtist;
group_by_[1] = CollectionModel::GroupBy::AlbumDisc;
group_by_[2] = CollectionModel::GroupBy::None;
QList<QSize> nocover_sizes = album_icon_.availableSizes();
no_cover_icon_ = album_icon_.pixmap(nocover_sizes.last()).scaled(CollectionModel::kPrettyCoverSize, CollectionModel::kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
void StreamingSearchModel::AddResults(const StreamingSearchView::ResultList &results) {
for (const StreamingSearchView::Result &result : results) {
QStandardItem *parent = invisibleRootItem();
// Find (or create) the container nodes for this result if we can.
ContainerKey key;
parent = BuildContainers(result.metadata_, parent, &key);
// Create the item
QStandardItem *item = new QStandardItem;
item->setText(result.metadata_.TitleWithCompilationArtist());
item->setData(QVariant::fromValue(result), Role_Result);
parent->appendRow(item);
}
}
QStandardItem *StreamingSearchModel::BuildContainers(const Song &s, QStandardItem *parent, ContainerKey *key, const int level) {
if (level >= 3) {
return parent;
}
bool has_artist_icon = false;
bool has_album_icon = false;
QString display_text;
QString sort_text;
QString unique_tag;
switch (group_by_[level]) {
case CollectionModel::GroupBy::AlbumArtist:
if (s.is_compilation()) {
display_text = tr("Various artists");
sort_text = QLatin1String("aaaaaa");
}
else {
display_text = CollectionModel::TextOrUnknown(s.effective_albumartist());
sort_text = CollectionModel::SortTextForArtist(s.effective_albumartist(), true);
}
has_artist_icon = true;
break;
case CollectionModel::GroupBy::Artist:
if (s.is_compilation()) {
display_text = tr("Various artists");
sort_text = QLatin1String("aaaaaa");
}
else {
display_text = CollectionModel::TextOrUnknown(s.artist());
sort_text = CollectionModel::SortTextForArtist(s.artist(), true);
}
has_artist_icon = true;
break;
case CollectionModel::GroupBy::Album:
display_text = CollectionModel::TextOrUnknown(s.album());
sort_text = CollectionModel::SortTextForArtist(s.album(), true);
unique_tag = s.album_id();
has_album_icon = true;
break;
case CollectionModel::GroupBy::AlbumDisc:{
int disc = qMax(0, s.disc());
display_text = CollectionModel::PrettyAlbumDisc(s.album(), disc);
sort_text = s.album() + CollectionModel::SortTextForNumber(disc);
unique_tag = s.album_id();
has_album_icon = true;
break;
}
case CollectionModel::GroupBy::YearAlbum:{
int year = qMax(0, s.year());
display_text = CollectionModel::PrettyYearAlbum(year, s.album());
sort_text = CollectionModel::SortTextForNumber(year) + s.album();
unique_tag = s.album_id();
has_album_icon = true;
break;
}
case CollectionModel::GroupBy::YearAlbumDisc:{
int year = qMax(0, s.year());
int disc = qMax(0, s.disc());
display_text = CollectionModel::PrettyYearAlbumDisc(year, s.album(), disc);
sort_text = CollectionModel::SortTextForNumber(year) + s.album() + CollectionModel::SortTextForNumber(disc);
unique_tag = s.album_id();
has_album_icon = true;
break;
}
case CollectionModel::GroupBy::OriginalYearAlbum:{
int year = qMax(0, s.effective_originalyear());
display_text = CollectionModel::PrettyYearAlbum(year, s.album());
sort_text = CollectionModel::SortTextForNumber(year) + s.album();
unique_tag = s.album_id();
has_album_icon = true;
break;
}
case CollectionModel::GroupBy::OriginalYearAlbumDisc:{
const int year = qMax(0, s.effective_originalyear());
const int disc = qMax(0, s.disc());
display_text = CollectionModel::PrettyYearAlbumDisc(year, s.album(), disc);
sort_text = CollectionModel::SortTextForNumber(year) + s.album() + CollectionModel::SortTextForNumber(disc);
unique_tag = s.album_id();
has_album_icon = true;
break;
}
case CollectionModel::GroupBy::Disc:
display_text = CollectionModel::PrettyDisc(s.disc());
sort_text = CollectionModel::SortTextForArtist(display_text, true);
has_album_icon = true;
break;
case CollectionModel::GroupBy::Year:{
const int year = qMax(0, s.year());
display_text = QString::number(year);
sort_text = CollectionModel::SortTextForNumber(year) + QLatin1Char(' ');
break;
}
case CollectionModel::GroupBy::OriginalYear:{
const int year = qMax(0, s.effective_originalyear());
display_text = QString::number(year);
sort_text = CollectionModel::SortTextForNumber(year) + QLatin1Char(' ');
break;
}
case CollectionModel::GroupBy::Genre:
display_text = CollectionModel::TextOrUnknown(s.genre());
sort_text = CollectionModel::SortTextForArtist(s.genre(), true);
has_album_icon = true;
break;
case CollectionModel::GroupBy::Composer:
display_text = CollectionModel::TextOrUnknown(s.composer());
sort_text = CollectionModel::SortTextForArtist(s.composer(), true);
has_album_icon = true;
break;
case CollectionModel::GroupBy::Performer:
display_text = CollectionModel::TextOrUnknown(s.performer());
sort_text = CollectionModel::SortTextForArtist(s.performer(), true);
has_album_icon = true;
break;
case CollectionModel::GroupBy::Grouping:
display_text = CollectionModel::TextOrUnknown(s.grouping());
sort_text = CollectionModel::SortTextForArtist(s.grouping(), true);
has_album_icon = true;
break;
case CollectionModel::GroupBy::FileType:
display_text = s.TextForFiletype();
sort_text = display_text;
break;
case CollectionModel::GroupBy::Format:
if (s.samplerate() <= 0) {
display_text = s.TextForFiletype();
}
else {
if (s.bitdepth() <= 0) {
display_text = QStringLiteral("%1 (%2)").arg(s.TextForFiletype(), QString::number(s.samplerate() / 1000.0, 'G', 5));
}
else {
display_text = QStringLiteral("%1 (%2/%3)").arg(s.TextForFiletype(), QString::number(s.samplerate() / 1000.0, 'G', 5), QString::number(s.bitdepth()));
}
}
sort_text = display_text;
break;
case CollectionModel::GroupBy::Samplerate:
display_text = QString::number(s.samplerate());
sort_text = display_text;
break;
case CollectionModel::GroupBy::Bitdepth:
display_text = QString::number(s.bitdepth());
sort_text = display_text;
break;
case CollectionModel::GroupBy::Bitrate:
display_text = QString::number(s.bitrate());
sort_text = display_text;
break;
case CollectionModel::GroupBy::None:
case CollectionModel::GroupBy::GroupByCount:
return parent;
}
if (display_text.isEmpty() || sort_text.isEmpty()) {
display_text = QLatin1String("Unknown");
}
// Find a container for this level
key->group_[level] = display_text + unique_tag;
QStandardItem *container = nullptr;
if (containers_.contains(*key)) {
container = containers_[*key];
}
else {
container = new QStandardItem(display_text);
container->setData(sort_text, CollectionModel::Role_SortText);
container->setData(static_cast<int>(group_by_[level]), CollectionModel::Role_ContainerType);
if (has_artist_icon) {
container->setIcon(artist_icon_);
}
else if (has_album_icon) {
if (use_pretty_covers_) {
container->setData(no_cover_icon_, Qt::DecorationRole);
}
else {
container->setIcon(album_icon_);
}
}
parent->appendRow(container);
containers_[*key] = container;
}
// Create the container for the next level.
return BuildContainers(s, container, key, level + 1);
}
void StreamingSearchModel::Clear() {
containers_.clear();
clear();
}
StreamingSearchView::ResultList StreamingSearchModel::GetChildResults(const QModelIndexList &indexes) const {
QList<QStandardItem*> items;
items.reserve(indexes.count());
for (const QModelIndex &idx : indexes) {
items << itemFromIndex(idx);
}
return GetChildResults(items);
}
StreamingSearchView::ResultList StreamingSearchModel::GetChildResults(const QList<QStandardItem*> &items) const {
StreamingSearchView::ResultList results;
QSet<const QStandardItem*> visited;
for (QStandardItem *item : items) {
GetChildResults(item, &results, &visited);
}
return results;
}
void StreamingSearchModel::GetChildResults(const QStandardItem *item, StreamingSearchView::ResultList *results, QSet<const QStandardItem*> *visited) const {
if (visited->contains(item)) {
return;
}
visited->insert(item);
// Does this item have children?
if (item->rowCount() > 0) {
const QModelIndex parent_proxy_index = proxy_->mapFromSource(item->index());
// Yes - visit all the children, but do so through the proxy, so we get them in the right order.
for (int i = 0; i < item->rowCount(); ++i) {
const QModelIndex proxy_index = parent_proxy_index.model()->index(i, 0, parent_proxy_index);
const QModelIndex idx = proxy_->mapToSource(proxy_index);
GetChildResults(itemFromIndex(idx), results, visited);
}
}
else {
// No - maybe it's a song, add its result if valid
QVariant result = item->data(Role_Result);
if (result.isValid()) {
results->append(result.value<StreamingSearchView::Result>());
}
}
}
QMimeData *StreamingSearchModel::mimeData(const QModelIndexList &indexes) const {
return LoadTracks(GetChildResults(indexes));
}
namespace {
void GatherResults(const QStandardItem *parent, StreamingSearchView::ResultList *results) {
QVariant result_variant = parent->data(StreamingSearchModel::Role_Result);
if (result_variant.isValid()) {
StreamingSearchView::Result result = result_variant.value<StreamingSearchView::Result>();
(*results).append(result);
}
for (int i = 0; i < parent->rowCount(); ++i) {
GatherResults(parent->child(i), results);
}
}
} // namespace
void StreamingSearchModel::SetGroupBy(const CollectionModel::Grouping grouping, const bool regroup_now) {
const CollectionModel::Grouping old_group_by = group_by_;
group_by_ = grouping;
if (regroup_now && group_by_ != old_group_by) {
// Walk the tree gathering the results we have already
StreamingSearchView::ResultList results;
GatherResults(invisibleRootItem(), &results);
// Reset the model and re-add all the results using the new grouping.
Clear();
AddResults(results);
}
}
MimeData *StreamingSearchModel::LoadTracks(const StreamingSearchView::ResultList &results) const {
if (results.isEmpty()) {
return nullptr;
}
SongList songs;
QList<QUrl> urls;
songs.reserve(results.count());
urls.reserve(results.count());
for (const StreamingSearchView::Result &result : results) {
songs.append(result.metadata_);
urls << result.metadata_.url();
}
StreamSongMimeData *streaming_song_mime_data = new StreamSongMimeData(service_);
streaming_song_mime_data->songs = songs;
MimeData *mime_data = streaming_song_mime_data;
mime_data->setUrls(urls);
return mime_data;
}

View File

@@ -0,0 +1,120 @@
/*
* Strawberry Music Player
* This code was part of Clementine (GlobalSearch)
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef STREAMINGSEARCHMODEL_H
#define STREAMINGSEARCHMODEL_H
#include "config.h"
#include <QtGlobal>
#include <QObject>
#include <QAbstractItemModel>
#include <QStandardItemModel>
#include <QStandardItem>
#include <QSet>
#include <QList>
#include <QMap>
#include <QString>
#include <QIcon>
#include <QPixmap>
#include "core/shared_ptr.h"
#include "core/song.h"
#include "collection/collectionmodel.h"
#include "streamingsearchview.h"
class QMimeData;
class QSortFilterProxyModel;
class MimeData;
class StreamingService;
class StreamingSearchModel : public QStandardItemModel {
Q_OBJECT
public:
explicit StreamingSearchModel(SharedPtr<StreamingService> service, QObject *parent = nullptr);
enum Role {
Role_Result = CollectionModel::LastRole,
Role_LazyLoadingArt,
LastRole
};
struct ContainerKey {
QString group_[3];
};
void set_proxy(QSortFilterProxyModel *proxy) { proxy_ = proxy; }
void set_use_pretty_covers(const bool pretty) { use_pretty_covers_ = pretty; }
void SetGroupBy(const CollectionModel::Grouping grouping, const bool regroup_now);
void Clear();
StreamingSearchView::ResultList GetChildResults(const QModelIndexList &indexes) const;
StreamingSearchView::ResultList GetChildResults(const QList<QStandardItem*> &items) const;
QMimeData *mimeData(const QModelIndexList &indexes) const override;
// Loads tracks for results that were previously emitted by ResultsAvailable.
// The implementation creates a SongMimeData with one Song for each Result.
MimeData *LoadTracks(const StreamingSearchView::ResultList &results) const;
public slots:
void AddResults(const StreamingSearchView::ResultList &results);
private:
QStandardItem *BuildContainers(const Song &s, QStandardItem *parent, ContainerKey *key, const int level = 0);
void GetChildResults(const QStandardItem *item, StreamingSearchView::ResultList *results, QSet<const QStandardItem*> *visited) const;
private:
SharedPtr<StreamingService> service_;
QSortFilterProxyModel *proxy_;
bool use_pretty_covers_;
QIcon artist_icon_;
QIcon album_icon_;
QPixmap no_cover_icon_;
CollectionModel::Grouping group_by_;
QMap<ContainerKey, QStandardItem*> containers_;
};
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
inline size_t qHash(const StreamingSearchModel::ContainerKey &key) {
#else
inline uint qHash(const StreamingSearchModel::ContainerKey &key) {
#endif
return qHash(key.group_[0]) ^ qHash(key.group_[1]) ^ qHash(key.group_[2]);
}
inline bool operator<(const StreamingSearchModel::ContainerKey &left, const StreamingSearchModel::ContainerKey &right) {
#define CMP(field) \
if (left.field < right.field) return true; \
if (left.field > right.field) return false
CMP(group_[0]);
CMP(group_[1]);
CMP(group_[2]);
return false;
#undef CMP
}
#endif // STREAMINGSEARCHMODEL_H

View File

@@ -0,0 +1,68 @@
/*
* Strawberry Music Player
* This code was part of Clementine (GlobalSearch)
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QObject>
#include <QAbstractItemModel>
#include <QSortFilterProxyModel>
#include <QString>
#include "collection/collectionmodel.h"
#include "streamingsearchmodel.h"
#include "streamingsearchsortmodel.h"
#include "streamingsearchview.h"
StreamingSearchSortModel::StreamingSearchSortModel(QObject *parent) : QSortFilterProxyModel(parent) {}
bool StreamingSearchSortModel::lessThan(const QModelIndex &left, const QModelIndex &right) const {
// Dividers always go first
if (left.data(CollectionModel::Role_IsDivider).toBool()) return true;
if (right.data(CollectionModel::Role_IsDivider).toBool()) return false;
// Containers go before songs if they're at the same level
const bool left_is_container = left.data(CollectionModel::Role_ContainerType).isValid();
const bool right_is_container = right.data(CollectionModel::Role_ContainerType).isValid();
if (left_is_container && !right_is_container) return true;
if (right_is_container && !left_is_container) return false;
// Containers get sorted on their sort text.
if (left_is_container) {
return QString::localeAwareCompare(left.data(CollectionModel::Role_SortText).toString(), right.data(CollectionModel::Role_SortText).toString()) < 0;
}
// Otherwise we're comparing songs. Sort by disc, track, then title.
const StreamingSearchView::Result r1 = left.data(StreamingSearchModel::Role_Result).value<StreamingSearchView::Result>();
const StreamingSearchView::Result r2 = right.data(StreamingSearchModel::Role_Result).value<StreamingSearchView::Result>();
if (r1.metadata_.disc() < r2.metadata_.disc()) return true;
if (r1.metadata_.disc() > r2.metadata_.disc()) return false;
if (r1.metadata_.track() < r2.metadata_.track()) return true;
if (r1.metadata_.track() > r2.metadata_.track()) return false;
int ret = QString::localeAwareCompare(r1.metadata_.title(), r2.metadata_.title());
if (ret < 0) return true;
if (ret > 0) return false;
return false;
}

View File

@@ -0,0 +1,39 @@
/*
* Strawberry Music Player
* This code was part of Clementine (GlobalSearch)
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef STREAMINGSEARCHSORTMODEL_H
#define STREAMINGSEARCHSORTMODEL_H
#include <QSortFilterProxyModel>
class QObject;
class QModelIndex;
class StreamingSearchSortModel : public QSortFilterProxyModel {
Q_OBJECT
public:
explicit StreamingSearchSortModel(QObject *parent = nullptr);
protected:
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
};
#endif // STREAMINGSEARCHSORTMODEL_H

View File

@@ -0,0 +1,881 @@
/*
* Strawberry Music Player
* This code was part of Clementine (GlobalSearch)
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <memory>
#include <utility>
#include <QtGlobal>
#include <QObject>
#include <QAbstractItemModel>
#include <QStandardItemModel>
#include <QItemSelectionModel>
#include <QSortFilterProxyModel>
#include <QApplication>
#include <QWidget>
#include <QTimer>
#include <QPair>
#include <QList>
#include <QMap>
#include <QString>
#include <QStringList>
#include <QRegularExpression>
#include <QPixmap>
#include <QPixmapCache>
#include <QPainter>
#include <QPalette>
#include <QColor>
#include <QFont>
#include <QSize>
#include <QStandardItem>
#include <QMenu>
#include <QAction>
#include <QActionGroup>
#include <QSettings>
#include <QStackedWidget>
#include <QLabel>
#include <QProgressBar>
#include <QRadioButton>
#include <QScrollArea>
#include <QToolButton>
#include <QEvent>
#include <QTimerEvent>
#include <QKeyEvent>
#include <QContextMenuEvent>
#include <QShowEvent>
#include <QHideEvent>
#include "core/application.h"
#include "core/mimedata.h"
#include "core/iconloader.h"
#include "core/song.h"
#include "core/settings.h"
#include "collection/collectionfilterwidget.h"
#include "collection/collectionmodel.h"
#include "collection/groupbydialog.h"
#include "collection/savedgroupingmanager.h"
#include "covermanager/albumcoverloader.h"
#include "covermanager/albumcoverloaderresult.h"
#include "streamsongmimedata.h"
#include "streamingservice.h"
#include "streamingsearchitemdelegate.h"
#include "streamingsearchmodel.h"
#include "streamingsearchsortmodel.h"
#include "streamingsearchview.h"
#include "ui_streamingsearchview.h"
#include "settings/appearancesettingspage.h"
using std::make_unique;
const int StreamingSearchView::kSwapModelsTimeoutMsec = 250;
const int StreamingSearchView::kDelayedSearchTimeoutMs = 200;
const int StreamingSearchView::kArtHeight = 32;
StreamingSearchView::StreamingSearchView(QWidget *parent)
: QWidget(parent),
app_(nullptr),
service_(nullptr),
ui_(new Ui_StreamingSearchView),
context_menu_(nullptr),
group_by_actions_(nullptr),
front_model_(nullptr),
back_model_(nullptr),
current_model_(nullptr),
front_proxy_(new StreamingSearchSortModel(this)),
back_proxy_(new StreamingSearchSortModel(this)),
current_proxy_(front_proxy_),
swap_models_timer_(new QTimer(this)),
use_pretty_covers_(true),
search_type_(StreamingSearchView::SearchType::Artists),
search_error_(false),
last_search_id_(0),
searches_next_id_(1) {
ui_->setupUi(this);
ui_->search->installEventFilter(this);
ui_->results_stack->installEventFilter(this);
ui_->settings->setIcon(IconLoader::Load(QStringLiteral("configure")));
// Set the appearance of the results list
ui_->results->setItemDelegate(new StreamingSearchItemDelegate(this));
ui_->results->setAttribute(Qt::WA_MacShowFocusRect, false);
ui_->results->setStyleSheet(QStringLiteral("QTreeView::item{padding-top:1px;}"));
// Show the help page initially
ui_->results_stack->setCurrentWidget(ui_->help_page);
ui_->help_frame->setBackgroundRole(QPalette::Base);
// Set the colour of the help text to the disabled window text colour
QPalette help_palette = ui_->label_helptext->palette();
const QColor help_color = help_palette.color(QPalette::Disabled, QPalette::WindowText);
help_palette.setColor(QPalette::Normal, QPalette::WindowText, help_color);
help_palette.setColor(QPalette::Inactive, QPalette::WindowText, help_color);
ui_->label_helptext->setPalette(help_palette);
// Make it bold
QFont help_font = ui_->label_helptext->font();
help_font.setBold(true);
ui_->label_helptext->setFont(help_font);
// Hide progressbar
ui_->progressbar->hide();
ui_->progressbar->reset();
}
StreamingSearchView::~StreamingSearchView() { delete ui_; }
void StreamingSearchView::Init(Application *app, StreamingServicePtr service) {
app_ = app;
service_ = service;
front_model_ = new StreamingSearchModel(service, this);
back_model_ = new StreamingSearchModel(service, this);
front_proxy_ = new StreamingSearchSortModel(this);
back_proxy_ = new StreamingSearchSortModel(this);
front_model_->set_proxy(front_proxy_);
back_model_->set_proxy(back_proxy_);
current_model_ = front_model_;
current_proxy_ = front_proxy_;
// Set up the sorting proxy model
front_proxy_->setSourceModel(front_model_);
front_proxy_->setDynamicSortFilter(true);
front_proxy_->sort(0);
back_proxy_->setSourceModel(back_model_);
back_proxy_->setDynamicSortFilter(true);
back_proxy_->sort(0);
// Add actions to the settings menu
group_by_actions_ = CollectionFilterWidget::CreateGroupByActions(SavedGroupingManager::GetSavedGroupingsSettingsGroup(service_->settings_group()), this);
QMenu *settings_menu = new QMenu(this);
settings_menu->addActions(group_by_actions_->actions());
settings_menu->addSeparator();
settings_menu->addAction(IconLoader::Load(QStringLiteral("configure")), tr("Configure %1...").arg(Song::DescriptionForSource(service_->source())), this, &StreamingSearchView::OpenSettingsDialog);
ui_->settings->setMenu(settings_menu);
swap_models_timer_->setSingleShot(true);
swap_models_timer_->setInterval(kSwapModelsTimeoutMsec);
QObject::connect(swap_models_timer_, &QTimer::timeout, this, &StreamingSearchView::SwapModels);
QObject::connect(ui_->radiobutton_search_artists, &QRadioButton::clicked, this, &StreamingSearchView::SearchArtistsClicked);
QObject::connect(ui_->radiobutton_search_albums, &QRadioButton::clicked, this, &StreamingSearchView::SearchAlbumsClicked);
QObject::connect(ui_->radiobutton_search_songs, &QRadioButton::clicked, this, &StreamingSearchView::SearchSongsClicked);
QObject::connect(group_by_actions_, &QActionGroup::triggered, this, &StreamingSearchView::GroupByClicked);
QObject::connect(group_by_actions_, &QActionGroup::triggered, this, &StreamingSearchView::GroupByClicked);
QObject::connect(ui_->search, &QSearchField::textChanged, this, &StreamingSearchView::TextEdited);
QObject::connect(ui_->results, &AutoExpandingTreeView::AddToPlaylistSignal, this, &StreamingSearchView::AddToPlaylist);
QObject::connect(ui_->results, &AutoExpandingTreeView::FocusOnFilterSignal, this, &StreamingSearchView::FocusOnFilter);
QObject::connect(&*service_, &StreamingService::SearchUpdateStatus, this, &StreamingSearchView::UpdateStatus);
QObject::connect(&*service_, &StreamingService::SearchProgressSetMaximum, this, &StreamingSearchView::ProgressSetMaximum);
QObject::connect(&*service_, &StreamingService::SearchUpdateProgress, this, &StreamingSearchView::UpdateProgress);
QObject::connect(&*service_, &StreamingService::SearchResults, this, &StreamingSearchView::SearchDone);
QObject::connect(app_, &Application::SettingsChanged, this, &StreamingSearchView::ReloadSettings);
QObject::connect(&*app_->album_cover_loader(), &AlbumCoverLoader::AlbumCoverLoaded, this, &StreamingSearchView::AlbumCoverLoaded);
QObject::connect(ui_->settings, &QToolButton::clicked, ui_->settings, &QToolButton::showMenu);
ReloadSettings();
}
void StreamingSearchView::ReloadSettings() {
Settings s;
// Collection settings
s.beginGroup(service_->settings_group());
use_pretty_covers_ = s.value("pretty_covers", true).toBool();
front_model_->set_use_pretty_covers(use_pretty_covers_);
back_model_->set_use_pretty_covers(use_pretty_covers_);
// Streaming search settings
search_type_ = static_cast<StreamingSearchView::SearchType>(s.value("type", static_cast<int>(StreamingSearchView::SearchType::Artists)).toInt());
switch (search_type_) {
case StreamingSearchView::SearchType::Artists:
ui_->radiobutton_search_artists->setChecked(true);
break;
case StreamingSearchView::SearchType::Albums:
ui_->radiobutton_search_albums->setChecked(true);
break;
case StreamingSearchView::SearchType::Songs:
ui_->radiobutton_search_songs->setChecked(true);
break;
}
int group_by_version = s.value("search_group_by_version", 0).toInt();
if (group_by_version == 1 && s.contains("search_group_by1") && s.contains("search_group_by2") && s.contains("search_group_by3")) {
SetGroupBy(CollectionModel::Grouping(
static_cast<CollectionModel::GroupBy>(s.value("search_group_by1", static_cast<int>(CollectionModel::GroupBy::AlbumArtist)).toInt()),
static_cast<CollectionModel::GroupBy>(s.value("search_group_by2", static_cast<int>(CollectionModel::GroupBy::AlbumDisc)).toInt()),
static_cast<CollectionModel::GroupBy>(s.value("search_group_by3", static_cast<int>(CollectionModel::GroupBy::None)).toInt())));
}
else {
SetGroupBy(CollectionModel::Grouping(CollectionModel::GroupBy::AlbumArtist, CollectionModel::GroupBy::AlbumDisc, CollectionModel::GroupBy::None));
}
s.endGroup();
s.beginGroup(AppearanceSettingsPage::kSettingsGroup);
int iconsize = s.value(AppearanceSettingsPage::kIconSizeConfigureButtons, 20).toInt();
s.endGroup();
ui_->settings->setIconSize(QSize(iconsize, iconsize));
ui_->search->setIconSize(iconsize);
}
void StreamingSearchView::showEvent(QShowEvent *e) {
QWidget::showEvent(e);
#ifndef Q_OS_MACOS
FocusSearchField();
#endif
}
bool StreamingSearchView::eventFilter(QObject *object, QEvent *e) {
if (object == ui_->search && e->type() == QEvent::KeyRelease) {
if (SearchKeyEvent(static_cast<QKeyEvent*>(e))) {
return true;
}
}
else if (object == ui_->results_stack && e->type() == QEvent::ContextMenu) {
if (ResultsContextMenuEvent(static_cast<QContextMenuEvent*>(e))) {
return true;
}
}
return QWidget::eventFilter(object, e);
}
bool StreamingSearchView::SearchKeyEvent(QKeyEvent *e) {
switch (e->key()) {
case Qt::Key_Up:
ui_->results->UpAndFocus();
break;
case Qt::Key_Down:
ui_->results->DownAndFocus();
break;
case Qt::Key_Escape:
ui_->search->clear();
break;
case Qt::Key_Return:
TextEdited(ui_->search->text());
break;
default:
return false;
}
e->accept();
return true;
}
bool StreamingSearchView::ResultsContextMenuEvent(QContextMenuEvent *e) {
if (!context_menu_) {
context_menu_ = new QMenu(this);
context_actions_ << context_menu_->addAction(IconLoader::Load(QStringLiteral("media-playback-start")), tr("Append to current playlist"), this, &StreamingSearchView::AddSelectedToPlaylist);
context_actions_ << context_menu_->addAction(IconLoader::Load(QStringLiteral("media-playback-start")), tr("Replace current playlist"), this, &StreamingSearchView::LoadSelected);
context_actions_ << context_menu_->addAction(IconLoader::Load(QStringLiteral("document-new")), tr("Open in new playlist"), this, &StreamingSearchView::OpenSelectedInNewPlaylist);
context_menu_->addSeparator();
context_actions_ << context_menu_->addAction(IconLoader::Load(QStringLiteral("go-next")), tr("Queue track"), this, &StreamingSearchView::AddSelectedToPlaylistEnqueue);
context_menu_->addSeparator();
if (service_->artists_collection_model() || service_->albums_collection_model() || service_->songs_collection_model()) {
if (service_->artists_collection_model()) {
context_actions_ << context_menu_->addAction(IconLoader::Load(QStringLiteral("folder-new")), tr("Add to artists"), this, &StreamingSearchView::AddArtists);
}
if (service_->albums_collection_model()) {
context_actions_ << context_menu_->addAction(IconLoader::Load(QStringLiteral("folder-new")), tr("Add to albums"), this, &StreamingSearchView::AddAlbums);
}
if (service_->songs_collection_model()) {
context_actions_ << context_menu_->addAction(IconLoader::Load(QStringLiteral("folder-new")), tr("Add to songs"), this, &StreamingSearchView::AddSongs);
}
context_menu_->addSeparator();
}
if (ui_->results->selectionModel() && ui_->results->selectionModel()->selectedRows().length() == 1) {
context_actions_ << context_menu_->addAction(IconLoader::Load(QStringLiteral("search")), tr("Search for this"), this, &StreamingSearchView::SearchForThis);
}
context_menu_->addSeparator();
context_menu_->addMenu(tr("Group by"))->addActions(group_by_actions_->actions());
context_menu_->addAction(IconLoader::Load(QStringLiteral("configure")), tr("Configure %1...").arg(Song::TextForSource(service_->source())), this, &StreamingSearchView::OpenSettingsDialog);
}
const bool enable_context_actions = ui_->results->selectionModel() && ui_->results->selectionModel()->hasSelection();
for (QAction *action : std::as_const(context_actions_)) {
action->setEnabled(enable_context_actions);
}
context_menu_->popup(e->globalPos());
return true;
}
void StreamingSearchView::timerEvent(QTimerEvent *e) {
QMap<int, DelayedSearch>::iterator it = delayed_searches_.find(e->timerId());
if (it != delayed_searches_.end()) {
SearchAsync(it.value().id_, it.value().query_, it.value().type_);
delayed_searches_.erase(it); // clazy:exclude=strict-iterators
return;
}
QObject::timerEvent(e);
}
void StreamingSearchView::StartSearch(const QString &query) {
ui_->search->setText(query);
TextEdited(query);
// Swap models immediately
swap_models_timer_->stop();
SwapModels();
}
void StreamingSearchView::TextEdited(const QString &text) {
const QString trimmed(text.trimmed());
search_error_ = false;
cover_loader_tasks_.clear();
// Add results to the back model, switch models after some delay.
back_model_->Clear();
current_model_ = back_model_;
current_proxy_ = back_proxy_;
swap_models_timer_->start();
// Cancel the last search (if any) and start the new one.
CancelSearch(last_search_id_);
// If text query is empty, don't start a new search
if (trimmed.isEmpty()) {
last_search_id_ = -1;
ui_->label_helptext->setText(tr("Enter search terms above to find music"));
ui_->label_status->clear();
ui_->progressbar->hide();
ui_->progressbar->reset();
}
else {
ui_->progressbar->reset();
last_search_id_ = SearchAsync(trimmed, search_type_);
}
}
void StreamingSearchView::SwapModels() {
cover_loader_tasks_.clear();
std::swap(front_model_, back_model_);
std::swap(front_proxy_, back_proxy_);
ui_->results->setModel(front_proxy_);
if (ui_->search->text().trimmed().isEmpty() || search_error_) {
ui_->results_stack->setCurrentWidget(ui_->help_page);
}
else {
ui_->results_stack->setCurrentWidget(ui_->results_page);
}
}
QStringList StreamingSearchView::TokenizeQuery(const QString &query) {
QStringList tokens(query.split(QRegularExpression(QStringLiteral("\\s+"))));
for (QStringList::iterator it = tokens.begin(); it != tokens.end(); ++it) {
(*it).remove(QLatin1Char('('));
(*it).remove(QLatin1Char(')'));
(*it).remove(QLatin1Char('"'));
const qint64 colon = (*it).indexOf(QLatin1Char(':'));
if (colon != -1) {
(*it).remove(0, colon + 1);
}
}
return tokens;
}
bool StreamingSearchView::Matches(const QStringList &tokens, const QString &string) {
for (const QString &token : tokens) {
if (!string.contains(token, Qt::CaseInsensitive)) {
return false;
}
}
return true;
}
int StreamingSearchView::SearchAsync(const QString &query, const SearchType type) {
const int id = searches_next_id_++;
int timer_id = startTimer(kDelayedSearchTimeoutMs);
delayed_searches_[timer_id].id_ = id;
delayed_searches_[timer_id].query_ = query;
delayed_searches_[timer_id].type_ = type;
return id;
}
void StreamingSearchView::SearchAsync(const int id, const QString &query, const SearchType type) {
const int service_id = service_->Search(query, type);
pending_searches_[service_id] = PendingState(id, TokenizeQuery(query));
}
void StreamingSearchView::SearchDone(const int service_id, const SongMap &songs, const QString &error) {
if (!pending_searches_.contains(service_id)) return;
// Map back to the original id.
const PendingState state = pending_searches_.take(service_id);
const int search_id = state.orig_id_;
if (songs.isEmpty()) {
SearchError(search_id, error);
return;
}
ResultList results;
results.reserve(songs.count());
for (const Song &song : songs) {
Result result;
result.metadata_ = song;
results << result;
}
// Load cached pixmaps into the results
for (StreamingSearchView::ResultList::iterator it = results.begin(); it != results.end(); ++it) {
it->pixmap_cache_key_ = PixmapCacheKey(*it);
}
AddResults(search_id, results);
}
void StreamingSearchView::CancelSearch(const int id) {
QMap<int, DelayedSearch>::iterator it;
for (it = delayed_searches_.begin(); it != delayed_searches_.end(); ++it) {
if (it.value().id_ == id) {
killTimer(it.key());
delayed_searches_.erase(it); // clazy:exclude=strict-iterators
return;
}
}
service_->CancelSearch();
}
void StreamingSearchView::AddResults(const int id, const StreamingSearchView::ResultList &results) {
if (id != last_search_id_ || results.isEmpty()) return;
ui_->label_status->clear();
ui_->progressbar->reset();
ui_->progressbar->hide();
current_model_->AddResults(results);
}
void StreamingSearchView::SearchError(const int id, const QString &error) {
if (id != last_search_id_) return;
search_error_ = true;
ui_->label_helptext->setText(error);
ui_->label_status->clear();
ui_->progressbar->reset();
ui_->progressbar->hide();
ui_->results_stack->setCurrentWidget(ui_->help_page);
}
void StreamingSearchView::UpdateStatus(const int service_id, const QString &text) {
if (!pending_searches_.contains(service_id)) return;
const PendingState state = pending_searches_[service_id];
const int search_id = state.orig_id_;
if (search_id != last_search_id_) return;
ui_->progressbar->show();
ui_->label_status->setText(text);
}
void StreamingSearchView::ProgressSetMaximum(const int service_id, const int max) {
if (!pending_searches_.contains(service_id)) return;
const PendingState state = pending_searches_[service_id];
const int search_id = state.orig_id_;
if (search_id != last_search_id_) return;
ui_->progressbar->setMaximum(max);
}
void StreamingSearchView::UpdateProgress(const int service_id, const int progress) {
if (!pending_searches_.contains(service_id)) return;
const PendingState state = pending_searches_[service_id];
const int search_id = state.orig_id_;
if (search_id != last_search_id_) return;
ui_->progressbar->setValue(progress);
}
MimeData *StreamingSearchView::SelectedMimeData() {
if (!ui_->results->selectionModel()) return nullptr;
// Get all selected model indexes
QModelIndexList indexes = ui_->results->selectionModel()->selectedRows();
if (indexes.isEmpty()) {
// There's nothing selected - take the first thing in the model that isn't a divider.
for (int i = 0; i < front_proxy_->rowCount(); ++i) {
QModelIndex idx = front_proxy_->index(i, 0);
if (!idx.data(CollectionModel::Role_IsDivider).toBool()) {
indexes << idx; // clazy:exclude=reserve-candidates
ui_->results->setCurrentIndex(idx);
break;
}
}
}
// Still got nothing? Give up.
if (indexes.isEmpty()) {
return nullptr;
}
// Get items for these indexes
QList<QStandardItem*> items;
for (const QModelIndex &idx : std::as_const(indexes)) {
items << (front_model_->itemFromIndex(front_proxy_->mapToSource(idx))); // clazy:exclude=reserve-candidates
}
// Get a MimeData for these items
return front_model_->LoadTracks(front_model_->GetChildResults(items));
}
void StreamingSearchView::AddSelectedToPlaylist() {
emit AddToPlaylist(SelectedMimeData());
}
void StreamingSearchView::LoadSelected() {
MimeData *mimedata = SelectedMimeData();
if (!mimedata) return;
mimedata->clear_first_ = true;
emit AddToPlaylist(mimedata);
}
void StreamingSearchView::AddSelectedToPlaylistEnqueue() {
MimeData *mimedata = SelectedMimeData();
if (!mimedata) return;
mimedata->enqueue_now_ = true;
emit AddToPlaylist(mimedata);
}
void StreamingSearchView::OpenSelectedInNewPlaylist() {
MimeData *mimedata = SelectedMimeData();
if (!mimedata) return;
mimedata->open_in_new_playlist_ = true;
emit AddToPlaylist(mimedata);
}
void StreamingSearchView::SearchForThis() {
StartSearch(ui_->results->selectionModel()->selectedRows().first().data().toString());
}
bool StreamingSearchView::SearchFieldHasFocus() const {
return ui_->search->hasFocus();
}
void StreamingSearchView::FocusSearchField() {
ui_->search->setFocus();
ui_->search->selectAll();
}
void StreamingSearchView::FocusOnFilter(QKeyEvent *e) {
ui_->search->setFocus();
QApplication::sendEvent(ui_->search, e);
}
void StreamingSearchView::OpenSettingsDialog() {
app_->OpenSettingsDialogAtPage(service_->settings_page());
}
void StreamingSearchView::GroupByClicked(QAction *action) {
if (action->property("group_by").isNull()) {
if (!group_by_dialog_) {
group_by_dialog_ = make_unique<GroupByDialog>();
QObject::connect(&*group_by_dialog_, &GroupByDialog::Accepted, this, &StreamingSearchView::SetGroupBy);
}
group_by_dialog_->show();
return;
}
SetGroupBy(action->property("group_by").value<CollectionModel::Grouping>());
}
void StreamingSearchView::SetGroupBy(const CollectionModel::Grouping g) {
// Clear requests: changing "group by" on the models will cause all the items to be removed/added again,
// so all the QModelIndex here will become invalid. New requests will be created for those
// songs when they will be displayed again anyway (when StreamingSearchItemDelegate::paint will call LazyLoadAlbumCover)
cover_loader_tasks_.clear();
// Update the models
front_model_->SetGroupBy(g, true);
back_model_->SetGroupBy(g, false);
// Save the setting
Settings s;
s.beginGroup(service_->settings_group());
s.setValue("search_group_by_version", 1);
s.setValue("search_group_by1", static_cast<int>(g.first));
s.setValue("search_group_by2", static_cast<int>(g.second));
s.setValue("search_group_by3", static_cast<int>(g.third));
s.endGroup();
// Make sure the correct action is checked.
const QList<QAction*> actions = group_by_actions_->actions();
for (QAction *action : actions) {
if (action->property("group_by").isNull()) continue;
if (g == action->property("group_by").value<CollectionModel::Grouping>()) {
action->setChecked(true);
return;
}
}
// Check the advanced action
actions.last()->setChecked(true);
}
void StreamingSearchView::SearchArtistsClicked(const bool) {
SetSearchType(StreamingSearchView::SearchType::Artists);
}
void StreamingSearchView::SearchAlbumsClicked(const bool) {
SetSearchType(StreamingSearchView::SearchType::Albums);
}
void StreamingSearchView::SearchSongsClicked(const bool) {
SetSearchType(StreamingSearchView::SearchType::Songs);
}
void StreamingSearchView::SetSearchType(const StreamingSearchView::SearchType type) {
search_type_ = type;
Settings s;
s.beginGroup(service_->settings_group());
s.setValue("type", static_cast<int>(search_type_));
s.endGroup();
TextEdited(ui_->search->text());
}
void StreamingSearchView::AddArtists() {
MimeData *mimedata = SelectedMimeData();
if (!mimedata) return;
if (const StreamSongMimeData *streaming_song_data = qobject_cast<const StreamSongMimeData*>(mimedata)) {
emit AddArtistsSignal(streaming_song_data->songs);
}
}
void StreamingSearchView::AddAlbums() {
MimeData *mimedata = SelectedMimeData();
if (!mimedata) return;
if (const StreamSongMimeData *streaming_song_data = qobject_cast<const StreamSongMimeData*>(mimedata)) {
emit AddAlbumsSignal(streaming_song_data->songs);
}
}
void StreamingSearchView::AddSongs() {
MimeData *mimedata = SelectedMimeData();
if (!mimedata) return;
if (const StreamSongMimeData *streaming_song_data = qobject_cast<const StreamSongMimeData*>(mimedata)) {
emit AddSongsSignal(streaming_song_data->songs);
}
}
QString StreamingSearchView::PixmapCacheKey(const StreamingSearchView::Result &result) const {
if (result.metadata_.art_automatic_is_valid()) {
return Song::TextForSource(service_->source()) + QLatin1Char('/') + result.metadata_.art_automatic().toString();
}
if (!result.metadata_.effective_albumartist().isEmpty() && !result.metadata_.album().isEmpty()) {
return Song::TextForSource(service_->source()) + QLatin1Char('/') + result.metadata_.effective_albumartist() + QLatin1Char('/') + result.metadata_.album();
}
return Song::TextForSource(service_->source()) + QLatin1Char('/') + result.metadata_.url().toString();
}
bool StreamingSearchView::FindCachedPixmap(const StreamingSearchView::Result &result, QPixmap *pixmap) const {
return QPixmapCache::find(result.pixmap_cache_key_, pixmap);
}
void StreamingSearchView::LazyLoadAlbumCover(const QModelIndex &proxy_index) {
if (!proxy_index.isValid() || proxy_index.model() != front_proxy_) {
return;
}
const QModelIndex source_index = front_proxy_->mapToSource(proxy_index);
if (!source_index.isValid()) {
return;
}
// Already loading art for this item?
if (source_index.data(StreamingSearchModel::Role_LazyLoadingArt).isValid()) {
return;
}
// Should we even load art at all?
if (!use_pretty_covers_) {
return;
}
// Is this an album?
const CollectionModel::GroupBy container_type = static_cast<CollectionModel::GroupBy>(proxy_index.data(CollectionModel::Role_ContainerType).toInt());
if (!CollectionModel::IsAlbumGroupBy(container_type)) return;
// Mark the item as loading art
QStandardItem *item_album = front_model_->itemFromIndex(source_index);
if (!item_album) {
return;
}
item_album->setData(true, StreamingSearchModel::Role_LazyLoadingArt);
// Walk down the item's children until we find a track
QStandardItem *item_song = item_album;
while (item_song->rowCount() > 0) {
item_song = item_song->child(0);
}
// Get the track's Result
const StreamingSearchView::Result result = item_song->data(StreamingSearchModel::Role_Result).value<StreamingSearchView::Result>();
QPixmap cached_pixmap;
if (QPixmapCache::find(result.pixmap_cache_key_, &cached_pixmap)) {
item_album->setData(cached_pixmap, Qt::DecorationRole);
}
else {
AlbumCoverLoaderOptions cover_loader_options(AlbumCoverLoaderOptions::Option::ScaledImage | AlbumCoverLoaderOptions::Option::PadScaledImage);
cover_loader_options.desired_scaled_size = QSize(kArtHeight, kArtHeight);
quint64 loader_id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options, result.metadata_);
cover_loader_tasks_[loader_id] = qMakePair(source_index, result.pixmap_cache_key_);
}
}
void StreamingSearchView::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &albumcover_result) {
if (!cover_loader_tasks_.contains(id)) return;
QPair<QModelIndex, QString> cover_loader_task = cover_loader_tasks_.take(id);
QModelIndex idx = cover_loader_task.first;
QString key = cover_loader_task.second;
if (albumcover_result.success && !albumcover_result.image_scaled.isNull()) {
QPixmap pixmap = QPixmap::fromImage(albumcover_result.image_scaled);
if (!pixmap.isNull()) {
QPixmapCache::insert(key, pixmap);
}
if (idx.isValid()) {
QStandardItem *item = front_model_->itemFromIndex(idx);
if (item) {
item->setData(albumcover_result.image_scaled, Qt::DecorationRole);
}
}
}
}

View File

@@ -0,0 +1,221 @@
/*
* Strawberry Music Player
* This code was part of Clementine (GlobalSearch)
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef STREAMINGSEARCHVIEW_H
#define STREAMINGSEARCHVIEW_H
#include "config.h"
#include <QtGlobal>
#include <QObject>
#include <QWidget>
#include <QSet>
#include <QPair>
#include <QList>
#include <QMap>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QImage>
#include <QPixmap>
#include <QMetaType>
#include "core/scoped_ptr.h"
#include "core/shared_ptr.h"
#include "core/song.h"
#include "collection/collectionmodel.h"
#include "covermanager/albumcoverloaderresult.h"
class QSortFilterProxyModel;
class QMimeData;
class QTimer;
class QMenu;
class QAction;
class QActionGroup;
class QEvent;
class QKeyEvent;
class QShowEvent;
class QContextMenuEvent;
class QTimerEvent;
class Application;
class MimeData;
class GroupByDialog;
class StreamingService;
class StreamingSearchModel;
class Ui_StreamingSearchView;
class StreamingSearchView : public QWidget {
Q_OBJECT
public:
explicit StreamingSearchView(QWidget *parent = nullptr);
~StreamingSearchView() override;
enum class SearchType {
Artists = 1,
Albums = 2,
Songs = 3
};
struct Result {
Song metadata_;
QString pixmap_cache_key_;
};
using ResultList = QList<Result>;
void Init(Application *app, SharedPtr<StreamingService> service);
bool SearchFieldHasFocus() const;
void FocusSearchField();
void LazyLoadAlbumCover(const QModelIndex &proxy_index);
protected:
struct PendingState {
PendingState() : orig_id_(-1) {}
PendingState(int orig_id, const QStringList &tokens) : orig_id_(orig_id), tokens_(tokens) {}
int orig_id_;
QStringList tokens_;
bool operator<(const PendingState &b) const {
return orig_id_ < b.orig_id_;
}
bool operator==(const PendingState &b) const {
return orig_id_ == b.orig_id_;
}
};
void showEvent(QShowEvent *e) override;
bool eventFilter(QObject *object, QEvent *e) override;
void timerEvent(QTimerEvent *e) override;
// These functions treat queries in the same way as CollectionQuery.
// They're useful for figuring out whether you got a result because it matched in the song title or the artist/album name.
static QStringList TokenizeQuery(const QString &query);
static bool Matches(const QStringList &tokens, const QString &string);
private:
struct DelayedSearch {
int id_;
QString query_;
SearchType type_;
};
bool SearchKeyEvent(QKeyEvent *e);
bool ResultsContextMenuEvent(QContextMenuEvent *e);
MimeData *SelectedMimeData();
void SetSearchType(const SearchType type);
int SearchAsync(const QString &query, SearchType type);
void SearchAsync(const int id, const QString &query, const SearchType type);
void SearchError(const int id, const QString &error);
void CancelSearch(const int id);
QString PixmapCacheKey(const Result &result) const;
bool FindCachedPixmap(const Result &result, QPixmap *pixmap) const;
int LoadAlbumCoverAsync(const Result &result);
signals:
void AddToPlaylist(QMimeData*);
void AddArtistsSignal(const SongList &songs);
void AddAlbumsSignal(const SongList &songs);
void AddSongsSignal(const SongList &songs);
private slots:
void SwapModels();
void TextEdited(const QString &text);
void StartSearch(const QString &query);
void SearchDone(const int service_id, const SongMap &songs, const QString &error);
void UpdateStatus(const int service_id, const QString &text);
void ProgressSetMaximum(const int service_id, const int max);
void UpdateProgress(const int service_id, const int progress);
void AddResults(const int service_id, const StreamingSearchView::ResultList &results);
void FocusOnFilter(QKeyEvent *e);
void AddSelectedToPlaylist();
void LoadSelected();
void OpenSelectedInNewPlaylist();
void AddSelectedToPlaylistEnqueue();
void AddArtists();
void AddAlbums();
void AddSongs();
void SearchForThis();
void OpenSettingsDialog();
void SearchArtistsClicked(const bool);
void SearchAlbumsClicked(const bool);
void SearchSongsClicked(const bool);
void GroupByClicked(QAction *action);
void SetGroupBy(const CollectionModel::Grouping g);
void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &albumcover_result);
public slots:
void ReloadSettings();
private:
static const int kSwapModelsTimeoutMsec;
static const int kDelayedSearchTimeoutMs;
static const int kArtHeight;
private:
Application *app_;
SharedPtr<StreamingService> service_;
Ui_StreamingSearchView *ui_;
ScopedPtr<GroupByDialog> group_by_dialog_;
QMenu *context_menu_;
QList<QAction*> context_actions_;
QActionGroup *group_by_actions_;
// Like graphics APIs have a front buffer and a back buffer, there's a front model and a back model
// The front model is the one that's shown in the UI and the back model is the one that lies in wait.
// current_model_ will point to either the front or the back model.
StreamingSearchModel *front_model_;
StreamingSearchModel *back_model_;
StreamingSearchModel *current_model_;
QSortFilterProxyModel *front_proxy_;
QSortFilterProxyModel *back_proxy_;
QSortFilterProxyModel *current_proxy_;
QTimer *swap_models_timer_;
bool use_pretty_covers_;
SearchType search_type_;
bool search_error_;
int last_search_id_;
int searches_next_id_;
QMap<int, DelayedSearch> delayed_searches_;
QMap<int, PendingState> pending_searches_;
QMap<quint64, QPair<QModelIndex, QString>> cover_loader_tasks_;
};
Q_DECLARE_METATYPE(StreamingSearchView::Result)
Q_DECLARE_METATYPE(StreamingSearchView::ResultList)
#endif // STREAMINGSEARCHVIEW_H

View File

@@ -0,0 +1,312 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>StreamingSearchView</class>
<widget class="QWidget" name="StreamingSearchView">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>660</height>
</rect>
</property>
<property name="windowTitle">
<string>Streaming Search View</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="widget_search" native="true">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<item>
<layout class="QVBoxLayout" name="layout_top" stretch="0,0,0">
<item>
<layout class="QHBoxLayout" name="layout_search">
<item>
<widget class="QSearchField" name="search" native="true">
<property name="placeholderText" stdset="0">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="settings">
<property name="minimumSize">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
<property name="accessibleName">
<string>MenuPopupToolButton</string>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="popupMode">
<enum>QToolButton::MenuButtonPopup</enum>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QWidget" name="widget_searchby" native="true">
<property name="minimumSize">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>2</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QRadioButton" name="radiobutton_search_artists">
<property name="text">
<string>artists</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radiobutton_search_albums">
<property name="text">
<string>albums</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radiobutton_search_songs">
<property name="text">
<string>songs</string>
</property>
</widget>
</item>
<item>
<spacer name="spacer_searchby">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="layout_progress">
<item>
<widget class="QLabel" name="label_status">
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressbar">
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QStackedWidget" name="results_stack">
<property name="currentIndex">
<number>1</number>
</property>
<widget class="QWidget" name="results_page">
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="AutoExpandingTreeView" name="results">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragOnly</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="allColumnsShowFocus">
<bool>true</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="help_page">
<layout class="QVBoxLayout" name="verticalLayout_6">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="help_frame">
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="help_frame_contents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>398</width>
<height>518</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QWidget" name="widget" native="true">
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="leftMargin">
<number>32</number>
</property>
<property name="topMargin">
<number>16</number>
</property>
<property name="rightMargin">
<number>32</number>
</property>
<property name="bottomMargin">
<number>64</number>
</property>
<item>
<widget class="QLabel" name="label_helptext">
<property name="text">
<string>Enter search terms above to find music</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<spacer name="spacer_helptext">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>QSearchField</class>
<extends>QWidget</extends>
<header>widgets/qsearchfield.h</header>
</customwidget>
<customwidget>
<class>AutoExpandingTreeView</class>
<extends>QTreeView</extends>
<header>widgets/autoexpandingtreeview.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,36 @@
/*
* Strawberry Music Player
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include <QObject>
#include <QString>
#include "streamingservice.h"
#include "core/song.h"
#include "settings/settingsdialog.h"
class Application;
StreamingService::StreamingService(const Song::Source source, const QString &name, const QString &url_scheme, const QString &settings_group, const SettingsDialog::Page settings_page, Application *app, QObject *parent)
: QObject(parent),
app_(app),
source_(source),
name_(name),
url_scheme_(url_scheme),
settings_group_(settings_group),
settings_page_(settings_page) {}

View File

@@ -0,0 +1,152 @@
/*
* Strawberry Music Player
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef STREAMINGSERVICE_H
#define STREAMINGSERVICE_H
#include <QtGlobal>
#include <QObject>
#include <QMetaType>
#include <QMap>
#include <QString>
#include <QUrl>
#include <QIcon>
#include "core/shared_ptr.h"
#include "core/song.h"
#include "settings/settingsdialog.h"
#include "streamingsearchview.h"
class Application;
class CollectionBackend;
class CollectionModel;
class CollectionFilter;
class StreamingService : public QObject {
Q_OBJECT
public:
explicit StreamingService(const Song::Source source, const QString &name, const QString &url_scheme, const QString &settings_group, const SettingsDialog::Page settings_page, Application *app, QObject *parent = nullptr);
~StreamingService() override {}
virtual void Exit() {}
virtual Song::Source source() const { return source_; }
virtual QString name() const { return name_; }
virtual QString url_scheme() const { return url_scheme_; }
virtual QString settings_group() const { return settings_group_; }
virtual SettingsDialog::Page settings_page() const { return settings_page_; }
virtual bool has_initial_load_settings() const { return false; }
virtual void InitialLoadSettings() {}
virtual void ReloadSettings() {}
virtual QIcon Icon() const { return Song::IconForSource(source_); }
virtual bool oauth() const { return false; }
virtual bool authenticated() const { return false; }
virtual int Search(const QString &query, StreamingSearchView::SearchType type) { Q_UNUSED(query); Q_UNUSED(type); return 0; }
virtual void CancelSearch() {}
virtual SharedPtr<CollectionBackend> artists_collection_backend() { return nullptr; }
virtual SharedPtr<CollectionBackend> albums_collection_backend() { return nullptr; }
virtual SharedPtr<CollectionBackend> songs_collection_backend() { return nullptr; }
virtual CollectionModel *artists_collection_model() { return nullptr; }
virtual CollectionModel *albums_collection_model() { return nullptr; }
virtual CollectionModel *songs_collection_model() { return nullptr; }
virtual CollectionFilter *artists_collection_filter_model() { return nullptr; }
virtual CollectionFilter *albums_collection_filter_model() { return nullptr; }
virtual CollectionFilter *songs_collection_filter_model() { return nullptr; }
public slots:
virtual void ShowConfig() {}
virtual void GetArtists() {}
virtual void GetAlbums() {}
virtual void GetSongs() {}
virtual void ResetArtistsRequest() {}
virtual void ResetAlbumsRequest() {}
virtual void ResetSongsRequest() {}
signals:
void ExitFinished();
void RequestLogin();
void RequestLogout();
void LoginWithCredentials(const QString &api_token, const QString &username, const QString &password);
void LoginSuccess();
void LoginFailure(const QString &failure_reason);
void LoginComplete(const bool success, const QString &error = QString());
void TestSuccess();
void TestFailure(const QString &failure_reason);
void TestComplete(const bool success, const QString &error = QString());
void Error(const QString &error);
void Results(const SongMap &songs, const QString &error);
void UpdateStatus(const QString &text);
void ProgressSetMaximum(const int max);
void UpdateProgress(const int max);
void ArtistsResults(const SongMap &songs, const QString &error);
void ArtistsUpdateStatus(const QString &text);
void ArtistsProgressSetMaximum(const int max);
void ArtistsUpdateProgress(const int max);
void AlbumsResults(const SongMap &songs, const QString &error);
void AlbumsUpdateStatus(const QString &text);
void AlbumsProgressSetMaximum(const int max);
void AlbumsUpdateProgress(const int max);
void SongsResults(const SongMap &songs, const QString &error);
void SongsUpdateStatus(const QString &text);
void SongsProgressSetMaximum(const int max);
void SongsUpdateProgress(const int max);
void SearchResults(const int id, const SongMap &songs, const QString &error);
void SearchUpdateStatus(const int id, const QString &text);
void SearchProgressSetMaximum(const int id, const int max);
void SearchUpdateProgress(const int id, const int max);
void AddArtists(const SongList &songs);
void AddAlbums(const SongList &songs);
void AddSongs(const SongList &songs);
void RemoveArtists(const SongList &songs);
void RemoveAlbums(const SongList &songs);
void RemoveSongsByList(const SongList &songs);
void RemoveSongsByMap(const SongMap &songs);
void StreamURLFailure(const uint id, const QUrl &media_url, const QString &error);
void StreamURLSuccess(const uint id, const QUrl &media_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration);
protected:
Application *app_;
private:
Song::Source source_;
QString name_;
QString url_scheme_;
QString settings_group_;
SettingsDialog::Page settings_page_;
};
using StreamingServicePtr = SharedPtr<StreamingService>;
Q_DECLARE_METATYPE(StreamingService*)
Q_DECLARE_METATYPE(StreamingServicePtr)
#endif // STREAMINGSERVICE_H

View File

@@ -0,0 +1,97 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QObject>
#include <QMap>
#include <QString>
#include "core/logging.h"
#include "streamingservices.h"
#include "streamingservice.h"
StreamingServices::StreamingServices(QObject *parent) : QObject(parent) {}
StreamingServices::~StreamingServices() {
while (!services_.isEmpty()) {
StreamingServicePtr service = services_.first();
RemoveService(service);
}
}
void StreamingServices::AddService(StreamingServicePtr service) {
services_.insert(service->source(), service);
if (service->has_initial_load_settings()) service->InitialLoadSettings();
else service->ReloadSettings();
qLog(Debug) << "Added streaming service" << service->name();
}
void StreamingServices::RemoveService(StreamingServicePtr service) {
if (!services_.contains(service->source())) return;
services_.remove(service->source());
QObject::disconnect(&*service, nullptr, this, nullptr);
qLog(Debug) << "Removed streaming service" << service->name();
}
StreamingServicePtr StreamingServices::ServiceBySource(const Song::Source source) const {
if (services_.contains(source)) return services_.value(source);
return nullptr;
}
void StreamingServices::ReloadSettings() {
const QList<StreamingServicePtr> services = services_.values();
for (StreamingServicePtr service : services) {
service->ReloadSettings();
}
}
void StreamingServices::Exit() {
const QList<StreamingServicePtr> services = services_.values();
for (StreamingServicePtr service : services) {
wait_for_exit_ << &*service;
QObject::connect(&*service, &StreamingService::ExitFinished, this, &StreamingServices::ExitReceived);
service->Exit();
}
if (wait_for_exit_.isEmpty()) emit ExitFinished();
}
void StreamingServices::ExitReceived() {
StreamingService *service = qobject_cast<StreamingService*>(sender());
wait_for_exit_.removeAll(service);
if (wait_for_exit_.isEmpty()) emit ExitFinished();
}

View File

@@ -0,0 +1,69 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef STREAMINGSERVICES_H
#define STREAMINGSERVICES_H
#include "config.h"
#include <memory>
#include <QObject>
#include <QList>
#include <QMap>
#include <QString>
#include <core/shared_ptr.h>
#include "core/song.h"
class StreamingService;
class StreamingServices : public QObject {
Q_OBJECT
public:
explicit StreamingServices(QObject *parent = nullptr);
~StreamingServices() override;
SharedPtr<StreamingService> ServiceBySource(const Song::Source source) const;
template <typename T>
SharedPtr<T> Service() {
return std::static_pointer_cast<T>(ServiceBySource(T::kSource));
}
void AddService(SharedPtr<StreamingService> service);
void RemoveService(SharedPtr<StreamingService> service);
void ReloadSettings();
void Exit();
signals:
void ExitFinished();
private slots:
void ExitReceived();
private:
QMap<Song::Source, SharedPtr<StreamingService>> services_;
QList<StreamingService*> wait_for_exit_;
};
#endif // STREAMINGSERVICES_H

View File

@@ -0,0 +1,139 @@
/*
* Strawberry Music Player
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QtGlobal>
#include <QWidget>
#include <QString>
#include <QStackedWidget>
#include <QContextMenuEvent>
#include <QSortFilterProxyModel>
#include <QLabel>
#include <QProgressBar>
#include <QPushButton>
#include <QAction>
#include "core/application.h"
#include "core/iconloader.h"
#include "collection/collectionbackend.h"
#include "collection/collectionmodel.h"
#include "collection/collectionfilter.h"
#include "streamingservice.h"
#include "streamingsongsview.h"
#include "streamingcollectionview.h"
#include "ui_streamingcollectionviewcontainer.h"
StreamingSongsView::StreamingSongsView(Application *app, StreamingServicePtr service, const QString &settings_group, const SettingsDialog::Page settings_page, QWidget *parent)
: QWidget(parent),
app_(app),
service_(service),
settings_group_(settings_group),
settings_page_(settings_page),
ui_(new Ui_StreamingCollectionViewContainer) {
ui_->setupUi(this);
ui_->stacked->setCurrentWidget(ui_->streamingcollection_page);
ui_->view->Init(app_, service_->songs_collection_backend(), service_->songs_collection_model(), false);
ui_->view->setModel(service_->songs_collection_filter_model());
ui_->view->SetFilter(ui_->filter_widget);
ui_->filter_widget->SetSettingsGroup(settings_group);
ui_->filter_widget->Init(service_->songs_collection_model(), service_->songs_collection_filter_model());
QAction *action_configure = new QAction(IconLoader::Load(QStringLiteral("configure")), tr("Configure %1...").arg(Song::DescriptionForSource(service_->source())), this);
QObject::connect(action_configure, &QAction::triggered, this, &StreamingSongsView::OpenSettingsDialog);
ui_->filter_widget->AddMenuAction(action_configure);
QObject::connect(ui_->view, &StreamingCollectionView::GetSongs, this, &StreamingSongsView::GetSongs);
QObject::connect(ui_->view, &StreamingCollectionView::RemoveSongs, &*service_, &StreamingService::RemoveSongsByList);
QObject::connect(ui_->refresh, &QPushButton::clicked, this, &StreamingSongsView::GetSongs);
QObject::connect(ui_->close, &QPushButton::clicked, this, &StreamingSongsView::AbortGetSongs);
QObject::connect(ui_->abort, &QPushButton::clicked, this, &StreamingSongsView::AbortGetSongs);
QObject::connect(&*service_, &StreamingService::SongsResults, this, &StreamingSongsView::SongsFinished);
QObject::connect(&*service_, &StreamingService::SongsUpdateStatus, ui_->status, &QLabel::setText);
QObject::connect(&*service_, &StreamingService::SongsProgressSetMaximum, ui_->progressbar, &QProgressBar::setMaximum);
QObject::connect(&*service_, &StreamingService::SongsUpdateProgress, ui_->progressbar, &QProgressBar::setValue);
QObject::connect(service_->songs_collection_model(), &CollectionModel::TotalArtistCountUpdated, ui_->view, &StreamingCollectionView::TotalArtistCountUpdated);
QObject::connect(service_->songs_collection_model(), &CollectionModel::TotalAlbumCountUpdated, ui_->view, &StreamingCollectionView::TotalAlbumCountUpdated);
QObject::connect(service_->songs_collection_model(), &CollectionModel::TotalSongCountUpdated, ui_->view, &StreamingCollectionView::TotalSongCountUpdated);
QObject::connect(service_->songs_collection_model(), &CollectionModel::modelAboutToBeReset, ui_->view, &StreamingCollectionView::SaveFocus);
QObject::connect(service_->songs_collection_model(), &CollectionModel::modelReset, ui_->view, &StreamingCollectionView::RestoreFocus);
ReloadSettings();
}
StreamingSongsView::~StreamingSongsView() { delete ui_; }
void StreamingSongsView::ReloadSettings() {
ui_->filter_widget->ReloadSettings();
ui_->view->ReloadSettings();
}
void StreamingSongsView::OpenSettingsDialog() {
app_->OpenSettingsDialogAtPage(service_->settings_page());
}
void StreamingSongsView::GetSongs() {
if (!service_->authenticated() && service_->oauth()) {
service_->ShowConfig();
return;
}
ui_->status->clear();
ui_->progressbar->show();
ui_->abort->show();
ui_->close->hide();
ui_->stacked->setCurrentWidget(ui_->help_page);
service_->GetSongs();
}
void StreamingSongsView::AbortGetSongs() {
service_->ResetSongsRequest();
ui_->progressbar->setValue(0);
ui_->status->clear();
ui_->stacked->setCurrentWidget(ui_->streamingcollection_page);
}
void StreamingSongsView::SongsFinished(const SongMap &songs, const QString &error) {
if (songs.isEmpty() && !error.isEmpty()) {
ui_->status->setText(error);
ui_->progressbar->setValue(0);
ui_->progressbar->hide();
ui_->abort->hide();
ui_->close->show();
}
else {
ui_->stacked->setCurrentWidget(ui_->streamingcollection_page);
ui_->status->clear();
service_->songs_collection_backend()->UpdateSongsBySongIDAsync(songs);
}
}

View File

@@ -0,0 +1,69 @@
/*
* Strawberry Music Player
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef STREAMINGSONGSVIEW_H
#define STREAMINGSONGSVIEW_H
#include "config.h"
#include <QObject>
#include <QWidget>
#include <QMap>
#include <QString>
#include "core/shared_ptr.h"
#include "core/song.h"
#include "settings/settingsdialog.h"
#include "ui_streamingcollectionviewcontainer.h"
class QContextMenuEvent;
class Application;
class StreamingService;
class StreamingCollectionView;
class StreamingSongsView : public QWidget {
Q_OBJECT
public:
explicit StreamingSongsView(Application *app, SharedPtr<StreamingService> service, const QString &settings_group, const SettingsDialog::Page settings_page, QWidget *parent = nullptr);
~StreamingSongsView() override;
void ReloadSettings();
StreamingCollectionView *view() const { return ui_->view; }
bool SearchFieldHasFocus() const { return ui_->filter_widget->SearchFieldHasFocus(); }
void FocusSearchField() { ui_->filter_widget->FocusSearchField(); }
private slots:
void OpenSettingsDialog();
void GetSongs();
void AbortGetSongs();
void SongsFinished(const SongMap &songs, const QString &error);
private:
Application *app_;
SharedPtr<StreamingService> service_;
QString settings_group_;
SettingsDialog::Page settings_page_;
Ui_StreamingCollectionViewContainer *ui_;
};
#endif // STREAMINGSONGSVIEW_H

View File

@@ -0,0 +1,364 @@
/*
* Strawberry Music Player
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QtGlobal>
#include <QWidget>
#include <QVariant>
#include <QString>
#include <QLabel>
#include <QProgressBar>
#include <QPushButton>
#include <QSortFilterProxyModel>
#include <QTabWidget>
#include <QStackedWidget>
#include <QContextMenuEvent>
#include <QAction>
#include <QSettings>
#include "core/application.h"
#include "core/iconloader.h"
#include "core/settings.h"
#include "collection/collectionbackend.h"
#include "collection/collectionmodel.h"
#include "collection/collectionfilter.h"
#include "collection/collectionfilterwidget.h"
#include "streamingservice.h"
#include "streamingtabsview.h"
#include "streamingcollectionview.h"
#include "streamingcollectionviewcontainer.h"
#include "ui_streamingtabsview.h"
StreamingTabsView::StreamingTabsView(Application *app, StreamingServicePtr service, const QString &settings_group, const SettingsDialog::Page settings_page, QWidget *parent)
: QWidget(parent),
app_(app),
service_(service),
settings_group_(settings_group),
settings_page_(settings_page),
ui_(new Ui_StreamingTabsView) {
ui_->setupUi(this);
ui_->search_view->Init(app, service);
QObject::connect(ui_->search_view, &StreamingSearchView::AddArtistsSignal, &*service_, &StreamingService::AddArtists);
QObject::connect(ui_->search_view, &StreamingSearchView::AddAlbumsSignal, &*service_, &StreamingService::AddAlbums);
QObject::connect(ui_->search_view, &StreamingSearchView::AddSongsSignal, &*service_, &StreamingService::AddSongs);
QAction *action_configure = new QAction(IconLoader::Load(QStringLiteral("configure")), tr("Configure %1...").arg(Song::TextForSource(service_->source())), this);
QObject::connect(action_configure, &QAction::triggered, this, &StreamingTabsView::OpenSettingsDialog);
if (service_->artists_collection_model()) {
ui_->artists_collection->stacked()->setCurrentWidget(ui_->artists_collection->streamingcollection_page());
ui_->artists_collection->view()->Init(app_, service_->artists_collection_backend(), service_->artists_collection_model(), true);
ui_->artists_collection->view()->setModel(service_->artists_collection_filter_model());
ui_->artists_collection->view()->SetFilter(ui_->artists_collection->filter_widget());
ui_->artists_collection->filter_widget()->SetSettingsGroup(settings_group);
ui_->artists_collection->filter_widget()->SetSettingsPrefix(QStringLiteral("artists"));
ui_->artists_collection->filter_widget()->Init(service_->artists_collection_model(), service_->artists_collection_filter_model());
ui_->artists_collection->filter_widget()->AddMenuAction(action_configure);
QObject::connect(ui_->artists_collection->view(), &StreamingCollectionView::GetSongs, this, &StreamingTabsView::GetArtists);
QObject::connect(ui_->artists_collection->view(), &StreamingCollectionView::RemoveSongs, &*service_, &StreamingService::RemoveArtists);
QObject::connect(ui_->artists_collection->button_refresh(), &QPushButton::clicked, this, &StreamingTabsView::GetArtists);
QObject::connect(ui_->artists_collection->button_close(), &QPushButton::clicked, this, &StreamingTabsView::AbortGetArtists);
QObject::connect(ui_->artists_collection->button_abort(), &QPushButton::clicked, this, &StreamingTabsView::AbortGetArtists);
QObject::connect(&*service_, &StreamingService::ArtistsResults, this, &StreamingTabsView::ArtistsFinished);
QObject::connect(&*service_, &StreamingService::ArtistsUpdateStatus, ui_->artists_collection->status(), &QLabel::setText);
QObject::connect(&*service_, &StreamingService::ArtistsProgressSetMaximum, ui_->artists_collection->progressbar(), &QProgressBar::setMaximum);
QObject::connect(&*service_, &StreamingService::ArtistsUpdateProgress, ui_->artists_collection->progressbar(), &QProgressBar::setValue);
QObject::connect(service_->artists_collection_model(), &CollectionModel::TotalArtistCountUpdated, ui_->artists_collection->view(), &StreamingCollectionView::TotalArtistCountUpdated);
QObject::connect(service_->artists_collection_model(), &CollectionModel::TotalAlbumCountUpdated, ui_->artists_collection->view(), &StreamingCollectionView::TotalAlbumCountUpdated);
QObject::connect(service_->artists_collection_model(), &CollectionModel::TotalSongCountUpdated, ui_->artists_collection->view(), &StreamingCollectionView::TotalSongCountUpdated);
QObject::connect(service_->artists_collection_model(), &CollectionModel::modelAboutToBeReset, ui_->artists_collection->view(), &StreamingCollectionView::SaveFocus);
QObject::connect(service_->artists_collection_model(), &CollectionModel::modelReset, ui_->artists_collection->view(), &StreamingCollectionView::RestoreFocus);
}
else {
ui_->tabs->removeTab(ui_->tabs->indexOf(ui_->artists));
}
if (service_->albums_collection_model()) {
ui_->albums_collection->stacked()->setCurrentWidget(ui_->albums_collection->streamingcollection_page());
ui_->albums_collection->view()->Init(app_, service_->albums_collection_backend(), service_->albums_collection_model(), true);
ui_->albums_collection->view()->setModel(service_->albums_collection_filter_model());
ui_->albums_collection->view()->SetFilter(ui_->albums_collection->filter_widget());
ui_->albums_collection->filter_widget()->SetSettingsGroup(settings_group);
ui_->albums_collection->filter_widget()->SetSettingsPrefix(QStringLiteral("albums"));
ui_->albums_collection->filter_widget()->Init(service_->albums_collection_model(), service_->albums_collection_filter_model());
ui_->albums_collection->filter_widget()->AddMenuAction(action_configure);
QObject::connect(ui_->albums_collection->view(), &StreamingCollectionView::GetSongs, this, &StreamingTabsView::GetAlbums);
QObject::connect(ui_->albums_collection->view(), &StreamingCollectionView::RemoveSongs, &*service_, &StreamingService::RemoveAlbums);
QObject::connect(ui_->albums_collection->button_refresh(), &QPushButton::clicked, this, &StreamingTabsView::GetAlbums);
QObject::connect(ui_->albums_collection->button_close(), &QPushButton::clicked, this, &StreamingTabsView::AbortGetAlbums);
QObject::connect(ui_->albums_collection->button_abort(), &QPushButton::clicked, this, &StreamingTabsView::AbortGetAlbums);
QObject::connect(&*service_, &StreamingService::AlbumsResults, this, &StreamingTabsView::AlbumsFinished);
QObject::connect(&*service_, &StreamingService::AlbumsUpdateStatus, ui_->albums_collection->status(), &QLabel::setText);
QObject::connect(&*service_, &StreamingService::AlbumsProgressSetMaximum, ui_->albums_collection->progressbar(), &QProgressBar::setMaximum);
QObject::connect(&*service_, &StreamingService::AlbumsUpdateProgress, ui_->albums_collection->progressbar(), &QProgressBar::setValue);
QObject::connect(service_->albums_collection_model(), &CollectionModel::TotalArtistCountUpdated, ui_->albums_collection->view(), &StreamingCollectionView::TotalArtistCountUpdated);
QObject::connect(service_->albums_collection_model(), &CollectionModel::TotalAlbumCountUpdated, ui_->albums_collection->view(), &StreamingCollectionView::TotalAlbumCountUpdated);
QObject::connect(service_->albums_collection_model(), &CollectionModel::TotalSongCountUpdated, ui_->albums_collection->view(), &StreamingCollectionView::TotalSongCountUpdated);
QObject::connect(service_->albums_collection_model(), &CollectionModel::modelAboutToBeReset, ui_->albums_collection->view(), &StreamingCollectionView::SaveFocus);
QObject::connect(service_->albums_collection_model(), &CollectionModel::modelReset, ui_->albums_collection->view(), &StreamingCollectionView::RestoreFocus);
}
else {
ui_->tabs->removeTab(ui_->tabs->indexOf(ui_->albums));
}
if (service_->songs_collection_model()) {
ui_->songs_collection->stacked()->setCurrentWidget(ui_->songs_collection->streamingcollection_page());
ui_->songs_collection->view()->Init(app_, service_->songs_collection_backend(), service_->songs_collection_model(), true);
ui_->songs_collection->view()->setModel(service_->songs_collection_filter_model());
ui_->songs_collection->view()->SetFilter(ui_->songs_collection->filter_widget());
ui_->songs_collection->filter_widget()->SetSettingsGroup(settings_group);
ui_->songs_collection->filter_widget()->SetSettingsPrefix(QStringLiteral("songs"));
ui_->songs_collection->filter_widget()->Init(service_->songs_collection_model(), service_->songs_collection_filter_model());
ui_->songs_collection->filter_widget()->AddMenuAction(action_configure);
QObject::connect(ui_->songs_collection->view(), &StreamingCollectionView::GetSongs, this, &StreamingTabsView::GetSongs);
QObject::connect(ui_->songs_collection->view(), &StreamingCollectionView::RemoveSongs, &*service_, &StreamingService::RemoveSongsByList);
QObject::connect(ui_->songs_collection->button_refresh(), &QPushButton::clicked, this, &StreamingTabsView::GetSongs);
QObject::connect(ui_->songs_collection->button_close(), &QPushButton::clicked, this, &StreamingTabsView::AbortGetSongs);
QObject::connect(ui_->songs_collection->button_abort(), &QPushButton::clicked, this, &StreamingTabsView::AbortGetSongs);
QObject::connect(&*service_, &StreamingService::SongsResults, this, &StreamingTabsView::SongsFinished);
QObject::connect(&*service_, &StreamingService::SongsUpdateStatus, ui_->songs_collection->status(), &QLabel::setText);
QObject::connect(&*service_, &StreamingService::SongsProgressSetMaximum, ui_->songs_collection->progressbar(), &QProgressBar::setMaximum);
QObject::connect(&*service_, &StreamingService::SongsUpdateProgress, ui_->songs_collection->progressbar(), &QProgressBar::setValue);
QObject::connect(service_->songs_collection_model(), &CollectionModel::TotalArtistCountUpdated, ui_->songs_collection->view(), &StreamingCollectionView::TotalArtistCountUpdated);
QObject::connect(service_->songs_collection_model(), &CollectionModel::TotalAlbumCountUpdated, ui_->songs_collection->view(), &StreamingCollectionView::TotalAlbumCountUpdated);
QObject::connect(service_->songs_collection_model(), &CollectionModel::TotalSongCountUpdated, ui_->songs_collection->view(), &StreamingCollectionView::TotalSongCountUpdated);
QObject::connect(service_->songs_collection_model(), &CollectionModel::modelAboutToBeReset, ui_->songs_collection->view(), &StreamingCollectionView::SaveFocus);
QObject::connect(service_->songs_collection_model(), &CollectionModel::modelReset, ui_->songs_collection->view(), &StreamingCollectionView::RestoreFocus);
}
else {
ui_->tabs->removeTab(ui_->tabs->indexOf(ui_->songs));
}
Settings s;
s.beginGroup(settings_group_);
QString tab = s.value("tab", QStringLiteral("artists")).toString().toLower();
s.endGroup();
if (tab == QLatin1String("artists")) {
ui_->tabs->setCurrentWidget(ui_->artists);
}
else if (tab == QLatin1String("albums")) {
ui_->tabs->setCurrentWidget(ui_->albums);
}
else if (tab == QLatin1String("songs")) {
ui_->tabs->setCurrentWidget(ui_->songs);
}
else if (tab == QLatin1String("search")) {
ui_->tabs->setCurrentWidget(ui_->search);
}
ReloadSettings();
}
StreamingTabsView::~StreamingTabsView() {
Settings s;
s.beginGroup(settings_group_);
s.setValue("tab", ui_->tabs->currentWidget()->objectName().toLower());
s.endGroup();
delete ui_;
}
void StreamingTabsView::ReloadSettings() {
if (service_->artists_collection_model()) {
ui_->artists_collection->view()->ReloadSettings();
}
if (service_->albums_collection_model()) {
ui_->albums_collection->view()->ReloadSettings();
}
if (service_->songs_collection_model()) {
ui_->songs_collection->view()->ReloadSettings();
}
ui_->search_view->ReloadSettings();
}
bool StreamingTabsView::SearchFieldHasFocus() const {
return ((ui_->tabs->currentWidget() == ui_->artists && ui_->artists_collection->SearchFieldHasFocus()) ||
(ui_->tabs->currentWidget() == ui_->albums && ui_->albums_collection->SearchFieldHasFocus()) ||
(ui_->tabs->currentWidget() == ui_->songs && ui_->songs_collection->SearchFieldHasFocus()) ||
(ui_->tabs->currentWidget() == ui_->search && ui_->search_view->SearchFieldHasFocus()));
}
void StreamingTabsView::FocusSearchField() {
if (ui_->tabs->currentWidget() == ui_->artists) {
ui_->artists_collection->FocusSearchField();
}
else if (ui_->tabs->currentWidget() == ui_->albums) {
ui_->albums_collection->FocusSearchField();
}
else if (ui_->tabs->currentWidget() == ui_->songs) {
ui_->songs_collection->FocusSearchField();
}
else if (ui_->tabs->currentWidget() == ui_->search) {
ui_->search_view->FocusSearchField();
}
}
void StreamingTabsView::GetArtists() {
if (!service_->authenticated() && service_->oauth()) {
service_->ShowConfig();
return;
}
ui_->artists_collection->status()->clear();
ui_->artists_collection->progressbar()->show();
ui_->artists_collection->button_abort()->show();
ui_->artists_collection->button_close()->hide();
ui_->artists_collection->stacked()->setCurrentWidget(ui_->artists_collection->help_page());
service_->GetArtists();
}
void StreamingTabsView::AbortGetArtists() {
service_->ResetArtistsRequest();
ui_->artists_collection->progressbar()->setValue(0);
ui_->artists_collection->status()->clear();
ui_->artists_collection->stacked()->setCurrentWidget(ui_->artists_collection->streamingcollection_page());
}
void StreamingTabsView::ArtistsFinished(const SongMap &songs, const QString &error) {
if (songs.isEmpty() && !error.isEmpty()) {
ui_->artists_collection->status()->setText(error);
ui_->artists_collection->progressbar()->setValue(0);
ui_->artists_collection->progressbar()->hide();
ui_->artists_collection->button_abort()->hide();
ui_->artists_collection->button_close()->show();
}
else {
ui_->artists_collection->stacked()->setCurrentWidget(ui_->artists_collection->streamingcollection_page());
ui_->artists_collection->status()->clear();
service_->artists_collection_backend()->UpdateSongsBySongIDAsync(songs);
}
}
void StreamingTabsView::GetAlbums() {
if (!service_->authenticated() && service_->oauth()) {
service_->ShowConfig();
return;
}
ui_->albums_collection->status()->clear();
ui_->albums_collection->progressbar()->show();
ui_->albums_collection->button_abort()->show();
ui_->albums_collection->button_close()->hide();
ui_->albums_collection->stacked()->setCurrentWidget(ui_->albums_collection->help_page());
service_->GetAlbums();
}
void StreamingTabsView::AbortGetAlbums() {
service_->ResetAlbumsRequest();
ui_->albums_collection->progressbar()->setValue(0);
ui_->albums_collection->status()->clear();
ui_->albums_collection->stacked()->setCurrentWidget(ui_->albums_collection->streamingcollection_page());
}
void StreamingTabsView::AlbumsFinished(const SongMap &songs, const QString &error) {
if (songs.isEmpty() && !error.isEmpty()) {
ui_->albums_collection->status()->setText(error);
ui_->albums_collection->progressbar()->setValue(0);
ui_->albums_collection->progressbar()->hide();
ui_->albums_collection->button_abort()->hide();
ui_->albums_collection->button_close()->show();
}
else {
ui_->albums_collection->stacked()->setCurrentWidget(ui_->albums_collection->streamingcollection_page());
ui_->albums_collection->status()->clear();
service_->albums_collection_backend()->UpdateSongsBySongIDAsync(songs);
}
}
void StreamingTabsView::GetSongs() {
if (!service_->authenticated() && service_->oauth()) {
service_->ShowConfig();
return;
}
ui_->songs_collection->status()->clear();
ui_->songs_collection->progressbar()->show();
ui_->songs_collection->button_abort()->show();
ui_->songs_collection->button_close()->hide();
ui_->songs_collection->stacked()->setCurrentWidget(ui_->songs_collection->help_page());
service_->GetSongs();
}
void StreamingTabsView::AbortGetSongs() {
service_->ResetSongsRequest();
ui_->songs_collection->progressbar()->setValue(0);
ui_->songs_collection->status()->clear();
ui_->songs_collection->stacked()->setCurrentWidget(ui_->songs_collection->streamingcollection_page());
}
void StreamingTabsView::SongsFinished(const SongMap &songs, const QString &error) {
if (songs.isEmpty() && !error.isEmpty()) {
ui_->songs_collection->status()->setText(error);
ui_->songs_collection->progressbar()->setValue(0);
ui_->songs_collection->progressbar()->hide();
ui_->songs_collection->button_abort()->hide();
ui_->songs_collection->button_close()->show();
}
else {
ui_->songs_collection->stacked()->setCurrentWidget(ui_->songs_collection->streamingcollection_page());
ui_->songs_collection->status()->clear();
service_->songs_collection_backend()->UpdateSongsBySongIDAsync(songs);
}
}
void StreamingTabsView::OpenSettingsDialog() {
app_->OpenSettingsDialogAtPage(service_->settings_page());
}

View File

@@ -0,0 +1,80 @@
/*
* Strawberry Music Player
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef STREAMINGTABSVIEW_H
#define STREAMINGTABSVIEW_H
#include "config.h"
#include <QObject>
#include <QWidget>
#include <QMap>
#include <QString>
#include "core/shared_ptr.h"
#include "settings/settingsdialog.h"
#include "streamingcollectionviewcontainer.h"
#include "ui_streamingtabsview.h"
#include "core/song.h"
class QContextMenuEvent;
class Application;
class StreamingService;
class StreamingCollectionView;
class StreamingSearchView;
class StreamingTabsView : public QWidget {
Q_OBJECT
public:
explicit StreamingTabsView(Application *app, SharedPtr<StreamingService> service, const QString &settings_group, const SettingsDialog::Page settings_page, QWidget *parent = nullptr);
~StreamingTabsView() override;
void ReloadSettings();
StreamingCollectionView *artists_collection_view() const { return ui_->artists_collection->view(); }
StreamingCollectionView *albums_collection_view() const { return ui_->albums_collection->view(); }
StreamingCollectionView *songs_collection_view() const { return ui_->songs_collection->view(); }
StreamingSearchView *search_view() const { return ui_->search_view; }
bool SearchFieldHasFocus() const;
void FocusSearchField();
private slots:
void OpenSettingsDialog();
void GetArtists();
void GetAlbums();
void GetSongs();
void AbortGetArtists();
void AbortGetAlbums();
void AbortGetSongs();
void ArtistsFinished(const SongMap &songs, const QString &error);
void AlbumsFinished(const SongMap &songs, const QString &error);
void SongsFinished(const SongMap &songs, const QString &error);
private:
Application *app_;
SharedPtr <StreamingService> service_;
QString settings_group_;
SettingsDialog::Page settings_page_;
Ui_StreamingTabsView *ui_;
};
#endif // STREAMINGTABSVIEW_H

View File

@@ -0,0 +1,136 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>StreamingTabsView</class>
<widget class="QWidget" name="StreamingTabsView">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>660</height>
</rect>
</property>
<property name="windowTitle">
<string>Streaming Tabs View</string>
</property>
<layout class="QVBoxLayout" name="layout_streamingtabsview">
<item>
<widget class="QTabWidget" name="tabs">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="artists">
<attribute name="title">
<string>Artists</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_7">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="StreamingCollectionViewContainer" name="artists_collection" native="true"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="albums">
<attribute name="title">
<string>Albums</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_8">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="StreamingCollectionViewContainer" name="albums_collection" native="true"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="songs">
<attribute name="title">
<string>Songs</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_9">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="StreamingCollectionViewContainer" name="songs_collection" native="true"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="search">
<attribute name="title">
<string>Search</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="StreamingSearchView" name="search_view" native="true"/>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>StreamingSearchView</class>
<extends>QWidget</extends>
<header>streaming/streamingsearchview.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>StreamingCollectionViewContainer</class>
<extends>QWidget</extends>
<header>streaming/streamingcollectionviewcontainer.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,84 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QApplication>
#include <QVariant>
#include "streamplaylistitem.h"
#include "streamingservice.h"
#include "core/sqlrow.h"
StreamPlaylistItem::StreamPlaylistItem(const Song::Source source)
: PlaylistItem(source),
source_(source) {}
StreamPlaylistItem::StreamPlaylistItem(const Song &metadata)
: PlaylistItem(metadata.source()),
source_(metadata.source()),
metadata_(metadata) {
InitMetadata();
}
StreamPlaylistItem::StreamPlaylistItem(StreamingServicePtr service, const Song &metadata)
: PlaylistItem(metadata.source()),
source_(service->source()),
metadata_(metadata) {
InitMetadata();
}
bool StreamPlaylistItem::InitFromQuery(const SqlRow &query) {
metadata_.InitFromQuery(query, false, static_cast<int>(Song::kRowIdColumns.count()));
InitMetadata();
return true;
}
QVariant StreamPlaylistItem::DatabaseValue(DatabaseColumn column) const {
return PlaylistItem::DatabaseValue(column);
}
void StreamPlaylistItem::InitMetadata() {
if (metadata_.title().isEmpty()) metadata_.set_title(metadata_.url().toString());
if (metadata_.source() == Song::Source::Unknown) metadata_.set_source(Song::Source::Stream);
if (metadata_.filetype() == Song::FileType::Unknown) metadata_.set_filetype(Song::FileType::Stream);
metadata_.set_valid(true);
}
Song StreamPlaylistItem::Metadata() const {
if (HasTemporaryMetadata()) return temp_metadata_;
return metadata_;
}
QUrl StreamPlaylistItem::Url() const { return metadata_.url(); }
void StreamPlaylistItem::SetArtManual(const QUrl &cover_url) {
metadata_.set_art_manual(cover_url);
temp_metadata_.set_art_manual(cover_url);
}

View File

@@ -0,0 +1,66 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef STREAMPLAYLISTITEM_H
#define STREAMPLAYLISTITEM_H
#include "config.h"
#include <QVariant>
#include <QUrl>
#include "core/shared_ptr.h"
#include "core/song.h"
#include "core/sqlrow.h"
#include "playlist/playlistitem.h"
class StreamingService;
class StreamPlaylistItem : public PlaylistItem {
public:
explicit StreamPlaylistItem(const Song::Source source);
explicit StreamPlaylistItem(const Song &metadata);
explicit StreamPlaylistItem(SharedPtr<StreamingService> service, const Song &metadata);
bool InitFromQuery(const SqlRow &query) override;
Song Metadata() const override;
Song OriginalMetadata() const override { return metadata_; }
QUrl Url() const override;
void SetMetadata(const Song &metadata) override { metadata_ = metadata; }
void SetArtManual(const QUrl &cover_url) override;
protected:
QVariant DatabaseValue(DatabaseColumn) const override;
Song DatabaseSongMetadata() const override { return metadata_; }
private:
void InitMetadata();
private:
Song::Source source_;
Song metadata_;
Q_DISABLE_COPY(StreamPlaylistItem)
};
#endif // STREAMPLAYLISTITEM_H

View File

@@ -0,0 +1,40 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2011, David Sansome <me@davidsansome.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef STREAMSONGMIMEDATA_H
#define STREAMSONGMIMEDATA_H
#include "core/shared_ptr.h"
#include "core/mimedata.h"
#include "core/song.h"
class StreamingService;
class StreamSongMimeData : public MimeData {
Q_OBJECT
public:
explicit StreamSongMimeData(SharedPtr<StreamingService> _service, QObject* = nullptr) : service(_service) {}
SharedPtr<StreamingService> service;
SongList songs;
};
#endif // STREAMSONGMIMEDATA_H