summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorYoann Lopes <yoann.lopes@nokia.com>2011-11-01 17:38:43 +0100
committerYoann Lopes <yoann.lopes@nokia.com>2011-11-01 17:38:43 +0100
commitaf96acce2ab301bf0d8f1b3a5cef72115af44607 (patch)
tree805c4eb8b227fe71354e251d3fc7abfce81ec6f9
parentd7bd0a8b8d4656e23cb8ce50a5532220b3784d0d (diff)
Added a search field to playlist view.
-rw-r--r--libQtSpotify/qspotifyplaylist.cpp60
-rw-r--r--libQtSpotify/qspotifyplaylist.h13
-rw-r--r--qml/InboxTrackDelegate.qml18
-rw-r--r--qml/TrackDelegate.qml8
-rw-r--r--qml/TracklistPage.qml129
-rw-r--r--qml/Utilities.js30
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;
+}