diff options
author | Yoann Lopes <yoann.lopes@nokia.com> | 2011-11-01 17:38:43 +0100 |
---|---|---|
committer | Yoann Lopes <yoann.lopes@nokia.com> | 2011-11-01 17:38:43 +0100 |
commit | af96acce2ab301bf0d8f1b3a5cef72115af44607 (patch) | |
tree | 805c4eb8b227fe71354e251d3fc7abfce81ec6f9 | |
parent | d7bd0a8b8d4656e23cb8ce50a5532220b3784d0d (diff) |
Added a search field to playlist view.
-rw-r--r-- | libQtSpotify/qspotifyplaylist.cpp | 60 | ||||
-rw-r--r-- | libQtSpotify/qspotifyplaylist.h | 13 | ||||
-rw-r--r-- | qml/InboxTrackDelegate.qml | 18 | ||||
-rw-r--r-- | qml/TrackDelegate.qml | 8 | ||||
-rw-r--r-- | qml/TracklistPage.qml | 129 | ||||
-rw-r--r-- | qml/Utilities.js | 30 |
6 files changed, 226 insertions, 32 deletions
diff --git a/libQtSpotify/qspotifyplaylist.cpp b/libQtSpotify/qspotifyplaylist.cpp index 17f15b9..af9011e 100644 --- a/libQtSpotify/qspotifyplaylist.cpp +++ b/libQtSpotify/qspotifyplaylist.cpp @@ -195,6 +195,7 @@ QSpotifyPlaylist::QSpotifyPlaylist(Type type, sp_playlist *playlist, bool incrRe connect(this, SIGNAL(dataChanged()), this, SIGNAL(playlistDataChanged())); connect(this, SIGNAL(isLoadedChanged()), this, SIGNAL(thisIsLoadedChanged())); connect(this, SIGNAL(playlistDataChanged()), this , SIGNAL(seenCountChanged())); + connect(this, SIGNAL(playlistDataChanged()), this, SIGNAL(tracksChanged())); metadataUpdated(); } @@ -296,7 +297,7 @@ void QSpotifyPlaylist::addTrack(sp_track *track, int pos) bool QSpotifyPlaylist::event(QEvent *e) { if (e->type() == QEvent::User) { - m_skipUpdateTracks = true; + m_skipUpdateTracks = true; metadataUpdated(); m_skipUpdateTracks = false; e->accept(); @@ -319,14 +320,14 @@ bool QSpotifyPlaylist::event(QEvent *e) QSpotifyTracksAddedEvent *ev = static_cast<QSpotifyTracksAddedEvent *>(e); QVector<sp_track*> tracks = ev->tracks(); int pos = ev->position(); - for (int i = 0; i < tracks.count(); ++i) - addTrack(tracks.at(i), pos++); - emit dataChanged(); - if (m_type == Starred || m_type == Inbox) - emit tracksAdded(tracks); - m_trackList->setShuffle(m_trackList->isShuffle()); - if (QSpotifySession::instance()->playQueue()->isCurrentTrackList(m_trackList)) - QSpotifySession::instance()->playQueue()->tracksUpdated(); + for (int i = 0; i < tracks.count(); ++i) + addTrack(tracks.at(i), pos++); + emit dataChanged(); + if (m_type == Starred || m_type == Inbox) + emit tracksAdded(tracks); + m_trackList->setShuffle(m_trackList->isShuffle()); + if (QSpotifySession::instance()->playQueue()->isCurrentTrackList(m_trackList)) + QSpotifySession::instance()->playQueue()->tracksUpdated(); e->accept(); return true; } else if (e->type() == QEvent::User + 4) { @@ -441,6 +442,22 @@ int QSpotifyPlaylist::trackCount() const return c; } +static bool stringContainsWord(const QString &string, const QString &word) +{ + if (word.isEmpty()) + return true; + + int index = string.indexOf(word, 0, Qt::CaseInsensitive); + + if (index == -1) + return false; + + if (index == 0 || string.at(index - 1) == QLatin1Char(' ')) + return true; + + return false; +} + QList<QObject*> QSpotifyPlaylist::tracksAsQObject() const { QList<QObject*> list; @@ -448,14 +465,23 @@ QList<QObject*> QSpotifyPlaylist::tracksAsQObject() const // Reverse order for StarredList to get the most recents first for (int i = m_trackList->m_tracks.count() - 1; i >= 0 ; --i) { QSpotifyTrack *t = m_trackList->m_tracks[i]; - if (t->error() == QSpotifyTrack::Ok) + if (t->error() == QSpotifyTrack::Ok && (m_trackFilter.isEmpty() + || stringContainsWord(t->name(), m_trackFilter) + || stringContainsWord(t->artists(), m_trackFilter) + || stringContainsWord(t->album(), m_trackFilter) + || stringContainsWord(t->creator(), m_trackFilter))) { list.append((QObject*)(t)); + } } } else { for (int i = 0; i < m_trackList->m_tracks.count(); ++i) { QSpotifyTrack *t = m_trackList->m_tracks[i]; - if (t->error() == QSpotifyTrack::Ok) + if (t->error() == QSpotifyTrack::Ok && (m_trackFilter.isEmpty() + || stringContainsWord(t->name(), m_trackFilter) + || stringContainsWord(t->artists(), m_trackFilter) + || stringContainsWord(t->album(), m_trackFilter))) { list.append((QObject*)(t)); + } } } return list; @@ -569,3 +595,15 @@ void QSpotifyPlaylist::unregisterTrackType(QSpotifyTrack *t) m_offlineTracks.remove(t); m_availableTracks.remove(t); } + +void QSpotifyPlaylist::setTrackFilter(const QString &filter) +{ + if (m_trackFilter == filter) + return; + + m_trackFilter = filter; + emit trackFilterChanged(); + emit tracksChanged(); +} + + diff --git a/libQtSpotify/qspotifyplaylist.h b/libQtSpotify/qspotifyplaylist.h index 59ca4e4..9542550 100644 --- a/libQtSpotify/qspotifyplaylist.h +++ b/libQtSpotify/qspotifyplaylist.h @@ -63,7 +63,7 @@ class QSpotifyPlaylist : public QSpotifyObject Q_PROPERTY(QString name READ name NOTIFY playlistDataChanged) Q_PROPERTY(int trackCount READ trackCount NOTIFY playlistDataChanged) Q_PROPERTY(int totalDuration READ totalDuration NOTIFY playlistDataChanged) - Q_PROPERTY(QList<QObject *> tracks READ tracksAsQObject NOTIFY playlistDataChanged) + Q_PROPERTY(QList<QObject *> tracks READ tracksAsQObject NOTIFY tracksChanged) Q_PROPERTY(bool isLoaded READ isLoaded NOTIFY thisIsLoadedChanged) Q_PROPERTY(Type type READ type NOTIFY playlistDataChanged) Q_PROPERTY(OfflineStatus offlineStatus READ offlineStatus NOTIFY playlistDataChanged) @@ -74,6 +74,7 @@ class QSpotifyPlaylist : public QSpotifyObject Q_PROPERTY(bool availableOffline READ availableOffline WRITE setAvailableOffline NOTIFY availableOfflineChanged) Q_PROPERTY(int unseenCount READ unseenCount NOTIFY seenCountChanged) Q_PROPERTY(bool hasOfflineTracks READ hasOfflineTracks NOTIFY hasOfflineTracksChanged) + Q_PROPERTY(QString trackFilter READ trackFilter WRITE setTrackFilter NOTIFY trackFilterChanged) Q_ENUMS(Type) Q_ENUMS(OfflineStatus) public: @@ -106,10 +107,12 @@ public: bool availableOffline() const { return m_availableOffline; } void setAvailableOffline(bool offline); QString listSection() const; - QList<QSpotifyTrack *> tracks() const { return m_trackList->m_tracks; } + QList<QSpotifyTrack *> tracks() const { return m_trackList->tracks(); } QList<QObject *> tracksAsQObject() const; int unseenCount() const; bool hasOfflineTracks() const { return m_offlineTracks.count() > 0; } + QString trackFilter() const { return m_trackFilter; } + void setTrackFilter(const QString &filter); bool contains(sp_track *t) const { return m_tracksSet.contains(t); } @@ -137,6 +140,8 @@ Q_SIGNALS: void availableOfflineChanged(); void seenCountChanged(); void hasOfflineTracksChanged(); + void trackFilterChanged(); + void tracksChanged(); protected: bool updateData(); @@ -169,9 +174,11 @@ private: QSet<QSpotifyTrack *> m_availableTracks; QString m_uri; - + bool m_skipUpdateTracks; + QString m_trackFilter; + friend class QSpotifyPlaylistContainer; friend class QSpotifyUser; friend class QSpotifyTrack; diff --git a/qml/InboxTrackDelegate.qml b/qml/InboxTrackDelegate.qml index bf3be42..14712e0 100644 --- a/qml/InboxTrackDelegate.qml +++ b/qml/InboxTrackDelegate.qml @@ -5,22 +5,22 @@ ** Contact: Yoann Lopes (yoann.lopes@nokia.com) ** ** This file is part of the MeeSpot project. -** +** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions ** are met: -** +** ** Redistributions of source code must retain the above copyright notice, ** this list of conditions and the following disclaimer. -** +** ** Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. -** +** ** Neither the name of Nokia Corporation and its Subsidiary(-ies) nor the names of its ** contributors may be used to endorse or promote products derived from ** this software without specific prior written permission. -** +** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS @@ -71,7 +71,9 @@ Item { property real backgroundOpacity: 0.0 - height: UI.LIST_ITEM_HEIGHT + thirdText.height + property real defaultHeight: UI.LIST_ITEM_HEIGHT + thirdText.height + + height: defaultHeight width: parent.width SequentialAnimation { @@ -144,6 +146,7 @@ Item { anchors.right: parent.right Label { id: mainText + height: 34 anchors.left: parent.left anchors.right: iconItem.left anchors.rightMargin: UI.MARGIN_XLARGE @@ -152,6 +155,7 @@ Item { font.pixelSize: listItem.titleSize color: highlighted ? listItem.highlightColor : listItem.titleColor elide: Text.ElideRight + clip: true Behavior on color { ColorAnimation { duration: 200 } } } Image { @@ -172,6 +176,7 @@ Item { anchors.right: parent.right Label { id: subText + height: 29 anchors.left: parent.left anchors.right: timing.left anchors.rightMargin: UI.MARGIN_XLARGE @@ -180,6 +185,7 @@ Item { font.weight: Font.Light color: highlighted ? listItem.highlightColor : listItem.subtitleColor elide: Text.ElideRight + clip: true visible: text != "" Behavior on color { ColorAnimation { duration: 200 } } } diff --git a/qml/TrackDelegate.qml b/qml/TrackDelegate.qml index 37cba77..b5b501b 100644 --- a/qml/TrackDelegate.qml +++ b/qml/TrackDelegate.qml @@ -71,7 +71,9 @@ Item { property real backgroundOpacity: 0.0 - height: UI.LIST_ITEM_HEIGHT + property real defaultHeight: UI.LIST_ITEM_HEIGHT + + height: defaultHeight width: parent.width SequentialAnimation { @@ -172,6 +174,7 @@ Item { anchors.right: parent.right Label { id: mainText + height: 34 anchors.left: parent.left anchors.right: iconItem.left anchors.rightMargin: UI.MARGIN_XLARGE @@ -180,6 +183,7 @@ Item { font.pixelSize: listItem.titleSize color: highlighted ? listItem.highlightColor : listItem.titleColor elide: Text.ElideRight + clip: true Behavior on color { ColorAnimation { duration: 200 } } } Image { @@ -200,6 +204,7 @@ Item { anchors.right: parent.right Label { id: subText + height: 29 anchors.left: parent.left anchors.right: timing.left anchors.rightMargin: UI.MARGIN_XLARGE @@ -208,6 +213,7 @@ Item { font.weight: Font.Light color: highlighted ? listItem.highlightColor : listItem.subtitleColor elide: Text.ElideRight + clip: true visible: text != "" Behavior on color { ColorAnimation { duration: 200 } } } diff --git a/qml/TracklistPage.qml b/qml/TracklistPage.qml index f2bd08b..ea1a903 100644 --- a/qml/TracklistPage.qml +++ b/qml/TracklistPage.qml @@ -43,6 +43,7 @@ import QtQuick 1.1 import com.meego 1.0 import QtSpotify 1.0 import "UIConstants.js" as UI +import "Utilities.js" as Util Page { id: tracklistPage @@ -51,6 +52,8 @@ Page { anchors.rightMargin: UI.MARGIN_XLARGE anchors.leftMargin: UI.MARGIN_XLARGE + Component.onCompleted: playlist.trackFilter = "" + TrackMenu { id: menu deleteVisible: playlist && spotifySession.user ? (playlist.type != SpotifyPlaylist.Starred && spotifySession.user.canModifyPlaylist(playlist)) @@ -61,8 +64,10 @@ Page { Component { id: trackDelegate TrackDelegate { - name: modelData.name - artistAndAlbum: modelData.artists + " | " + modelData.album + name: searchField.text.length > 0 ? Util.highlightWord(modelData.name, searchField.text) : modelData.name + artistAndAlbum: (searchField.text.length > 0 ? Util.highlightWord(modelData.artists, searchField.text) : modelData.artists) + + " | " + + (searchField.text.length > 0 ? Util.highlightWord(modelData.album, searchField.text) : modelData.album) duration: modelData.duration highlighted: modelData.isCurrentPlayingTrack starred: modelData.isStarred @@ -78,9 +83,12 @@ Page { Component { id: inboxDelegate InboxTrackDelegate { - name: modelData.name - artistAndAlbum: modelData.artists + " | " + modelData.album - creatorAndDate: modelData.creator + " | " + Qt.formatDateTime(modelData.creationDate) + name: searchField.text.length > 0 ? Util.highlightWord(modelData.name, searchField.text) : modelData.name + artistAndAlbum: (searchField.text.length > 0 ? Util.highlightWord(modelData.artists, searchField.text) : modelData.artists) + + " | " + + (searchField.text.length > 0 ? Util.highlightWord(modelData.album, searchField.text) : modelData.album) + creatorAndDate: (searchField.text.length > 0 ? Util.highlightWord(modelData.creator, searchField.text) : modelData.creator) + + " | " + Qt.formatDateTime(modelData.creationDate) duration: modelData.duration highlighted: modelData.isCurrentPlayingTrack starred: modelData.isStarred @@ -96,7 +104,19 @@ Page { ListView { id: tracks - anchors.fill: parent + + property bool showSearchField: false + property bool _movementFromBeginning: false + + Timer { + id: searchFieldTimer + onTriggered: tracks.showSearchField = false + interval: 5000 + } + + width: parent.width + anchors.top: searchFieldContainer.bottom + anchors.bottom: parent.bottom cacheBuffer: 3000 highlightMoveDuration: 1 @@ -107,6 +127,21 @@ Page { : "Inbox")) } + onMovementStarted: { + tracks.focus = true; + if (atYBeginning) + _movementFromBeginning = true; + } + + onContentYChanged: { + if (contentY < 0 && _movementFromBeginning) { + showSearchField = true; + searchFieldTimer.start() + } else { + _movementFromBeginning = false; + } + } + Component.onCompleted: { tracks.delegate = playlist.type == SpotifyPlaylist.Inbox ? inboxDelegate : trackDelegate positionViewAtBeginning(); @@ -120,5 +155,87 @@ Page { } } + Rectangle { + id: searchFieldContainer + anchors.top: parent.top + width: parent.width + height: 0 + color: "black" + clip: true + + Column { + id: searchColumn + y: UI.MARGIN_XLARGE + width: parent.width + spacing: UI.MARGIN_XLARGE + opacity: 0 + + AdvancedTextField { + id: searchField + placeholderText: "Search" + width: parent.width + inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText + platformSipAttributes: SipAttributes { + actionKeyLabel: "Close" + actionKeyEnabled: true + } + Keys.onReturnPressed: { tracks.focus = true } + + onTextChanged: playlist.trackFilter = Util.trim(text) + + onActiveFocusChanged: { + if (activeFocus) + searchFieldTimer.stop(); + else if (text.length === 0) + searchFieldTimer.start(); + } + } + + Separator { width: parent.width } + } + + states: State { + name: "visible" + when: searchField.text.length > 0 || searchField.activeFocus || tracks.showSearchField + PropertyChanges { + target: searchFieldContainer + height: searchColumn.height + UI.MARGIN_XLARGE + } + PropertyChanges { + target: searchColumn + opacity: 1 + } + } + + transitions: [ + Transition { + from: "visible"; to: "" + SequentialAnimation { + NumberAnimation { + properties: "opacity" + duration: 250 + } + NumberAnimation { + properties: "height" + duration: 350 + } + } + }, + Transition { + from: ""; to: "visible" + SequentialAnimation { + NumberAnimation { + properties: "height" + duration: 100 + } + NumberAnimation { + properties: "opacity" + duration: 200 + } + } + } + ] + } + Scrollbar { listView: tracks } } diff --git a/qml/Utilities.js b/qml/Utilities.js index d38788d..d6e274d 100644 --- a/qml/Utilities.js +++ b/qml/Utilities.js @@ -5,22 +5,22 @@ ** Contact: Yoann Lopes (yoann.lopes@nokia.com) ** ** This file is part of the MeeSpot project. -** +** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions ** are met: -** +** ** Redistributions of source code must retain the above copyright notice, ** this list of conditions and the following disclaimer. -** +** ** Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. -** +** ** Neither the name of Nokia Corporation and its Subsidiary(-ies) nor the names of its ** contributors may be used to endorse or promote products derived from ** this software without specific prior written permission. -** +** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS @@ -60,3 +60,23 @@ function trim(value) { return lTrim(rTrim(value)); } + +function highlightWord(string, word) { + if (trim(word).length === 0) + return string; + + var lowerCaseString = string.toLowerCase(); + var lowerCaseWord = trim(word.toLowerCase()); + var index = lowerCaseString.indexOf(lowerCaseWord); + + if (index == -1) + return string; + + if (index === 0 || lowerCaseString.substring(index - 1, index) == " ") { + return string.substring(0, index) + + "<font color='#7AB800'><u>" + string.substring(index, index + lowerCaseWord.length) + "</u></font>" + + string.substring(index + lowerCaseWord.length); + } + + return string; +} |