on xenial help sign in
Introduce audio cards

Approved by: Michał Sawicz, Pawel Stolowski, Andrea Cimitan

commited38dce4e5834caefc64158bc83ae2ccada6a2d0 authorAlbert Astals Cid <albert.astals@canonical.com> dateThu 7 Jan 2016, 08:04 (11 years ago) parentdec4a8c0dd0efb
23 files changed+664−118
debian/control+3−3 plugins/Dash/AudioProgressBar.qml+53 plugins/Dash/CMakeLists.txt+3 plugins/Dash/CardAudioProgress.qml+59 plugins/Dash/CardCreator.js+107−2 plugins/Dash/DashAudioPlayer.qml+96 plugins/Dash/graphics/music_progress_bg.png plugins/Dash/plugin.cpp+35 plugins/Dash/qmldir+3 qml/Dash/Previews/PreviewAudioPlayback.qml+16−76 qml/Dash/Previews/graphics/music_progress_bg.png tests/mocks/QtMultimedia/CMakeLists.txt+1 tests/mocks/QtMultimedia/audio.cpp+21−19 tests/mocks/QtMultimedia/audio.h+8−6 tests/mocks/QtMultimedia/declarativeplaylist.cpp+73 tests/mocks/QtMultimedia/declarativeplaylist.h+54 tests/mocks/QtMultimedia/plugin.cpp+2 tests/mocks/Unity/CMakeLists.txt+1−1 tests/plugins/Dash/cardcreator/9.res+112 tests/plugins/Dash/cardcreator/9.tst+3 tests/qmltests/Dash/CardHelpers.js+1−1 tests/qmltests/Dash/Previews/tst_PreviewAudioPlayback.qml+5−9 tests/qmltests/Dash/tst_Card.qml+8−1
debian/control+3−3
@@ -29,7 +29,7 @@ Build-Depends: cmake,
2929 libqt5xmlpatterns5-dev,
3030 libsystemsettings-dev,
3131 libudev-dev,
32− libunity-api-dev (>= 7.104),
32+ libunity-api-dev (>= 7.105),
3333 libusermetricsoutput1-dev,
3434 libxcb1-dev,
3535 pkg-config,
@@ -48,7 +48,7 @@ Build-Depends: cmake,
4848 qtdeclarative5-dev (>= 5.4),
4949 qtdeclarative5-dev-tools,
5050 qtdeclarative5-private-dev (>= 5.4),
51− qtdeclarative5-qtmultimedia-plugin,
51+ qtdeclarative5-qtmultimedia-plugin (>= 5.4.1-1ubuntu19~overlay2),
5252 qtdeclarative5-ubuntu-content1,
5353 qtdeclarative5-ubuntu-settings-components (>= 0.6),
5454 qtdeclarative5-ubuntu-ui-toolkit-plugin (>= 1.3.1676) | qtdeclarative5-ubuntu-ui-toolkit-plugin-gles (>= 1.3.1676),
@@ -131,7 +131,7 @@ Depends: qml-module-qtquick-layouts,
131131 unity-application-impl-12,
132132 unity-notifications-impl-3,
133133 unity-plugin-scopes | unity-scopes-impl,
134− unity-scopes-impl-7,
134+ unity-scopes-impl-9,
135135 unity8-fake-env | unity-application-impl,
136136 ${misc:Depends},
137137Breaks: unity8 (<< 7.86),
plugins/Dash/AudioProgressBar.qmlnew file+53
@@ -0,0 +1,53 @@
1+/*
2+ * Copyright 2015 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import Ubuntu.Components 1.3
19+import Dash 0.1
20+
21+Item {
22+ id: root
23+
24+ implicitHeight: progressBarImage.height
25+
26+ property url source
27+ readonly property double progress: AudioUrlComparer.compare(source, DashAudioPlayer.currentSource) ? DashAudioPlayer.progress : 0
28+
29+ Image {
30+ id: progressBarImage
31+ anchors { left: parent.left; right: parent.right }
32+ height: units.dp(6)
33+ source: "graphics/music_progress_bg.png"
34+ sourceSize.width: width
35+ sourceSize.height: height
36+ }
37+
38+ UbuntuShape {
39+ id: progressBarFill
40+ objectName: "progressBarFill"
41+
42+ readonly property int maxWidth: progressBarImage.width
43+
44+ anchors {
45+ left: progressBarImage.left
46+ right: progressBarImage.right
47+ verticalCenter: progressBarImage.verticalCenter
48+ rightMargin: maxWidth - (maxWidth * root.progress)
49+ }
50+ height: units.dp(2)
51+ backgroundColor: UbuntuColors.orange
52+ }
53+}
plugins/Dash/CMakeLists.txt+3
@@ -38,3 +38,6 @@ target_link_libraries(Dash-qml
3838qt5_use_modules(Dash-qml Qml Quick Concurrent)
3939
4040add_unity8_plugin(Dash 0.1 Dash TARGETS Dash-qml)
41+
42+install(FILES graphics/music_progress_bg.png DESTINATION ${SHELL_INSTALL_QML}/Dash/graphics/)
43+file(COPY graphics/music_progress_bg.png DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/graphics/)
plugins/Dash/CardAudioProgress.qmlnew file+59
@@ -0,0 +1,59 @@
1+/*
2+ * Copyright 2015 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import Ubuntu.Components 1.3
19+import Dash 0.1
20+
21+Item {
22+ id: root
23+
24+ implicitHeight: positionLabel.y + positionLabel.height
25+ visible: AudioUrlComparer.compare(source, DashAudioPlayer.currentSource)
26+
27+ property int duration: 0
28+ property alias source: progress.source
29+ property color color: theme.palette.normal.baseText
30+ readonly property int position: root.visible ? DashAudioPlayer.position / 1000 : 0
31+
32+ AudioProgressBar {
33+ id: progress
34+ anchors { left: parent.left; right: parent.right }
35+ }
36+
37+ Label {
38+ id: positionLabel
39+ anchors {
40+ left: parent.left
41+ top: progress.bottom
42+ }
43+ verticalAlignment: Text.AlignBottom
44+ fontSize: "x-small"
45+ text: DashAudioPlayer.lengthToString(root.position)
46+ color: root.color
47+ }
48+
49+ Label {
50+ anchors {
51+ right: parent.right
52+ top: progress.bottom
53+ }
54+ verticalAlignment: Text.AlignBottom
55+ fontSize: "x-small"
56+ text: DashAudioPlayer.lengthToString(duration)
57+ color: root.color
58+ }
59+}
plugins/Dash/CardCreator.js+107−2
@@ -136,6 +136,43 @@ var kArtShapeHolderCode = 'Item { \n\
136136 } \n\
137137 }\n';
138138
139+// %1 is anchors.fill
140+// %2 is width
141+// %3 is height
142+var kAudioButtonCode = 'AbstractButton { \n\
143+ id: audioButton; \n\
144+ anchors.fill: %1; \n\
145+ width: %2; \n\
146+ height: %3; \n\
147+ readonly property url source: (cardData["quickPreviewData"] && cardData["quickPreviewData"]["uri"]) || ""; \n\
148+ UbuntuShape { \n\
149+ anchors.fill: parent; \n\
150+ visible: parent.pressed; \n\
151+ radius: "medium"; \n\
152+ } \n\
153+ Icon { \n\
154+ anchors.fill: parent; \n\
155+ anchors.margins: parent.height > units.gu(5) ? units.gu(2) : 0; \n\
156+ opacity: 0.9; \n\
157+ name: DashAudioPlayer.playing && AudioUrlComparer.compare(parent.source, DashAudioPlayer.currentSource) ? "media-playback-pause" : "media-playback-start"; \n\
158+ } \n\
159+ onClicked: { \n\
160+ if (AudioUrlComparer.compare(source, DashAudioPlayer.currentSource)) { \n\
161+ if (DashAudioPlayer.playing) { \n\
162+ DashAudioPlayer.pause(); \n\
163+ } else { \n\
164+ DashAudioPlayer.play(); \n\
165+ } \n\
166+ } else { \n\
167+ var playlist = (cardData["quickPreviewData"] && cardData["quickPreviewData"]["playlist"]) || null; \n\
168+ DashAudioPlayer.playSource(source, playlist); \n\
169+ } \n\
170+ } \n\
171+ onPressAndHold: { \n\
172+ root.pressAndHold(); \n\
173+ } \n\
174+ }';
175+
139176var kOverlayLoaderCode = 'Loader { \n\
140177 id: overlayLoader; \n\
141178 readonly property real overlayHeight: (fixedHeaderHeight > 0 ? fixedHeaderHeight : headerHeight) + units.gu(2); \n\
@@ -316,6 +353,22 @@ var kSummaryLabelCode = 'Label { \n\
316353 color: %3; \n\
317354 }\n';
318355
356+// %1 is used as bottom anchor of audio progress bar
357+// %2 is used as left anchor of audio progress bar
358+// %3 is used as text color
359+var kAudioProgressBarCode = 'CardAudioProgress { \n\
360+ id: audioProgressBar; \n\
361+ duration: (cardData["quickPreviewData"] && cardData["quickPreviewData"]["duration"]) || 0; \n\
362+ source: (cardData["quickPreviewData"] && cardData["quickPreviewData"]["uri"]) || ""; \n\
363+ anchors { \n\
364+ bottom: %1; \n\
365+ left: %2; \n\
366+ right: parent.right; \n\
367+ margins: units.gu(1); \n\
368+ } \n\
369+ color: %3; \n\
370+ }';
371+
319372function cardString(template, components) {
320373 var code;
321374
@@ -351,7 +404,20 @@ function cardString(template, components) {
351404 var headerAsOverlay = hasArt && template && template["overlay"] === true && (hasTitle || hasMascot);
352405 var hasSubtitle = hasTitle && components["subtitle"] || false;
353406 var hasHeaderRow = hasMascot && hasTitle;
354− var hasAttributes = hasTitle && components["attributes"]["field"] || false;
407+ var hasAttributes = hasTitle && components["attributes"] && components["attributes"]["field"] || false;
408+ var isAudio = template["quick-preview-type"] === "audio";
409+
410+ if (isAudio) {
411+ // For now we only support audio cards with [optional] art, title, subtitle
412+ // in horizontal mode
413+ // Anything else makes it behave not like an audio card
414+ if (hasSummary) isAudio = false;
415+ if (!isHorizontal) isAudio = false;
416+ if (hasMascot) isAudio = false;
417+ if (hasEmblem) isAudio = false;
418+ if (headerAsOverlay) isAudio = false;
419+ if (hasAttributes) isAudio = false;
420+ }
355421
356422 if (hasBackground) {
357423 var templateCardBackground = (template && typeof template["card-background"] === "string") ? template["card-background"] : "";
@@ -421,17 +487,22 @@ function cardString(template, components) {
421487 topMargin: units.gu(1);\n';
422488 }
423489 }
490+
424491 var headerLeftAnchor;
425492 var headerLeftAnchorHasMargin = false;
426493 if (isHorizontal && hasArt) {
427494 headerLeftAnchor = 'left: artShapeHolder.right; \n\
428495 leftMargin: units.gu(1);\n';
429496 headerLeftAnchorHasMargin = true;
497+ } else if (isHorizontal && isAudio) {
498+ headerLeftAnchor = 'left: audioButton.right; \n\
499+ leftMargin: units.gu(1);\n';
500+ headerLeftAnchorHasMargin = true;
430501 } else {
431502 headerLeftAnchor = 'left: parent.left;\n';
432503 }
433504
434− var touchdownOnArtShape = !hasBackground && hasArt && !hasMascot && !hasSummary;
505+ var touchdownOnArtShape = !hasBackground && hasArt && !hasMascot && !hasSummary && !isAudio;
435506
436507 if (hasHeaderRow) {
437508 code += 'readonly property int headerHeight: row.height;\n'
@@ -445,6 +516,14 @@ function cardString(template, components) {
445516 } else {
446517 code += 'readonly property int headerHeight: attributesRow.height;\n'
447518 }
519+ } else if (isAudio) {
520+ if (hasSubtitle) {
521+ code += 'readonly property int headerHeight: titleLabel.height + subtitleLabel.height + subtitleLabel.anchors.topMargin + audioProgressBar.height + audioProgressBar.anchors.topMargin;\n'
522+ } else if (hasTitle) {
523+ code += 'readonly property int headerHeight: titleLabel.height + audioProgressBar.height + audioProgressBar.anchors.topMargin;\n'
524+ } else {
525+ code += 'readonly property int headerHeight: audioProgressBar.height;\n'
526+ }
448527 } else if (hasSubtitle) {
449528 code += 'readonly property int headerHeight: titleLabel.height + subtitleLabel.height + subtitleLabel.anchors.topMargin;\n'
450529 } else if (hasTitle) {
@@ -632,6 +711,30 @@ function cardString(template, components) {
632711 code += mascotShapeCode + mascotCode + titleSubtitleCode;
633712 }
634713
714+ if (isAudio) {
715+ var audioProgressBarLeftAnchor = 'audioButton.right';
716+ var audioProgressBarBottomAnchor = 'audioButton.bottom';
717+ var audioProgressBarTextColor = 'root.scopeStyle ? root.scopeStyle.foreground : theme.palette.normal.baseText';
718+
719+ code += kAudioProgressBarCode.arg(audioProgressBarBottomAnchor)
720+ .arg(audioProgressBarLeftAnchor)
721+ .arg(audioProgressBarTextColor);
722+
723+ var audioButtonAnchorsFill;
724+ var audioButtonWidth;
725+ var audioButtonHeight;
726+ if (hasArt) {
727+ audioButtonAnchorsFill = 'artShapeHolder';
728+ audioButtonWidth = 'undefined';
729+ audioButtonHeight = 'undefined';
730+ } else {
731+ audioButtonAnchorsFill = 'undefined';
732+ audioButtonWidth = 'height';
733+ audioButtonHeight = '(root.fixedHeaderHeight > 0 ? root.fixedHeaderHeight : headerHeight) + 2 * units.gu(1)';
734+ }
735+ code += kAudioButtonCode.arg(audioButtonAnchorsFill).arg(audioButtonWidth).arg(audioButtonHeight);
736+ }
737+
635738 if (hasSummary) {
636739 var summaryTopAnchor;
637740 if (isHorizontal && hasArt) summaryTopAnchor = 'artShapeHolder.bottom';
@@ -670,6 +773,8 @@ function cardString(template, components) {
670773 var implicitHeight = 'implicitHeight: ';
671774 if (hasSummary) {
672775 implicitHeight += 'summary.y + summary.height + units.gu(1);\n';
776+ } else if (isAudio) {
777+ implicitHeight += 'audioButton.height;\n';
673778 } else if (headerAsOverlay) {
674779 implicitHeight += 'artShapeHolder.height;\n';
675780 } else if (hasHeaderRow) {
plugins/Dash/DashAudioPlayer.qmlnew file+96
@@ -0,0 +1,96 @@
1+/*
2+ * Copyright (C) 2015 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+pragma Singleton
18+import QtQuick 2.4
19+import QtMultimedia 5.6
20+import Dash 0.1
21+
22+QtObject {
23+ readonly property real progress: audio.position / audio.duration
24+ readonly property bool playing: audio.playbackState === Audio.PlayingState
25+ readonly property bool paused: audio.playbackState === Audio.PausedState
26+ readonly property bool stopped: audio.playbackState === Audio.StoppedState
27+ readonly property alias position: audio.position
28+ readonly property url currentSource: audio.playlist.currentItemSource
29+
30+ function playSource(newSource, newPlaylist) {
31+ stop();
32+ audio.playlist.clear();
33+ if (newPlaylist) {
34+ // Look for newSource in newPlaylist
35+ var sourceIndex = -1;
36+ for (var i in newPlaylist) {
37+ if (AudioUrlComparer.compare(newSource, newPlaylist[i])) {
38+ sourceIndex = i;
39+ break;
40+ }
41+ }
42+ var urls = [];
43+ if (sourceIndex === -1 && newSource != "") {
44+ // If the playing song is not in the playlist, add it
45+ urls.push(newSource);
46+ sourceIndex = 0;
47+ }
48+ for (var i in newPlaylist) {
49+ urls.push(newPlaylist[i]);
50+ }
51+ audio.playlist.addItems(urls);
52+ audio.playlist.currentIndex = sourceIndex;
53+ } else {
54+ audio.playlist.addItem(newSource);
55+ audio.playlist.currentIndex = 0;
56+ }
57+ play();
58+ }
59+
60+ function stop() {
61+ audio.stop();
62+ }
63+
64+ function play() {
65+ audio.play();
66+ }
67+
68+ function pause() {
69+ audio.pause();
70+ }
71+
72+ property QtObject audio: Audio {
73+ id: audio
74+ objectName: "audio"
75+ playlist: Playlist {
76+ objectName: "playlist"
77+ }
78+
79+ onErrorStringChanged: console.warn("Dash Audio player error:", errorString)
80+ }
81+
82+ function lengthToString(s) {
83+ if (typeof(s) !== "number" || s < 0) return "";
84+
85+ var sec = "" + s % 60;
86+ if (sec.length == 1) sec = "0" + sec;
87+ var hour = Math.floor(s / 3600);
88+ if (hour < 1) {
89+ return Math.floor(s / 60) + ":" + sec;
90+ } else {
91+ var min = "" + Math.floor(s / 60) % 60;
92+ if (min.length == 1) min = "0" + min;
93+ return hour + ":" + min + ":" + sec;
94+ }
95+ }
96+}
plugins/Dash/graphics/music_progress_bg.pngnew file

Binary file not shown.

plugins/Dash/plugin.cpp+35
@@ -23,6 +23,38 @@
2323#include "verticaljournal.h"
2424
2525#include <QAbstractItemModel>
26+#include <QUrlQuery>
27+
28+static QUrl oauthCleanedUrl(QUrl u)
29+{
30+ QUrlQuery q(u);
31+ q.removeQueryItem("oauth_nonce");
32+ q.removeQueryItem("oauth_timestamp");
33+ q.removeQueryItem("oauth_consumer_key");
34+ q.removeQueryItem("oauth_signature_method");
35+ q.removeQueryItem("oauth_version");
36+ q.removeQueryItem("oauth_signature");
37+ u.setQuery(q);
38+ return u;
39+}
40+
41+class AudioComparer : public QObject
42+{
43+ Q_OBJECT
44+public:
45+ Q_INVOKABLE bool compare(const QUrl &url1, const QUrl &url2)
46+ {
47+ return oauthCleanedUrl(url1) == oauthCleanedUrl(url2);
48+ }
49+};
50+
51+static QObject *audio_comparer_singleton_provider(QQmlEngine *engine, QJSEngine *scriptEngine)
52+{
53+ Q_UNUSED(engine)
54+ Q_UNUSED(scriptEngine)
55+
56+ return new AudioComparer();
57+}
2658
2759void DashPlugin::registerTypes(const char *uri)
2860{
@@ -32,4 +64,7 @@ void DashPlugin::registerTypes(const char *uri)
3264 qmlRegisterType<ListViewWithPageHeader>(uri, 0, 1, "ListViewWithPageHeader");
3365 qmlRegisterType<OrganicGrid>(uri, 0, 1, "OrganicGrid");
3466 qmlRegisterType<VerticalJournal>(uri, 0, 1, "VerticalJournal");
67+ qmlRegisterSingletonType<AudioComparer>(uri, 0, 1, "AudioUrlComparer", audio_comparer_singleton_provider);
3568}
69+
70+#include "plugin.moc"
plugins/Dash/qmldir+3
@@ -2,6 +2,9 @@ module Dash
22plugin Dash-qml
33typeinfo Dash.qmltypes
44singleton CardCreatorCache 0.1 CardCreatorCache.qml
5+singleton DashAudioPlayer 0.1 DashAudioPlayer.qml
56ScopeStyle 0.1 ScopeStyle.qml
67CardAttributes 0.1 CardAttributes.qml
78CroppedImageMinimumSourceSize 0.1 CroppedImageMinimumSourceSize.qml
9+AudioProgressBar 0.1 AudioProgressBar.qml
10+CardAudioProgress 0.1 CardAudioProgress.qml
qml/Dash/Previews/PreviewAudioPlayback.qml+16−76
@@ -15,8 +15,8 @@
1515 */
1616
1717import QtQuick 2.4
18−import QtMultimedia 5.0
1918import Ubuntu.Components 1.3
19+import Dash 0.1
2020
2121/*! \brief Preview widget for audio tracks.
2222
@@ -36,38 +36,6 @@ PreviewWidget {
3636 id: root
3737 implicitHeight: childrenRect.height
3838
39− onIsCurrentPreviewChanged: if (!isCurrentPreview) audio.stop()
40−
41− Audio {
42− id: audio
43− objectName: "audio"
44−
45− property real progress: audio.position / audio.duration
46− property Item playingItem
47−
48− Component.onDestruction: {
49− // destroying the component doesn't automatically send stop to the media service, probably a bug in QtMultimedia
50− audio.stop();
51− }
52−
53− onErrorStringChanged: console.warn("Audio player error:", errorString)
54−
55− function lengthToString(s) {
56− if (typeof(s) !== "number" || s <= 0) return "";
57−
58− var sec = "" + s % 60;
59− if (sec.length == 1) sec = "0" + sec;
60− var hour = Math.floor(s / 3600);
61− if (hour < 1) {
62− return Math.floor(s / 60) + ":" + sec;
63− } else {
64− var min = "" + Math.floor(s / 60) % 60;
65− if (min.length == 1) min = "0" + min;
66− return hour + ":" + min + ":" + sec;
67− }
68− }
69− }
70−
7139 Column {
7240 anchors { left: parent.left; right: parent.right }
7341 visible: trackRepeater.count > 0
@@ -77,20 +45,12 @@ PreviewWidget {
7745 objectName: "trackRepeater"
7846 model: root.widgetData["tracks"]
7947
80− function play(item, source) {
81− audio.stop();
82− // Make sure we change the source, even if two items point to the same uri location
83− audio.source = "";
84− audio.source = source;
85− audio.playingItem = item;
86− audio.play();
87− }
88−
8948 delegate: Item {
9049 id: trackItem
9150 objectName: "trackItem" + index
9251
93− property bool isPlayingItem: audio.playingItem == trackItem
52+ readonly property url sourceUrl: modelData["source"]
53+ readonly property bool isPlayingItem: AudioUrlComparer.compare(sourceUrl, DashAudioPlayer.currentSource)
9454
9555 anchors { left: parent.left; right: parent.right }
9656 height: units.gu(5)
@@ -98,9 +58,9 @@ PreviewWidget {
9858 Row {
9959 id: trackRow
10060
101− property int column1Width: units.gu(3)
102− property int column2Width: width - (2 * spacing) - column1Width - column3Width
103− property int column3Width: units.gu(4)
61+ readonly property int column1Width: units.gu(3)
62+ readonly property int column2Width: width - (2 * spacing) - column1Width - column3Width
63+ readonly property int column3Width: units.gu(4)
10464
10565 anchors.verticalCenter: parent.verticalCenter
10666 width: parent.width
@@ -110,7 +70,7 @@ PreviewWidget {
11070 objectName: "playButton"
11171 width: trackRow.column1Width
11272 height: width
113− iconSource: audio.playbackState == Audio.PlayingState && trackItem.isPlayingItem ? "image://theme/media-playback-pause" : "image://theme/media-playback-start"
73+ iconSource: DashAudioPlayer.playing && trackItem.isPlayingItem ? "image://theme/media-playback-pause" : "image://theme/media-playback-start"
11474
11575 // Can't be "transparent" or "#00xxxxxx" as the button optimizes away the surrounding shape
11676 // FIXME when this is resolved: https://bugs.launchpad.net/ubuntu-ui-toolkit/+bug/1251685
@@ -118,13 +78,13 @@ PreviewWidget {
11878
11979 onClicked: {
12080 if (trackItem.isPlayingItem) {
121− if (audio.playbackState == Audio.PlayingState) {
122− audio.pause();
123− } else if (audio.playbackState == Audio.PausedState) {
124− audio.play();
81+ if (DashAudioPlayer.playing) {
82+ DashAudioPlayer.pause();
83+ } else if (DashAudioPlayer.paused) {
84+ DashAudioPlayer.play();
12585 }
12686 } else {
127− trackRepeater.play(trackItem, modelData["source"]);
87+ DashAudioPlayer.playSource(sourceUrl);
12888 }
12989 }
13090 }
@@ -160,30 +120,10 @@ PreviewWidget {
160120 elide: Text.ElideRight
161121 }
162122
163− UbuntuShape {
164− id: progressBarFill
165− objectName: "progressBarFill"
166−
167− property int maxWidth: progressBarImage.width - units.dp(4)
168−
169− anchors {
170− left: progressBarImage.left
171− right: progressBarImage.right
172− verticalCenter: progressBarImage.verticalCenter
173− margins: units.dp(2)
174− rightMargin: maxWidth - (maxWidth * audio.progress) + units.dp(2)
175− }
176− height: units.dp(2)
177− visible: progressBarImage.visible
178− backgroundColor: UbuntuColors.orange
179− }
180−
181− Image {
182− id: progressBarImage
123+ AudioProgressBar {
183124 anchors { left: parent.left; top: parent.bottom; right: parent.right }
184− height: units.dp(6)
185− visible: audio.playbackState != Audio.StoppedState && trackItem.isPlayingItem && modelData["length"] > 0
186− source: "graphics/music_progress_bg.png"
125+ visible: !DashAudioPlayer.stopped && trackItem.isPlayingItem && modelData["length"] > 0
126+ source: sourceUrl
187127 }
188128 }
189129
@@ -196,7 +136,7 @@ PreviewWidget {
196136 color: scopeStyle ? scopeStyle.foreground : theme.palette.normal.baseText
197137 fontSize: "small"
198138 horizontalAlignment: Text.AlignRight
199− text: audio.lengthToString(modelData["length"])
139+ text: DashAudioPlayer.lengthToString(modelData["length"])
200140 }
201141 }
202142 }
qml/Dash/Previews/graphics/music_progress_bg.pngdeleted

Binary file not shown.

tests/mocks/QtMultimedia/CMakeLists.txt+1
@@ -1,6 +1,7 @@
11add_library(QtMultimedia-qml MODULE
22 plugin.cpp
33 audio.cpp
4+ declarativeplaylist.cpp
45 )
56
67qt5_use_modules(QtMultimedia-qml Qml)
tests/mocks/QtMultimedia/audio.cpp+21−19
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2013, 2015 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -20,30 +20,18 @@
2020
2121Audio::Audio(QObject* parent):
2222 QObject(parent),
23− m_playbackState(StoppedState)
23+ m_playbackState(StoppedState),
24+ m_playlist(0)
2425{
2526 qsrand(time(nullptr));
2627 m_timer.setInterval(1000);
2728 connect(&m_timer, &QTimer::timeout, this, &Audio::timerEvent);
28−}
29−
30−QUrl Audio::source() const
31−{
32− return m_source;
33−}
3429
35−void Audio::setSource(const QUrl &source)
36−{
37− if (m_source != source) {
38− m_source = source;
39− Q_EMIT sourceChanged(source);
40−
41− m_position = 0;
42− Q_EMIT positionChanged(m_position);
30+ m_position = 0;
31+ Q_EMIT positionChanged(m_position);
4332
44− m_duration = (qrand() % 20000) + 10000;
45− Q_EMIT durationChanged(m_duration);
46− }
33+ m_duration = (qrand() % 20000) + 10000;
34+ Q_EMIT durationChanged(m_duration);
4735}
4836
4937Audio::PlaybackState Audio::playbackState() const
@@ -115,3 +103,17 @@ void Audio::setAudioRole(Audio::AudioRole audioRole)
115103{
116104 Q_UNUSED(audioRole);
117105}
106+
107+DeclarativePlaylist *Audio::playlist() const
108+{
109+ return m_playlist;
110+}
111+
112+void Audio::setPlaylist(DeclarativePlaylist *playlist)
113+{
114+ if (playlist == m_playlist)
115+ return;
116+
117+ m_playlist = playlist;
118+ Q_EMIT playlistChanged();
119+}
tests/mocks/QtMultimedia/audio.h+8−6
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2012,2013 Canonical, Ltd.
2+ * Copyright (C) 2012, 2013, 2015 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -13,7 +13,6 @@
1313 * You should have received a copy of the GNU General Public License
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1515 *
16− * Authors: Michael Zanetti <michael.zanetti@canonical.com>
1716 */
1817
1918#ifndef MOCK_AUDIO_H
@@ -23,17 +22,19 @@
2322#include <QUrl>
2423#include <QTimer>
2524
25+class DeclarativePlaylist;
26+
2627class Audio: public QObject
2728{
2829 Q_OBJECT
2930 Q_ENUMS(PlaybackState)
3031 Q_ENUMS(AudioRole)
31− Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged)
3232 Q_PROPERTY(PlaybackState playbackState READ playbackState NOTIFY playbackStateChanged)
3333 Q_PROPERTY(int position READ position NOTIFY positionChanged)
3434 Q_PROPERTY(int duration READ duration NOTIFY durationChanged)
3535 Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged)
3636 Q_PROPERTY(AudioRole audioRole READ audioRole WRITE setAudioRole)
37+ Q_PROPERTY(DeclarativePlaylist *playlist READ playlist WRITE setPlaylist NOTIFY playlistChanged)
3738public:
3839 enum PlaybackState {
3940 PlayingState,
@@ -50,8 +51,8 @@ public:
5051
5152 explicit Audio(QObject *parent = 0);
5253
53− QUrl source() const;
54− void setSource(const QUrl &source);
54+ DeclarativePlaylist *playlist() const;
55+ void setPlaylist(DeclarativePlaylist *playlist);
5556
5657 PlaybackState playbackState() const;
5758
@@ -70,6 +71,7 @@ public Q_SLOTS:
7071 void stop();
7172
7273Q_SIGNALS:
74+ void playlistChanged();
7375 void sourceChanged(const QUrl &source);
7476 void playbackStateChanged(PlaybackState playbackState);
7577 void positionChanged(int position);
@@ -80,11 +82,11 @@ private Q_SLOTS:
8082 void timerEvent();
8183
8284private:
83− QUrl m_source;
8485 PlaybackState m_playbackState;
8586 QTimer m_timer;
8687 int m_position;
8788 int m_duration;
89+ DeclarativePlaylist *m_playlist;
8890};
8991
9092#endif
tests/mocks/QtMultimedia/declarativeplaylist.cppnew file+73
@@ -0,0 +1,73 @@
1+/*
2+ * Copyright (C) 2015 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ *
16+ */
17+
18+#include "declarativeplaylist.h"
19+
20+DeclarativePlaylist::DeclarativePlaylist(QObject *parent)
21+ : QObject(parent)
22+ , m_currentIndex(-1)
23+{
24+}
25+
26+QUrl DeclarativePlaylist::currentItemSource() const
27+{
28+ return itemSource(currentIndex());
29+}
30+
31+int DeclarativePlaylist::currentIndex() const
32+{
33+ return m_currentIndex;
34+}
35+
36+void DeclarativePlaylist::setCurrentIndex(int index)
37+{
38+ if (currentIndex() == index)
39+ return;
40+
41+ m_currentIndex = index;
42+ Q_EMIT currentIndexChanged();
43+ Q_EMIT currentItemSourceChanged();
44+}
45+
46+QUrl DeclarativePlaylist::itemSource(int index) const
47+{
48+ if (index < 0 || index >= m_medias.count())
49+ return QUrl();
50+ return m_medias[index];
51+}
52+
53+bool DeclarativePlaylist::addItem(const QUrl &source)
54+{
55+ m_medias << source;
56+ setCurrentIndex(0);
57+ return true;
58+}
59+
60+bool DeclarativePlaylist::addItems(const QList<QUrl> &sources)
61+{
62+ m_medias << sources;
63+ if (!sources.isEmpty())
64+ setCurrentIndex(0);
65+ return true;
66+}
67+
68+bool DeclarativePlaylist::clear()
69+{
70+ m_medias.clear();
71+ setCurrentIndex(-1);
72+ return true;
73+}
tests/mocks/QtMultimedia/declarativeplaylist.hnew file+54
@@ -0,0 +1,54 @@
1+/*
2+ * Copyright (C) 2015 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ *
16+ */
17+
18+#ifndef DECLARATIVEPLAYLIST_H
19+#define DECLARATIVEPLAYLIST_H
20+
21+#include <QObject>
22+#include <QUrl>
23+
24+class DeclarativePlaylist : public QObject
25+{
26+ Q_OBJECT
27+ Q_PROPERTY(QUrl currentItemSource READ currentItemSource NOTIFY currentItemSourceChanged)
28+ Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged)
29+
30+public:
31+ DeclarativePlaylist(QObject *parent = 0);
32+
33+ QUrl currentItemSource() const;
34+ int currentIndex() const;
35+ void setCurrentIndex(int currentIndex);
36+
37+public Q_SLOTS:
38+ QUrl itemSource(int index) const;
39+ bool addItem(const QUrl &source);
40+ bool addItems(const QList<QUrl> &sources);
41+ bool clear();
42+
43+Q_SIGNALS:
44+ void currentItemSourceChanged();
45+ void currentIndexChanged();
46+
47+private:
48+ Q_DISABLE_COPY(DeclarativePlaylist)
49+
50+ int m_currentIndex;
51+ QList<QUrl> m_medias;
52+};
53+
54+#endif
tests/mocks/QtMultimedia/plugin.cpp+2
@@ -18,6 +18,7 @@
1818
1919#include "plugin.h"
2020#include "audio.h"
21+#include "declarativeplaylist.h"
2122
2223#include <QtQml/qqml.h>
2324
@@ -26,4 +27,5 @@ void MockQtMultimediaPlugin::registerTypes(const char *uri)
2627 Q_ASSERT(uri == QLatin1String("QtMultimedia"));
2728 qmlRegisterType<Audio>(uri, 5, 0, "Audio");
2829 qmlRegisterType<Audio>(uri, 5, 0, "MediaPlayer");
30+ qmlRegisterType<DeclarativePlaylist>(uri, 5, 6, "Playlist");
2931}
tests/mocks/Unity/CMakeLists.txt+1−1
@@ -7,7 +7,7 @@ add_subdirectory(Notifications)
77add_subdirectory(DashCommunicator)
88
99pkg_search_module(GOBJECT gobject-2.0 REQUIRED)
10−pkg_check_modules(SCOPES_API REQUIRED unity-shell-scopes=8)
10+pkg_check_modules(SCOPES_API REQUIRED unity-shell-scopes=9)
1111
1212include_directories(
1313 ${CMAKE_CURRENT_BINARY_DIR}
tests/plugins/Dash/cardcreator/9.resnew file+112
@@ -0,0 +1,112 @@
1+AbstractButton {
2+ id: root;
3+ property var components;
4+ property var cardData;
5+ property string artShapeStyle: "inset";
6+ property real fontScale: 1.0;
7+ property var scopeStyle: null;
8+ property int titleAlignment: Text.AlignLeft;
9+ property int fixedHeaderHeight: -1;
10+ property size fixedArtShapeSize: Qt.size(-1, -1);
11+ readonly property string title: cardData && cardData["title"] || "";
12+ property bool asynchronous: true;
13+ property bool showHeader: true;
14+ implicitWidth: childrenRect.width;
15+ enabled: true;
16+
17+readonly property size artShapeSize: Qt.size(-1, -1);
18+readonly property int headerHeight: titleLabel.height + subtitleLabel.height + subtitleLabel.anchors.topMargin + audioProgressBar.height + audioProgressBar.anchors.topMargin;
19+Label {
20+ id: titleLabel;
21+ objectName: "titleLabel";
22+ anchors { right: parent.right;
23+rightMargin: units.gu(1);
24+left: audioButton.right;
25+ leftMargin: units.gu(1);
26+top: parent.top;
27+ topMargin: units.gu(1);
28+ }
29+ elide: Text.ElideRight;
30+ fontSize: "small";
31+ wrapMode: Text.Wrap;
32+ maximumLineCount: 2;
33+ font.pixelSize: Math.round(FontUtils.sizeToPixels(fontSize) * fontScale);
34+ color: root.scopeStyle ? root.scopeStyle.foreground : theme.palette.normal.baseText;
35+ visible: showHeader ;
36+ width: undefined;
37+ text: root.title;
38+ font.weight: cardData && cardData["subtitle"] ? Font.DemiBold : Font.Normal;
39+ horizontalAlignment: root.titleAlignment;
40+ }
41+Label {
42+ id: subtitleLabel;
43+ objectName: "subtitleLabel";
44+ anchors { left: titleLabel.left;
45+ leftMargin: titleLabel.leftMargin;
46+rightMargin: units.gu(1);
47+right: titleLabel.right;
48+top: titleLabel.bottom;
49+ }
50+ anchors.topMargin: units.dp(2);
51+ elide: Text.ElideRight;
52+ maximumLineCount: 1;
53+ fontSize: "x-small";
54+ font.pixelSize: Math.round(FontUtils.sizeToPixels(fontSize) * fontScale);
55+ color: root.scopeStyle ? root.scopeStyle.foreground : theme.palette.normal.baseText;
56+ visible: titleLabel.visible && titleLabel.text;
57+ text: cardData && cardData["subtitle"] || "";
58+ font.weight: Font.Light;
59+ }
60+CardAudioProgress {
61+ id: audioProgressBar;
62+ duration: (cardData["quickPreviewData"] && cardData["quickPreviewData"]["duration"]) || 0;
63+ source: (cardData["quickPreviewData"] && cardData["quickPreviewData"]["uri"]) || "";
64+ anchors {
65+ bottom: audioButton.bottom;
66+ left: audioButton.right;
67+ right: parent.right;
68+ margins: units.gu(1);
69+ }
70+ color: root.scopeStyle ? root.scopeStyle.foreground : theme.palette.normal.baseText;
71+ }AbstractButton {
72+ id: audioButton;
73+ anchors.fill: undefined;
74+ width: height;
75+ height: (root.fixedHeaderHeight > 0 ? root.fixedHeaderHeight : headerHeight) + 2 * units.gu(1);
76+ readonly property url source: (cardData["quickPreviewData"] && cardData["quickPreviewData"]["uri"]) || "";
77+ UbuntuShape {
78+ anchors.fill: parent;
79+ visible: parent.pressed;
80+ radius: "medium";
81+ }
82+ Icon {
83+ anchors.fill: parent;
84+ anchors.margins: parent.height > units.gu(5) ? units.gu(2) : 0;
85+ opacity: 0.9;
86+ name: DashAudioPlayer.playing && AudioUrlComparer.compare(parent.source, DashAudioPlayer.currentSource) ? "media-playback-pause" : "media-playback-start";
87+ }
88+ onClicked: {
89+ if (AudioUrlComparer.compare(source, DashAudioPlayer.currentSource)) {
90+ if (DashAudioPlayer.playing) {
91+ DashAudioPlayer.pause();
92+ } else {
93+ DashAudioPlayer.play();
94+ }
95+ } else {
96+ var playlist = (cardData["quickPreviewData"] && cardData["quickPreviewData"]["playlist"]) || null;
97+ DashAudioPlayer.playSource(source, playlist);
98+ }
99+ }
100+ onPressAndHold: {
101+ root.pressAndHold();
102+ }
103+ }UbuntuShape {
104+ id: touchdown;
105+ objectName: "touchdown";
106+ anchors { fill: root }
107+ visible: root.artShapeStyle != "shadow" && root.artShapeStyle != "icon" && root.pressed;
108+ radius: "medium";
109+ borderSource: "radius_pressed.sci"
110+ }
111+implicitHeight: audioButton.height;
112+}
tests/plugins/Dash/cardcreator/9.tstnew file+3
@@ -0,0 +1,3 @@
1+template: {"card-layout":"horizontal","card-size":"medium","category-layout":"grid","collapsed-rows":2,"quick-preview-type": "audio"}
2+components: {"art":{"aspect-ratio":1},"subtitle":{"field":"author"},"title":{"field":"title"}, "quickPreviewData": "quickPreviewData"}
3+result: 9.res
tests/qmltests/Dash/CardHelpers.js+1−1
@@ -16,7 +16,7 @@
1616
1717.pragma library
1818
19−var components = ["title", "art", "subtitle", "mascot", "emblem", "summary", "attributes", "overlayColor"]
19+var components = ["title", "art", "subtitle", "mascot", "emblem", "summary", "attributes", "overlayColor", "quickPreviewData"]
2020
2121var defaultLayout = ' \
2222{ \
tests/qmltests/Dash/Previews/tst_PreviewAudioPlayback.qml+5−9
@@ -18,6 +18,7 @@ import QtQuick 2.4
1818import QtTest 1.0
1919import Ubuntu.Components 1.3
2020import "../../../../qml/Dash/Previews"
21+import Dash 0.1
2122import Unity.Test 0.1 as UT
2223import QtMultimedia 5.0
2324
@@ -58,8 +59,8 @@ Rectangle {
5859 function test_time_formatter_data() {
5960 return [
6061 { tag: "NaN", value: "not a number", result: "" },
61− { tag: "0", value: 0, result: "" },
6262 { tag: "-1", value: -1, result: "" },
63+ { tag: "0", value: 0, result: "0:00" },
6364 { tag: "30", value: 30, result: "0:30" },
6465 { tag: "60", value: 60, result: "1:00" },
6566 { tag: "3600", value: 3600, result: "1:00:00" }
@@ -67,8 +68,7 @@ Rectangle {
6768 }
6869
6970 function test_time_formatter(data) {
70− var audio = findInvisibleChild(previewAudioPlayback, "audio");
71− compare(audio.lengthToString(data.value), data.result)
71+ compare(DashAudioPlayer.lengthToString(data.value), data.result)
7272 }
7373
7474 function test_tracks_data() {
@@ -102,7 +102,7 @@ Rectangle {
102102
103103 function checkPlayerSource(index) {
104104 var modelFilename = previewAudioPlayback.widgetData["tracks"][index]["source"].replace(/^.*[\\\/]/, '');
105− var playerFilename = findInvisibleChild(previewAudioPlayback, "audio").source.toString().replace(/^.*[\\\/]/, '');
105+ var playerFilename = DashAudioPlayer.currentSource.toString().replace(/^.*[\\\/]/, '');
106106
107107 compare(modelFilename, playerFilename, "Player source is not set correctly.");
108108 }
@@ -123,7 +123,7 @@ Rectangle {
123123 var track1PlayButton = findChild(track1Item, "playButton");
124124 var track2PlayButton = findChild(track2Item, "playButton");
125125
126− var audio = findInvisibleChild(previewAudioPlayback, "audio");
126+ var audio = DashAudioPlayer.audio;
127127
128128 // All progress bars must be hidden in the beginning
129129 compare(track0ProgressBar.visible, false);
@@ -170,10 +170,6 @@ Rectangle {
170170 tryCompare(track0ProgressBar, "visible", false);
171171 tryCompare(track1ProgressBar, "visible", false);
172172 tryCompare(track2ProgressBar, "visible", true);
173−
174− // Changing preview should make all players shut up!
175− previewAudioPlayback.isCurrentPreview = false
176− tryCompare(audio, "playbackState", Audio.StoppedState);
177173 }
178174 }
179175}
tests/qmltests/Dash/tst_Card.qml+8−1
@@ -35,7 +35,8 @@ Rectangle {
3535 "title": "foo",
3636 "subtitle": "bar",
3737 "summary": "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
38− "attributes": [{"value":"text1","icon":"image://theme/ok"},{"value":"text2","icon":"image://theme/cancel"}]
38+ "attributes": [{"value":"text1","icon":"image://theme/ok"},{"value":"text2","icon":"image://theme/cancel"}],
39+ "quickPreviewData": {"uri": "/some/file", "duration": "14"}
3940 }'
4041
4142 property var cardsModel: [
@@ -106,6 +107,11 @@ Rectangle {
106107 "layout": { "template": { "card-layout": "horizontal" },
107108 "components": JSON.parse(Helpers.fullMapping) }
108109 },
110+ {
111+ "name": "Art, title, subtitle - Audio",
112+ "layout": { "template": { "card-layout": "horizontal", "quick-preview-type": "audio" },
113+ "components": { "art": "art", "title": "title", "subtitle": "subtitle", "quickPreviewData": "quickPreviewData" } }
114+ },
109115 ]
110116
111117 CardTool {
@@ -129,6 +135,7 @@ Rectangle {
129135 item.width = Qt.binding(function() { return cardTool.cardWidth || item.implicitWidth; });
130136 item.height = Qt.binding(function() { return cardTool.cardHeight || item.implicitHeight; });
131137 item.fixedHeaderHeight = Qt.binding(function() { return cardTool.headerHeight; });
138+ item.fixedArtShapeSize = Qt.binding(function() { return cardTool.artShapeSize; });
132139 }
133140 }
134141