diff --git a/data/qml/CoverImage.qml b/data/qml/CoverImage.qml new file mode 100644 index 000000000..442d835cb --- /dev/null +++ b/data/qml/CoverImage.qml @@ -0,0 +1,122 @@ +import QtQuick 1.1 + +Item { + id: root + + // Labels & Cover + property string artistName + property string trackName + property string artworkId + + // The border color for the cover image + property color borderColor: "black" + // The border width for the cover image + property int borderWidth: 2 + + // needed to adjust the shadow + property color backgroundColor: "black" + + // sets the brightness for the item and its mirror (1: brightest, 0: darkest) + property double itemBrightness: 1 + property double mirrorBrightness: .5 + + + MouseArea { + anchors.fill: parent + onClicked: print("cover clicked") + } + + + Component { + id: coverImage + + Rectangle { + color: "white" + border.color: borderColor + border.width: borderWidth + + Image { + anchors.fill: parent + //anchors.margins: borderWidth + source: "image://albumart/" + artworkId + } + + + Rectangle { + id: textBackground + anchors { left: parent.left; right: parent.right; bottom: parent.bottom } + height: 32 + anchors.margins: 5 + color: "black" + opacity: 0.5 + radius: 3 + + } + Text { + color: "white" + font.bold: true + text: trackName + anchors { left: textBackground.left; right: textBackground.right; top: textBackground.top } + anchors.margins: 2 + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + } + Text { + color: "white" + text: artistName + anchors { left: textBackground.left; right: textBackground.right; bottom: textBackground.bottom } + anchors.margins: 2 + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + } + } + + } + Loader { + sourceComponent: coverImage + anchors.fill: parent + } + + Loader { + id: mirroredCover + sourceComponent: coverImage + opacity: parent.mirrorOpacity + anchors.fill: parent + transform : [ + Rotation { + angle: 180; origin.y: root.height + axis.x: 1; axis.y: 0; axis.z: 0 + } + ] + } + + Rectangle { + id: itemShadow + color: backgroundColor + anchors.fill: parent + anchors.bottomMargin: - parent.height + + // scaling might be off a pixel... make sure that the shadow is at least as large as the image + anchors.leftMargin: -2 + anchors.rightMargin: -2 + anchors.topMargin: -2 + + opacity: 1 - itemBrightness + } + + Rectangle { + id: mirrorShadow + color: parent.backgroundColor + height: parent.height + 2 + width: parent.width + 4 + anchors.centerIn: parent + anchors.verticalCenterOffset: parent.height + + gradient: Gradient { + // TODO: no clue how to get the RGB component of the container rectangle color + // For now the Qt.rgba needs to be manually updated to match the backgroundColor + GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, 1-mirrorBrightness) } + GradientStop { position: 0.5; color: backgroundColor } + } + } +} diff --git a/data/qml/StationScene.qml b/data/qml/StationScene.qml index 58a01eac0..01a6d08c4 100644 --- a/data/qml/StationScene.qml +++ b/data/qml/StationScene.qml @@ -5,171 +5,118 @@ Rectangle { id: scene color: "black" anchors.fill: parent + state: echonestStation.configured ? "list" : "configure" property int coverSize: 230 - Component { - id: coverImage + states: [ + State { + name: "configure" //; when: scene.state === "configure" + PropertyChanges { target: coverView; anchors.leftMargin: scene.width + scene.coverSize + 20 } + PropertyChanges { target: styleCloud; anchors.leftMargin: 0 } + } + ] - Rectangle { - height: scene.coverSize - width: scene.coverSize - color: scene.color - border.color: scene.color - border.width: 2 - - property string artistName - property string trackName - property string artworkId - - Image { - anchors.fill: parent - anchors.margins: parent.border.width - source: "image://albumart/" + parent.artworkId + transitions: [ + Transition { + NumberAnimation { + target: coverView + properties: "anchors.leftMargin"; easing.type: Easing.InOutQuad; duration: 500 } - - Rectangle { - id: textBackground - anchors { left: parent.left; right: parent.right; bottom: parent.bottom } - height: 32 - anchors.margins: 5 - color: "black" - opacity: 0.5 - radius: 3 - - } - Text { - color: "white" - font.bold: true - text: trackName - anchors { left: textBackground.left; right: textBackground.right; top: textBackground.top } - anchors.margins: 2 - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideRight - } - Text { - color: "white" - text: artistName - anchors { left: textBackground.left; right: textBackground.right; bottom: textBackground.bottom } - anchors.margins: 2 - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideRight - } - - MouseArea { - anchors.fill: parent - onClicked: print("cover clicked") + NumberAnimation { + target: styleCloud + properties: "anchors.leftMargin"; easing.type: Easing.InOutQuad; duration: 500 } } - } + ] - Component { - id: mirroredDelegate - - Item { - id: mirroredItem - height: scene.coverSize - width: scene.coverSize - - z: x - scale: PathView.itemScale - - property double itemOpacity: PathView.itemOpacity - property double shadowOp: PathView.shadowOpacity - - Connections { - target: dynamicModel.itemFromIndex( index ) - onCoverChanged: { - // We need to unset and re-set it because QML wouldn't re-query the image if it still has the same url - normalCover.item.artworkId = "" - mirroredCover.item.artworkId = "" - normalCover.item.artworkId = index - mirroredCover.item.artworkId = index - } - } - //Component.onCompleted: print("created delegate for", dynamicModel.itemFromIndex( index ) ) - - Loader { - id: normalCover - sourceComponent: coverImage - opacity: parent.itemOpacity - onLoaded: { - item.trackName = trackName - item.artistName = artistName - item.artworkId = index - } - } - Loader { - id: mirroredCover - sourceComponent: coverImage - opacity: parent.itemOpacity - onLoaded: { - item.trackName = trackName - item.artistName = artistName - item.artworkId = index - } - transform : [ - Rotation { - angle: 180; origin.y: scene.coverSize - axis.x: 1; axis.y: 0; axis.z: 0 - } - ] - } - - Rectangle { - color: scene.color - anchors.fill: parent - opacity: mirroredItem.shadowOp - } - Rectangle { - color: scene.color - height: scene.coverSize - width: scene.coverSize - anchors.centerIn: parent - anchors.verticalCenterOffset: scene.coverSize - gradient: Gradient { - // TODO: no clue how to get the RGB component of the container rectangle color - // For now the Qt.rgba needs to be manually updated to match scene.color - GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, mirroredItem.shadowOp + ( (1 - mirroredItem.shadowOp) * .4)) } - GradientStop { position: 0.5; color: scene.color } - } - } - } - } PathView { - id: view + id: coverView anchors.fill: parent - highlight: appHighlight - preferredHighlightBegin: 0.07 - preferredHighlightEnd: 0.07 + + preferredHighlightBegin: 0.07 // scene.width / 11000 + preferredHighlightEnd: preferredHighlightBegin pathItemCount: 4 highlightMoveDuration: 500 model: dynamicModel currentIndex: currentlyPlayedIndex - delegate: mirroredDelegate + delegate: CoverImage { + height: scene.coverSize + width: scene.coverSize + + scale: PathView.itemScale + + artistName: model.artistName + trackName: model.trackName + artworkId: index + + itemBrightness: PathView.itemBrightness + z: x + } path: Path { - startX: scene.width / 2 + 20 + startX: coverView.width / 2 + 20 startY: 155 + PathAttribute { name: "itemOpacity"; value: 0 } - PathAttribute { name: "shadowOpacity"; value: 0 } + PathAttribute { name: "itemBrightness"; value: 0 } PathAttribute { name: "itemScale"; value: 1.5 } - PathLine { x: scene.width / 2; y: 150 } + PathLine { x: coverView.width / 2; y: 150 } PathAttribute { name: "itemOpacity"; value: 1 } - PathAttribute { name: "shadowOpacity"; value: 0 } + PathAttribute { name: "itemBrightness"; value: 1 } PathAttribute { name: "itemScale"; value: 1.0 } - PathLine { x: scene.width / 2 - 100; y: 180;} + PathLine { x: coverView.width / 2 - 100; y: 180;} PathAttribute { name: "itemOpacity"; value: 1 } - PathAttribute { name: "shadowOpacity"; value: 0 } + PathAttribute { name: "itemBrightness"; value: 1 } PathAttribute { name: "itemScale"; value: 0.6 } PathLine { x: 100; y: 100;} PathAttribute { name: "itemOpacity"; value: 1 } - PathAttribute { name: "shadowOpacity"; value: 1 } + PathAttribute { name: "itemBrighness"; value: 0.4 } PathAttribute { name: "itemScale"; value: 0.4 } } } + + Item { + id: styleCloud + anchors { left: parent.left; top: parent.top; bottom: parent.bottom } + anchors.leftMargin: scene.width + width: scene.width + + function randomNumber(min, max) { + var date = new Date(); + return Math.floor((max - min) * Math.random(date.getSeconds())) + min + } + + Flow { + anchors.centerIn: parent + width: parent.width - 100 + //model: controlModel + spacing: 3 + Repeater { + model: generator.styles() + + delegate: Item { + width: delegateText.width + height: 28 + Text { + id: delegateText + color: "white" + //text: controlModel.controlAt( index ).summary + text: modelData + font.pixelSize: styleCloud.randomNumber(11, 28) + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: styleCloud.randomNumber(0, 15) + MouseArea { + anchors.fill: parent + onClicked: echonestStation.setMainControl(modelData); + } + } + } + } + } + } } diff --git a/resources.qrc b/resources.qrc index 4c05c85bf..f99f12517 100644 --- a/resources.qrc +++ b/resources.qrc @@ -148,5 +148,6 @@ data/images/scrollbar-horizontal-handle.png data/qml/ArtistInfoScene.qml data/qml/StationScene.qml + data/qml/CoverImage.qml diff --git a/src/libtomahawk/playlist/dynamic/GeneratorInterface.cpp b/src/libtomahawk/playlist/dynamic/GeneratorInterface.cpp index 4b6bd1aca..885bd9d29 100644 --- a/src/libtomahawk/playlist/dynamic/GeneratorInterface.cpp +++ b/src/libtomahawk/playlist/dynamic/GeneratorInterface.cpp @@ -56,6 +56,7 @@ void Tomahawk::GeneratorInterface::addControl( const Tomahawk::dyncontrol_ptr& control ) { m_controls << control; + emit controlAdded( control ); } @@ -77,6 +78,7 @@ void Tomahawk::GeneratorInterface::removeControl( const Tomahawk::dyncontrol_ptr& control ) { m_controls.removeAll( control ); + controlRemoved( control ); } diff --git a/src/libtomahawk/playlist/dynamic/GeneratorInterface.h b/src/libtomahawk/playlist/dynamic/GeneratorInterface.h index fd0746a5b..f4c4d5f3d 100644 --- a/src/libtomahawk/playlist/dynamic/GeneratorInterface.h +++ b/src/libtomahawk/playlist/dynamic/GeneratorInterface.h @@ -59,7 +59,7 @@ public: // empty QString means use default /// The generator will keep track of all the controls it creates. No need to tell it about controls /// you ask it to create - virtual dyncontrol_ptr createControl( const QString& type = QString() ); + Q_INVOKABLE virtual dyncontrol_ptr createControl( const QString& type = QString() ); /// A logo to display for this generator, if it has one virtual QPixmap logo(); @@ -127,6 +127,8 @@ signals: void error( const QString& title, const QString& body); void generated( const QList< Tomahawk::query_ptr>& queries ); void nextTrackGenerated( const Tomahawk::query_ptr& track ); + void controlAdded(const dyncontrol_ptr& control); + void controlRemoved(const dyncontrol_ptr& control); protected: QString m_type; diff --git a/src/libtomahawk/playlist/dynamic/echonest/EchonestGenerator.cpp b/src/libtomahawk/playlist/dynamic/echonest/EchonestGenerator.cpp index 1f3fd0cff..3059f9815 100644 --- a/src/libtomahawk/playlist/dynamic/echonest/EchonestGenerator.cpp +++ b/src/libtomahawk/playlist/dynamic/echonest/EchonestGenerator.cpp @@ -150,6 +150,7 @@ dyncontrol_ptr EchonestGenerator::createControl( const QString& type ) { m_controls << dyncontrol_ptr( new EchonestControl( type, GeneratorFactory::typeSelectors( m_type ) ) ); + emit controlAdded( m_controls.last() ); return m_controls.last(); } diff --git a/src/libtomahawk/playlist/dynamic/echonest/EchonestGenerator.h b/src/libtomahawk/playlist/dynamic/echonest/EchonestGenerator.h index 90cb7c338..231415eb7 100644 --- a/src/libtomahawk/playlist/dynamic/echonest/EchonestGenerator.h +++ b/src/libtomahawk/playlist/dynamic/echonest/EchonestGenerator.h @@ -71,7 +71,7 @@ public: explicit EchonestGenerator( QObject* parent = 0 ); virtual ~EchonestGenerator(); - virtual dyncontrol_ptr createControl( const QString& type = QString() ); + Q_INVOKABLE virtual dyncontrol_ptr createControl( const QString& type = QString() ); virtual QPixmap logo(); virtual void generate ( int number = -1 ); virtual void startOnDemand(); @@ -80,7 +80,7 @@ public: virtual bool onDemandSteerable() const { return true; } virtual QWidget* steeringWidget(); - static QStringList styles(); + Q_INVOKABLE static QStringList styles(); static QStringList moods(); static QStringList userCatalogs(); static QByteArray catalogId( const QString& collectionId ); diff --git a/src/libtomahawk/playlist/dynamic/widgets/DynamicQmlWidget.cpp b/src/libtomahawk/playlist/dynamic/widgets/DynamicQmlWidget.cpp index cb543e38f..1cc65b626 100644 --- a/src/libtomahawk/playlist/dynamic/widgets/DynamicQmlWidget.cpp +++ b/src/libtomahawk/playlist/dynamic/widgets/DynamicQmlWidget.cpp @@ -4,6 +4,7 @@ #include "playlist/PlayableProxyModel.h" #include "utils/TomahawkUtilsGui.h" #include "dynamic/DynamicModel.h" +#include "dynamic/echonest/EchonestControl.h" #include "dynamic/GeneratorInterface.h" #include "PlayableItem.h" #include "Source.h" @@ -30,19 +31,40 @@ DynamicQmlWidget::DynamicQmlWidget( const dynplaylist_ptr& playlist, QWidget* pa m_model = new DynamicModel( this ); m_proxyModel = new PlayableProxyModel( this ); m_proxyModel->setSourcePlayableModel( m_model ); - m_proxyModel->setShowOfflineResults( false ); + m_proxyModel->setShowOfflineResults( true ); m_model->loadPlaylist( m_playlist ); - m_model->startOnDemand(); + // Initially seed the playlist m_playlist->generator()->generate( 20 ); - m_playlist->resolve(); - rootContext()->setContextProperty( "dynamicModel", m_proxyModel ); - currentItemChanged( m_model->currentItem() ); + qDebug() << "###got" << m_playlist->generator()->controls().size() << "controls"; + + qmlRegisterUncreatableType("tomahawk", 1, 0, "EchonestStation", "bla"); // TODO: In case QML is used in more places, this should probably be moved to some generic place qmlRegisterType("tomahawk", 1, 0, "PlayableItem"); +// qmlRegisterUncreatableType("tomahawk", 1, 0, "DynamicControl", "use generator.createControl() isntead"); +// qmlRegisterUncreatableType("tomahawk", 1, 0, "EchonestControl", "use Generator.createControl() instead"); + qmlRegisterUncreatableType("tomahawk", 1, 0, "DynamicControl", "use generator.createControl() isntead"); + qmlRegisterUncreatableType("tomahawk", 1, 0, "EchonestControl", "use Generator.createControl() instead"); + qmlRegisterUncreatableType("tomahawk", 1, 0, "Generator", "you cannot create it on your own - should be set in context"); + + QStringList generatorControls; + + foreach(dyncontrol_ptr control, m_playlist->generator()->controls()) { + qDebug() << "**CTRL" << control->summary() << control->input() << control->match() << control->type() << control->selectedType(); + generatorControls << control->summary(); + } + + ControlModel *controls = new ControlModel(m_playlist->generator(), this); + + EchonestStation *station = new EchonestStation(m_playlist->generator(), this); + rootContext()->setContextProperty( "echonestStation", station); + rootContext()->setContextProperty( "controlModel", controls ); + rootContext()->setContextProperty( "dynamicModel", m_proxyModel ); + rootContext()->setContextProperty( "generator", m_playlist->generator().data() ); + currentItemChanged( m_model->currentItem() ); setSource( QUrl( "qrc" RESPATH "qml/StationScene.qml" ) ); @@ -127,19 +149,12 @@ void DynamicQmlWidget::currentItemChanged( const QPersistentModelIndex ¤tI void DynamicQmlWidget::tracksGenerated( const QList< query_ptr >& queries ) { - int limit = -1; // only limit the "preview" of a station -// if ( m_playlist->author()->isLocal() && m_playlist->mode() == Static ) -// { -// m_resolveOnNextLoad = true; -// } -// else if ( m_playlist->mode() == OnDemand ) -// { - limit = 5; -// } - - m_model->tracksGenerated( queries, limit ); + qDebug() << queries.count() << "tracks generated"; + m_model->tracksGenerated( queries, queries.count() ); m_playlist->resolve(); + // Ok... we have some intial stuff, switch to dynamic mode + //m_model->startOnDemand(); } void DynamicQmlWidget::onRevisionLoaded(DynamicPlaylistRevision) diff --git a/src/libtomahawk/playlist/dynamic/widgets/DynamicQmlWidget.h b/src/libtomahawk/playlist/dynamic/widgets/DynamicQmlWidget.h index d050d8ff5..ee7e173ef 100644 --- a/src/libtomahawk/playlist/dynamic/widgets/DynamicQmlWidget.h +++ b/src/libtomahawk/playlist/dynamic/widgets/DynamicQmlWidget.h @@ -53,6 +53,7 @@ public: QPixmap requestPixmap( const QString &id, QSize *size, const QSize &requestedSize ); + private slots: void currentItemChanged( const QPersistentModelIndex ¤tIndex ); void tracksGenerated( const QList< Tomahawk::query_ptr>& queries ); @@ -64,5 +65,79 @@ private: dynplaylist_ptr m_playlist; }; +} + +#include "dynamic/GeneratorInterface.h" + +namespace Tomahawk +{ +class EchonestStation: public QObject +{ + Q_OBJECT + Q_PROPERTY(bool configured READ configured NOTIFY configuredChanged) + Q_PROPERTY(Tomahawk::DynamicControl* mainControl READ mainControl) + +public: + EchonestStation(geninterface_ptr generator, QObject *parent = 0) : QObject(parent), m_generator(generator) {} + + Tomahawk::DynamicControl* mainControl() { + foreach(dyncontrol_ptr control, m_generator->controls()) { + qDebug() << "got control" << control->selectedType(); + if(control->selectedType() == "Artist" || control->selectedType() == "Style") { + return control.data(); + } + } + return 0; + } + + bool configured() { return mainControl() != 0; } + + Q_INVOKABLE void setMainControl(const QString &type) { + dyncontrol_ptr control = m_generator->createControl("echonest"); + control->setSelectedType("Style"); + control->setMatch("1"); + control->setInput(type); + qDebug() << "created control" << control->type() << control->selectedType() << control->match(); + m_generator->generate(20); + + emit configuredChanged(); + } + +signals: + void configuredChanged(); + +private: + geninterface_ptr m_generator; +}; +} + +namespace Tomahawk +{ + +class ControlModel: public QAbstractListModel +{ + Q_OBJECT +public: + ControlModel(geninterface_ptr generator, QObject *parent = 0): QAbstractListModel(parent), m_generator(generator) { + connect(generator.data(), SIGNAL(controlAdded(const dyncontrol_ptr&)), SLOT(controlAdded())); + } + + int rowCount(const QModelIndex &parent) const { return m_generator->controls().size(); } + QVariant data(const QModelIndex &index, int role) const { + return "blabla"; + } + Q_INVOKABLE Tomahawk::DynamicControl *controlAt( int index ) { qDebug() << "returning" << m_generator->controls().at(index).data(); return m_generator->controls().at(index).data(); } + +private slots: + void controlAdded() { + qDebug() << "control added"; + beginInsertRows(QModelIndex(), m_generator->controls().size() - 1, m_generator->controls().size() - 1); + endInsertRows(); + } +private: + geninterface_ptr m_generator; + +}; + } #endif // DYNAMIC_QML_WIDGET_H