ubuntu-touch-coreapps-reviewers team mailing list archive
-
ubuntu-touch-coreapps-reviewers team
-
Mailing list archive
-
Message #08539
[Merge] lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app
Andrew Hayzen has proposed merging lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app.
Commit message:
* Implement convergent mode with now playing and queue as a sidebar
Requested reviews:
Victor Thompson (vthompson)
Related bugs:
Bug #1253761 in Ubuntu Music App: "Implement initial conditional layout framework"
https://bugs.launchpad.net/music-app/+bug/1253761
Bug #1320252 in Ubuntu Music App: "[Tablet]Cannot Go Into Landscape"
https://bugs.launchpad.net/music-app/+bug/1320252
For more details, see:
https://code.launchpad.net/~ahayzen/music-app/convergence-tabs-with-sidebar-01/+merge/286127
* Implement convergent mode with now playing and queue as a sidebar
Known Issues:
* When the app is starting and is wide enough for wideAspect, a black box appears where the async loader ends up (this happens for all apps seems to be they are launched in portrait and resized too late) [bug 1548096]
--
Your team Music App Developers is subscribed to branch lp:music-app.
=== modified file 'app/components/BlurredBackground.qml'
--- app/components/BlurredBackground.qml 2016-01-16 01:30:31 +0000
+++ app/components/BlurredBackground.qml 2016-03-04 03:23:11 +0000
@@ -26,13 +26,14 @@
width: parent.width
property string art
+ property string color: "black"
// dark layer
Rectangle {
anchors {
fill: parent
}
- color: "black"
+ color: parent.color
}
// the album art
=== modified file 'app/components/Flickables/MusicGridView.qml'
--- app/components/Flickables/MusicGridView.qml 2016-01-12 00:30:08 +0000
+++ app/components/Flickables/MusicGridView.qml 2016-03-04 03:23:11 +0000
@@ -25,16 +25,6 @@
fill: parent
leftMargin: units.gu(1)
rightMargin: units.gu(1)
- // FIXME: workaround until pad.lv/1531016 (gridview juddery) is fixed
- // due to anchors.fill: parent not being used when the header is locked
- // an extra margin is needed
- topMargin: {
- if (parent.head.locked) {
- units.gu(6.125) * 2 + units.gu(2) // FIXME: 6.125 is header.height
- } else {
- units.gu(6.125) + units.gu(2) // FIXME: 6.125 is header.height
- }
- }
}
cellHeight: cellSize + heightOffset
cellWidth: cellSize + widthOffset
=== modified file 'app/components/HeadState/MultiSelectHeadState.qml'
--- app/components/HeadState/MultiSelectHeadState.qml 2016-01-16 01:30:31 +0000
+++ app/components/HeadState/MultiSelectHeadState.qml 2016-03-04 03:23:11 +0000
@@ -20,89 +20,101 @@
import Ubuntu.Components 1.3
import "../Flickables"
-PageHeadState {
- id: selectionState
- actions: [
- Action {
- iconName: "select"
- text: i18n.tr("Select All")
- onTriggered: {
- if (listview.getSelectedIndices().length === listview.model.count) {
- listview.clearSelection()
- } else {
- listview.selectAll()
- }
- }
- },
- Action {
- enabled: listview !== null ? listview.getSelectedIndices().length > 0 : false
- iconName: "add-to-playlist"
- text: i18n.tr("Add to playlist")
- onTriggered: {
- var items = []
- var indicies = listview.getSelectedIndices();
-
- for (var i=0; i < indicies.length; i++) {
- items.push(makeDict(listview.model.get(indicies[i], listview.model.RoleModelData)));
- }
-
- mainPageStack.push(Qt.resolvedUrl("../../ui/AddToPlaylist.qml"),
- {"chosenElements": items})
-
- listview.closeSelection()
- }
- },
- Action {
- enabled: listview !== null ? listview.getSelectedIndices().length > 0 : false
- iconName: "add"
- text: i18n.tr("Add to queue")
- visible: addToQueue
-
- onTriggered: {
- var items = [];
- var indicies = listview.getSelectedIndices();
-
- for (var i=0; i < indicies.length; i++) {
- items.push(Qt.resolvedUrl(listview.model.get(indicies[i], listview.model.RoleModelData).filename));
- }
-
- player.mediaPlayer.playlist.addItems(items);
-
- listview.closeSelection()
- }
- },
- Action {
- enabled: listview !== null ? listview.getSelectedIndices().length > 0 : false
- iconName: "delete"
- text: i18n.tr("Delete")
- visible: removable
-
- onTriggered: {
- removed(listview.getSelectedIndices())
-
- listview.closeSelection()
- }
- }
-
- ]
- backAction: Action {
- text: i18n.tr("Cancel selection")
- iconName: "back"
- onTriggered: listview.closeSelection()
- }
- head: thisPage.head
+State {
name: "selection"
- PropertyChanges {
- target: thisPage.head
- backAction: selectionState.backAction
- actions: selectionState.actions
- }
-
property bool addToQueue: true
property MultiSelectListView listview
property bool removable: false
- property Page thisPage
+ property PageHeader thisHeader: PageHeader {
+ id: selectionState
+ flickable: thisPage.flickable
+ leadingActionBar {
+ actions: [
+ Action {
+ text: i18n.tr("Cancel selection")
+ iconName: "back"
+ onTriggered: listview.closeSelection()
+ }
+ ]
+ }
+ title: thisPage.title
+ trailingActionBar {
+ actions: [
+ Action {
+ iconName: "select"
+ text: i18n.tr("Select All")
+ onTriggered: {
+ if (listview.getSelectedIndices().length === listview.model.count) {
+ listview.clearSelection()
+ } else {
+ listview.selectAll()
+ }
+ }
+ },
+ Action {
+ enabled: listview !== null ? listview.getSelectedIndices().length > 0 : false
+ iconName: "add-to-playlist"
+ text: i18n.tr("Add to playlist")
+ onTriggered: {
+ var items = []
+ var indicies = listview.getSelectedIndices();
+
+ for (var i=0; i < indicies.length; i++) {
+ items.push(makeDict(listview.model.get(indicies[i], listview.model.RoleModelData)));
+ }
+
+ mainPageStack.push(Qt.resolvedUrl("../../ui/AddToPlaylist.qml"),
+ {"chosenElements": items})
+
+ listview.closeSelection()
+ }
+ },
+ Action {
+ enabled: listview !== null ? listview.getSelectedIndices().length > 0 : false
+ iconName: "add"
+ text: i18n.tr("Add to queue")
+ visible: addToQueue
+
+ onTriggered: {
+ var items = [];
+ var indicies = listview.getSelectedIndices();
+
+ for (var i=0; i < indicies.length; i++) {
+ items.push(Qt.resolvedUrl(listview.model.get(indicies[i], listview.model.RoleModelData).filename));
+ }
+
+ player.mediaPlayer.playlist.addItems(items);
+
+ listview.closeSelection()
+ }
+ },
+ Action {
+ enabled: listview !== null ? listview.getSelectedIndices().length > 0 : false
+ iconName: "delete"
+ text: i18n.tr("Delete")
+ visible: removable
+
+ onTriggered: {
+ removed(listview.getSelectedIndices())
+
+ listview.closeSelection()
+ }
+ }
+ ]
+ }
+ visible: thisPage.state === "selection"
+
+ StyleHints {
+ backgroundColor: mainView.headerColor
+ }
+ }
+ property Item thisPage
signal removed(var selectedIndices)
+
+ PropertyChanges {
+ target: thisPage
+ header: thisHeader
+ }
}
=== modified file 'app/components/HeadState/PlaylistsHeadState.qml'
--- app/components/HeadState/PlaylistsHeadState.qml 2015-10-23 03:08:43 +0000
+++ app/components/HeadState/PlaylistsHeadState.qml 2016-03-04 03:23:11 +0000
@@ -20,28 +20,61 @@
import Ubuntu.Components 1.3
import Ubuntu.Components.Popups 1.3
-
-PageHeadState {
+State {
name: "default"
- head: thisPage.head
- actions: [
- Action {
- id: newPlaylistAction
- objectName: "newPlaylistButton"
- iconName: "add"
- onTriggered: {
- customdebug("New playlist.")
- thisPage.currentDialog = PopupUtils.open(Qt.resolvedUrl("../Dialog/NewPlaylistDialog.qml"), mainView)
- }
- },
- Action {
- id: searchAction
- iconName: "search"
- onTriggered: thisPage.state = "search"
- }
- ]
property alias newPlaylistEnabled: newPlaylistAction.enabled
property alias searchEnabled: searchAction.enabled
- property Page thisPage
+ property PageHeader thisHeader: PageHeader {
+ id: headerState
+ flickable: thisPage.flickable
+ leadingActionBar {
+ actions: {
+ if (mainPageStack.currentPage === tabs) {
+ tabs.tabActions
+ } else if (mainPageStack.depth > 1) {
+ backActionComponent
+ }
+ }
+ }
+ title: thisPage.title
+ trailingActionBar {
+ actions: [
+ Action {
+ id: newPlaylistAction
+ objectName: "newPlaylistButton"
+ iconName: "add"
+ onTriggered: {
+ customdebug("New playlist.")
+ thisPage.currentDialog = PopupUtils.open(Qt.resolvedUrl("../Dialog/NewPlaylistDialog.qml"), mainView)
+ }
+ },
+ Action {
+ id: searchAction
+ iconName: "search"
+ onTriggered: {
+ thisPage.state = "search";
+ thisPage.header.contents.forceActiveFocus();
+ }
+ }
+ ]
+ }
+ visible: thisPage.state === "default"
+
+ Action {
+ id: backActionComponent
+ iconName: "back"
+ onTriggered: mainPageStack.pop()
+ }
+
+ StyleHints {
+ backgroundColor: mainView.headerColor
+ }
+ }
+ property Item thisPage
+
+ PropertyChanges {
+ target: thisPage
+ header: thisHeader
+ }
}
=== modified file 'app/components/HeadState/SearchHeadState.qml'
--- app/components/HeadState/SearchHeadState.qml 2015-11-17 01:59:32 +0000
+++ app/components/HeadState/SearchHeadState.qml 2016-03-04 03:23:11 +0000
@@ -19,61 +19,78 @@
import QtQuick 2.4
import Ubuntu.Components 1.3
-PageHeadState {
- id: headerState
+State {
name: "search"
- head: thisPage.head
- backAction: Action {
- id: leaveSearchAction
- text: "back"
- iconName: "back"
- onTriggered: thisPage.state = "default"
- }
- contents: TextField {
- id: searchField
- anchors {
- left: parent ? parent.left : undefined
- right: parent ? parent.right : undefined
- rightMargin: units.gu(2)
- }
- color: styleMusic.common.black
- hasClearButton: true
- inputMethodHints: Qt.ImhNoPredictiveText
- placeholderText: i18n.tr("Search music")
+
+ property PageHeader thisHeader: PageHeader {
+ id: headerState
+ contents: TextField {
+ id: searchField
+ anchors {
+ left: parent ? parent.left : undefined
+ right: parent ? parent.right : undefined
+ verticalCenter: parent ? parent.verticalCenter : undefined
+ }
+ color: styleMusic.common.black
+ focus: true
+ hasClearButton: true
+ inputMethodHints: Qt.ImhNoPredictiveText
+ placeholderText: i18n.tr("Search music")
+
+ // Use the page onVisible as the text field goes visible=false when switching states
+ // This is used when popping from the pageStack and returning back to a page with search
+ Connections {
+ target: thisPage
+
+ onStateChanged: { // ensure the search is reset (eg pressing Esc)
+ if (state === "default") {
+ searchField.text = ""
+ }
+
+ // FIXME: Workaround for pad.lv/1514143 (keyboard show/hide on view moving)
+ // by locking the header and forcing a topMargin of page to the header height
+ thisPage.head.locked = state === headerState.name;
+ //thisPage.anchors.topMargin = state === headerState.name ? units.gu(6.125) : 0 // FIXME: 6.125 is header.height
+ }
+
+ onVisibleChanged: {
+ // clear when the page becomes visible not invisible
+ // if invisible is used the delegates can be destroyed which
+ // have created the pushed component
+ if (visible) {
+ thisPage.state = "default"
+ }
+ }
+ }
+ }
+ flickable: thisPage.flickable
+ leadingActionBar {
+ actions: [
+ Action {
+ id: leaveSearchAction
+ text: "back"
+ iconName: "back"
+ onTriggered: thisPage.state = "default"
+ }
+ ]
+ }
+ visible: thisPage.state === "search"
onVisibleChanged: {
if (visible) {
- forceActiveFocus()
+ searchField.forceActiveFocus()
}
}
- // Use the page onVisible as the text field goes visible=false when switching states
- // This is used when popping from the pageStack and returning back to a page with search
- Connections {
- target: thisPage
-
- onStateChanged: { // ensure the search is reset (eg pressing Esc)
- if (state === "default") {
- searchField.text = ""
- }
-
- // FIXME: Workaround for pad.lv/1514143 (keyboard show/hide on view moving)
- // by locking the header and forcing a topMargin of page to the header height
- headerState.head.locked = state === headerState.name;
- thisPage.anchors.topMargin = state === headerState.name ? units.gu(6.125) : 0 // FIXME: 6.125 is header.height
- }
-
- onVisibleChanged: {
- // clear when the page becomes visible not invisible
- // if invisible is used the delegates can be destroyed which
- // have created the pushed component
- if (visible) {
- thisPage.state = "default"
- }
- }
+ StyleHints {
+ backgroundColor: mainView.headerColor
}
}
-
- property Page thisPage
+ property Item thisPage
property alias query: searchField.text
+
+ PropertyChanges {
+ target: thisPage
+ header: thisHeader
+ }
}
=== modified file 'app/components/HeadState/SearchableHeadState.qml'
--- app/components/HeadState/SearchableHeadState.qml 2015-08-12 23:36:44 +0000
+++ app/components/HeadState/SearchableHeadState.qml 2016-03-04 03:23:11 +0000
@@ -20,15 +20,51 @@
import Ubuntu.Components 1.3
-PageHeadState {
+State {
name: "default"
- head: thisPage.head
- actions: Action {
- id: searchAction
- iconName: "search"
- onTriggered: thisPage.state = "search"
- }
property alias searchEnabled: searchAction.enabled
- property Page thisPage
+ property PageHeader thisHeader: PageHeader {
+ id: headerState
+ flickable: thisPage.flickable
+ leadingActionBar {
+ actions: {
+ if (mainPageStack.currentPage === tabs) {
+ tabs.tabActions
+ } else if (mainPageStack.depth > 1) {
+ backActionComponent
+ }
+ }
+ }
+ title: thisPage.title
+ trailingActionBar {
+ actions: [
+ Action {
+ id: searchAction
+ iconName: "search"
+ onTriggered: {
+ thisPage.state = "search";
+ thisPage.header.contents.forceActiveFocus();
+ }
+ }
+ ]
+ }
+ visible: thisPage.state === "default"
+
+ Action {
+ id: backActionComponent
+ iconName: "back"
+ onTriggered: mainPageStack.pop()
+ }
+
+ StyleHints {
+ backgroundColor: mainView.headerColor
+ }
+ }
+ property Item thisPage
+
+ PropertyChanges {
+ target: thisPage
+ header: thisHeader
+ }
}
=== modified file 'app/components/MusicPage.qml'
--- app/components/MusicPage.qml 2015-10-23 03:08:43 +0000
+++ app/components/MusicPage.qml 2016-03-04 03:23:11 +0000
@@ -29,6 +29,35 @@
bottomMargin: musicToolbar.visible ? musicToolbar.height : 0
fill: parent
}
+ head { // hide default header
+ locked: true
+ visible: false
+ }
+ header: PageHeader {
+ id: pageHeader
+ title: thisPage.title
+ leadingActionBar {
+ actions: {
+ if (mainPageStack.currentPage === tabs) {
+ tabs.tabActions
+ } else if (mainPageStack.depth > 1) {
+ backActionComponent
+ } else {
+ null
+ }
+ }
+ }
+
+ StyleHints {
+ backgroundColor: mainView.headerColor
+ }
+
+ Action {
+ id: backActionComponent
+ iconName: "back"
+ onTriggered: mainPageStack.pop()
+ }
+ }
property Dialog currentDialog
property bool searchable: false
=== modified file 'app/components/NowPlayingFullView.qml'
--- app/components/NowPlayingFullView.qml 2016-01-16 01:30:31 +0000
+++ app/components/NowPlayingFullView.qml 2016-03-04 03:23:11 +0000
@@ -29,6 +29,9 @@
fill: parent
}
+ property string backgroundColor: styleMusic.common.black
+ property bool sidebar: false
+
BlurredBackground {
id: blurredBackground
anchors {
@@ -37,7 +40,8 @@
top: parent.top
}
art: albumImage.firstSource
- height: parent.height - units.gu(7)
+ color: backgroundColor
+ height: parent.height - (sidebar ? units.gu(7) + nowPlayingWideAspectLabelsBackground.height : units.gu(7))
Item {
id: albumImageContainer
@@ -56,70 +60,6 @@
}
}
- Rectangle {
- id: nowPlayingWideAspectLabelsBackground
- anchors.bottom: parent.bottom
- color: styleMusic.common.black
- height: nowPlayingWideAspectTitle.lineCount === 1 ? units.gu(10) : units.gu(13)
- opacity: 0.8
- width: parent.width
- }
-
- /* Column for labels in wideAspect */
- Column {
- id: nowPlayingWideAspectLabels
- spacing: units.gu(1)
- anchors {
- left: parent.left
- leftMargin: units.gu(2)
- right: parent.right
- rightMargin: units.gu(2)
- top: nowPlayingWideAspectLabelsBackground.top
- topMargin: nowPlayingWideAspectTitle.lineCount === 1 ? units.gu(2) : units.gu(1.5)
- }
-
- /* Title of track */
- Label {
- id: nowPlayingWideAspectTitle
- anchors {
- left: parent.left
- leftMargin: units.gu(1)
- right: parent.right
- rightMargin: units.gu(1)
- }
- color: styleMusic.playerControls.labelColor
- elide: Text.ElideRight
- fontSize: "x-large"
- maximumLineCount: 2
- objectName: "playercontroltitle"
- text: {
- if (player.mediaPlayer.playlist.empty) {
- ""
- } else if (player.currentMeta.title === "") {
- player.mediaPlayer.playlist.currentSource
- } else {
- player.currentMeta.title
- }
- }
- wrapMode: Text.WordWrap
- }
-
- /* Artist of track */
- Label {
- id: nowPlayingWideAspectArtist
- anchors {
- left: parent.left
- leftMargin: units.gu(1)
- right: parent.right
- rightMargin: units.gu(1)
- }
- color: styleMusic.nowPlaying.labelSecondaryColor
- elide: Text.ElideRight
- fontSize: "small"
- text: player.mediaPlayer.playlist.empty ? "" : player.currentMeta.author
- }
- }
-
/* Detect cover art swipe */
MouseArea {
anchors.fill: parent
@@ -142,6 +82,74 @@
}
}
+ Rectangle {
+ id: nowPlayingWideAspectLabelsBackground
+ anchors {
+ bottom: sidebar ? undefined : blurredBackground.bottom
+ left: parent.left
+ right: parent.right
+ top: sidebar ? blurredBackground.bottom : undefined
+ }
+ color: backgroundColor
+ height: nowPlayingWideAspectTitle.lineCount === 1 ? units.gu(10) : units.gu(13)
+ opacity: sidebar ? 1.0 : 0.8
+ }
+
+ /* Column for labels in wideAspect */
+ Column {
+ id: nowPlayingWideAspectLabels
+ spacing: units.gu(1)
+ anchors {
+ left: parent.left
+ leftMargin: units.gu(2)
+ right: parent.right
+ rightMargin: units.gu(2)
+ top: nowPlayingWideAspectLabelsBackground.top
+ topMargin: nowPlayingWideAspectTitle.lineCount === 1 ? units.gu(2) : units.gu(1.5)
+ }
+
+ /* Title of track */
+ Label {
+ id: nowPlayingWideAspectTitle
+ anchors {
+ left: parent.left
+ leftMargin: units.gu(1)
+ right: parent.right
+ rightMargin: units.gu(1)
+ }
+ color: styleMusic.playerControls.labelColor
+ elide: Text.ElideRight
+ fontSize: "x-large"
+ maximumLineCount: 2
+ objectName: "playercontroltitle"
+ text: {
+ if (player.mediaPlayer.playlist.empty) {
+ ""
+ } else if (player.currentMeta.title === "") {
+ player.mediaPlayer.playlist.currentSource
+ } else {
+ player.currentMeta.title
+ }
+ }
+ wrapMode: Text.WordWrap
+ }
+
+ /* Artist of track */
+ Label {
+ id: nowPlayingWideAspectArtist
+ anchors {
+ left: parent.left
+ leftMargin: units.gu(1)
+ right: parent.right
+ rightMargin: units.gu(1)
+ }
+ color: styleMusic.nowPlaying.labelSecondaryColor
+ elide: Text.ElideRight
+ fontSize: "small"
+ text: player.mediaPlayer.playlist.empty ? "" : player.currentMeta.author
+ }
+ }
+
/* Background for progress bar component */
Rectangle {
id: musicToolbarFullProgressBackground
@@ -149,9 +157,9 @@
bottom: parent.bottom
left: parent.left
right: parent.right
- top: blurredBackground.bottom
+ top: sidebar ? nowPlayingWideAspectLabelsBackground.bottom : blurredBackground.bottom
}
- color: styleMusic.common.black
+ color: backgroundColor
}
/* Progress bar component */
@@ -161,7 +169,7 @@
anchors.leftMargin: units.gu(3)
anchors.right: parent.right
anchors.rightMargin: units.gu(3)
- anchors.top: blurredBackground.bottom
+ anchors.top: sidebar ? nowPlayingWideAspectLabelsBackground.bottom : blurredBackground.bottom
anchors.topMargin: units.gu(1)
height: units.gu(3)
width: parent.width
=== added file 'app/components/NowPlayingSidebar.qml'
--- app/components/NowPlayingSidebar.qml 1970-01-01 00:00:00 +0000
+++ app/components/NowPlayingSidebar.qml 2016-03-04 03:23:11 +0000
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2013, 2014, 2015, 2016
+ * Andrew Hayzen <ahayzen@xxxxxxxxx>
+ * Daniel Holm <d.holmen@xxxxxxxxx>
+ * Victor Thompson <victor.thompson@xxxxxxxxx>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import QtQuick 2.4
+import Ubuntu.Components 1.3
+
+import "HeadState"
+
+Rectangle {
+ id: nowPlayingSidebar
+ anchors {
+ fill: parent
+ }
+ color: "#2c2c34"
+ state: queue.state === "multiselectable" ? "selection" : "default"
+ states: [
+ QueueHeadState {
+ thisHeader {
+ leadingActionBar {
+ actions: [] // hide tab bar
+ }
+ z: 100 // put on top of content
+ }
+ thisPage: nowPlayingSidebar
+ },
+ MultiSelectHeadState {
+ addToQueue: false
+ listview: queue
+ removable: true
+ thisHeader {
+ z: 100 // put on top of content
+ }
+ thisPage: nowPlayingSidebar
+
+ onRemoved: {
+ // Remove the tracks from the queue
+ // Use slice() to copy the list
+ // so that the indexes don't change as they are removed
+ player.mediaPlayer.playlist.removeItemsWrapper(selectedIndices.slice());
+ }
+ }
+ ]
+ property alias flickable: queue // fake normal Page
+ property Item header: PageHeader {
+ id: pageHeader
+ leadingActionBar {
+ actions: nowPlayingSidebar.head.backAction
+ }
+ flickable: queue
+ trailingActionBar {
+ actions: nowPlayingSidebar.head.actions
+ }
+ z: 100 // put on top of content
+
+ StyleHints {
+ backgroundColor: mainView.headerColor
+ }
+ }
+ property Item previousHeader: null
+ property string title: "" // fake normal Page
+
+ onHeaderChanged: { // Copy what SDK does to parent header correctly
+ if (previousHeader) {
+ previousHeader.parent = null
+ }
+
+ header.parent = nowPlayingSidebar
+ previousHeader = header;
+ }
+
+ Loader {
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: parent.top
+ }
+ height: units.gu(6.125)
+ sourceComponent: header
+ }
+
+ Queue {
+ id: queue
+ anchors {
+ bottomMargin: 0
+ topMargin: 0
+ }
+ clip: true
+ isSidebar: true
+ header: Column {
+ id: sidebarColumn
+ anchors {
+ left: parent.left
+ right: parent.right
+ }
+
+ NowPlayingFullView {
+ anchors {
+ fill: undefined
+ }
+ backgroundColor: "#2c2c34"
+ clip: true
+ height: units.gu(47)
+ sidebar: true
+ width: parent.width
+ }
+
+ NowPlayingToolbar {
+ anchors {
+ fill: undefined
+ }
+ bottomProgressHint: false
+ color: "#2c2c34"
+ height: itemSize + 2 * spacing + units.gu(2)
+ width: parent.width
+ }
+ }
+ }
+}
=== modified file 'app/components/NowPlayingToolbar.qml'
--- app/components/NowPlayingToolbar.qml 2016-01-16 01:30:31 +0000
+++ app/components/NowPlayingToolbar.qml 2016-03-04 03:23:11 +0000
@@ -30,19 +30,23 @@
}
color: styleMusic.common.black
+ property alias bottomProgressHint: playerControlsProgressBar.visible
+ property int itemSize: units.gu(6)
+ property int spacing: units.gu(1)
+
/* Repeat button */
MouseArea {
id: nowPlayingRepeatButton
anchors.right: nowPlayingPreviousButton.left
- anchors.rightMargin: units.gu(1)
+ anchors.rightMargin: spacing
anchors.verticalCenter: nowPlayingPlayButton.verticalCenter
- height: units.gu(6)
+ height: itemSize
width: height
onClicked: player.repeat = !player.repeat
Icon {
id: repeatIcon
- height: units.gu(3)
+ height: itemSize / 2
width: height
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
@@ -58,15 +62,15 @@
id: nowPlayingPreviousButton
enabled: player.mediaPlayer.playlist.canGoPrevious
anchors.right: nowPlayingPlayButton.left
- anchors.rightMargin: units.gu(1)
+ anchors.rightMargin: spacing
anchors.verticalCenter: nowPlayingPlayButton.verticalCenter
- height: units.gu(6)
+ height: itemSize
width: height
onClicked: player.mediaPlayer.playlist.previousWrapper()
Icon {
id: nowPlayingPreviousIndicator
- height: units.gu(3)
+ height: itemSize / 2
width: height
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
@@ -81,13 +85,13 @@
MouseArea {
id: nowPlayingPlayButton
anchors.centerIn: parent
- height: units.gu(10)
+ height: itemSize + 2 * spacing
width: height
onClicked: player.mediaPlayer.toggle()
Icon {
id: nowPlayingPlayIndicator
- height: units.gu(6)
+ height: itemSize
width: height
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
@@ -101,16 +105,16 @@
MouseArea {
id: nowPlayingNextButton
anchors.left: nowPlayingPlayButton.right
- anchors.leftMargin: units.gu(1)
+ anchors.leftMargin: spacing
anchors.verticalCenter: nowPlayingPlayButton.verticalCenter
enabled: player.mediaPlayer.playlist.canGoNext
- height: units.gu(6)
+ height: itemSize
width: height
onClicked: player.mediaPlayer.playlist.nextWrapper()
Icon {
id: nowPlayingNextIndicator
- height: units.gu(3)
+ height: itemSize / 2
width: height
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
@@ -125,15 +129,15 @@
MouseArea {
id: nowPlayingShuffleButton
anchors.left: nowPlayingNextButton.right
- anchors.leftMargin: units.gu(1)
+ anchors.leftMargin: spacing
anchors.verticalCenter: nowPlayingPlayButton.verticalCenter
- height: units.gu(6)
+ height: itemSize
width: height
onClicked: player.shuffle = !player.shuffle
Icon {
id: shuffleIcon
- height: units.gu(3)
+ height: itemSize / 2
width: height
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
=== modified file 'app/components/Queue.qml'
--- app/components/Queue.qml 2016-01-12 11:44:16 +0000
+++ app/components/Queue.qml 2016-03-04 03:23:11 +0000
@@ -29,7 +29,9 @@
MultiSelectListView {
id: queueList
anchors {
+ bottomMargin: units.gu(1)
fill: parent
+ topMargin: units.gu(1)
}
autoModelMove: false // ensures we use moveItem() not move() in onReorder
footer: Item {
@@ -38,11 +40,13 @@
model: player.mediaPlayer.playlist
objectName: "nowPlayingqueueList"
+ property bool isSidebar: false
+
onCountChanged: customdebug("Queue: Now has: " + queueList.count + " tracks")
delegate: MusicListItem {
id: queueListItem
- color: player.mediaPlayer.playlist.currentIndex === index ? "#2c2c34" : styleMusic.mainView.backgroundColor
+ color: player.mediaPlayer.playlist.currentIndex === index ? (isSidebar ? "#3d3d45" : "#2c2c34") : (isSidebar ? "#2c2c34" : styleMusic.mainView.backgroundColor)
height: units.gu(7)
leadingActions: ListItemActions {
actions: [
=== modified file 'app/music-app.qml'
--- app/music-app.qml 2016-01-28 01:43:54 +0000
+++ app/music-app.qml 2016-03-04 03:23:11 +0000
@@ -115,7 +115,7 @@
break;
case Qt.Key_J: // Ctrl+J Jump to playing song
tabs.pushNowPlaying()
- mainPageStack.currentPage.isListView = true
+ mainPageStack.currentPage.setListView(true)
break;
case Qt.Key_N: // Ctrl+N Show Now playing
tabs.pushNowPlaying()
@@ -282,7 +282,7 @@
signal listItemSwiping(int i)
- property bool wideAspect: width >= units.gu(70) && loadedUI
+ property bool wideAspect: width >= units.gu(95) && loadedUI
property bool loadedUI: false // property to detect if the UI has finished
// FUNCTIONS
@@ -567,23 +567,16 @@
}
}
- Loader {
- id: musicToolbar
- anchors {
- bottom: parent.bottom
- left: parent.left
- right: parent.right
- }
- asynchronous: true
- source: "components/MusicToolbar.qml"
- visible: (mainPageStack.currentPage.showToolbar || mainPageStack.currentPage.showToolbar === undefined) &&
- !firstRun &&
- !noMusic
- z: 200 // put on top of everything else
- }
-
PageStack {
id: mainPageStack
+ anchors {
+ bottom: parent.bottom
+ fill: undefined
+ left: parent.left
+ right: nowPlayingSidebarLoader.left
+ top: parent.top
+ }
+ clip: true // otherwise listitems actions overflow
// Properties storing the current page info
property Page currentMusicPage: null // currentPage can be Tabs
@@ -644,6 +637,39 @@
}
property Tab lastTab: selectedTab
+ property list<Action> tabActions: [
+ Action {
+ enabled: recentTabRepeater.count > 0
+ text: enabled ? recentTabRepeater.itemAt(0).title : ""
+ visible: enabled
+
+ onTriggered: {
+ if (enabled) {
+ tabs.selectedTabIndex = recentTabRepeater.itemAt(0).index
+ }
+ }
+ },
+ Action {
+ text: artistsTab.title
+ onTriggered: tabs.selectedTabIndex = artistsTab.index
+ },
+ Action {
+ text: albumsTab.title
+ onTriggered: tabs.selectedTabIndex = albumsTab.index
+ },
+ Action {
+ text: genresTab.title
+ onTriggered: tabs.selectedTabIndex = genresTab.index
+ },
+ Action {
+ text: songsTab.title
+ onTriggered: tabs.selectedTabIndex = songsTab.index
+ },
+ Action {
+ text: playlistsTab.title
+ onTriggered: tabs.selectedTabIndex = playlistsTab.index
+ }
+ ]
onSelectedTabChanged: {
// pause loading of the models in the old tab
@@ -844,18 +870,70 @@
function pushNowPlaying()
{
- // only push if on a different page
- if (mainPageStack.currentPage.title !== i18n.tr("Now playing")) {
- mainPageStack.push(Qt.resolvedUrl("ui/NowPlaying.qml"), {})
- }
+ if (!wideAspect) {
+ // only push if on a different page
+ if (mainPageStack.currentPage.title !== i18n.tr("Now playing")) {
+ mainPageStack.push(Qt.resolvedUrl("ui/NowPlaying.qml"), {})
+ }
- if (mainPageStack.currentPage.isListView === true) {
- mainPageStack.currentPage.isListView = false; // ensure full view
+ if (mainPageStack.currentPage.isListView === true) {
+ mainPageStack.currentPage.setListView(false); // ensure full view
+ }
}
}
} // end of tabs
}
+ //
+ // Components that are ontop of the PageStack
+ //
+
+ Loader {
+ id: nowPlayingSidebarLoader
+ active: shown || anchors.leftMargin < 0
+ anchors { // start offscreen
+ bottom: parent.bottom
+ left: parent.right
+ leftMargin: shown && status === Loader.Ready ? -width : 0
+ top: parent.top
+ }
+ asynchronous: true
+ source: "components/NowPlayingSidebar.qml"
+ visible: width > 0
+ width: units.gu(40)
+
+ property bool shown: loadedUI && wideAspect && player.mediaPlayer.playlist.itemCount > 0
+
+ Behavior on anchors.leftMargin {
+ NumberAnimation {
+
+ }
+ }
+ }
+
+ Loader {
+ id: musicToolbar
+ active: !wideAspect || anchors.topMargin < 0
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: parent.bottom
+ topMargin: !wideAspect && status === Loader.Ready ? -height : 0
+ }
+ asynchronous: true
+ source: "components/MusicToolbar.qml"
+ visible: (mainPageStack.currentPage && (mainPageStack.currentPage.showToolbar || mainPageStack.currentPage.showToolbar === undefined)) &&
+ !firstRun &&
+ !noMusic &&
+ anchors.topMargin < 0
+
+ Behavior on anchors.topMargin {
+ NumberAnimation {
+
+ }
+ }
+ }
+
LoadingSpinnerComponent {
id: loading
}
=== modified file 'app/ui/AddToPlaylist.qml'
--- app/ui/AddToPlaylist.qml 2016-02-28 01:51:17 +0000
+++ app/ui/AddToPlaylist.qml 2016-03-04 03:23:11 +0000
@@ -59,10 +59,13 @@
// FIXME: workaround for pad.lv/1531016 (gridview juddery)
anchors {
+ bottom: parent.bottom
+ left: parent.left
fill: undefined
+ top: parent.top
}
- height: mainView.height + musicToolbar.height
- width: mainView.width
+ height: mainPageStack.height + musicToolbar.height
+ width: mainPageStack.width
property var chosenElements: []
=== modified file 'app/ui/Albums.qml'
--- app/ui/Albums.qml 2016-01-12 00:30:08 +0000
+++ app/ui/Albums.qml 2016-03-04 03:23:11 +0000
@@ -46,10 +46,13 @@
// FIXME: workaround for pad.lv/1531016 (gridview juddery)
anchors {
+ bottom: parent.bottom
+ left: parent.left
fill: undefined
+ top: parent.top
}
- height: mainView.height
- width: mainView.width
+ height: mainPageStack.height
+ width: mainPageStack.width
// Hack for autopilot otherwise Albums appears as MusicPage
// due to bug 1341671 it is required that there is a property so that
=== modified file 'app/ui/ArtistView.qml'
--- app/ui/ArtistView.qml 2016-01-12 00:30:08 +0000
+++ app/ui/ArtistView.qml 2016-03-04 03:23:11 +0000
@@ -38,10 +38,13 @@
// FIXME: workaround for pad.lv/1531016 (gridview juddery)
anchors {
+ bottom: parent.bottom
+ left: parent.left
fill: undefined
+ top: parent.top
}
- height: mainView.height
- width: mainView.width
+ height: mainPageStack.height
+ width: mainPageStack.width
MusicGridView {
id: artistAlbumView
=== modified file 'app/ui/Artists.qml'
--- app/ui/Artists.qml 2016-01-30 23:58:32 +0000
+++ app/ui/Artists.qml 2016-03-04 03:23:11 +0000
@@ -50,10 +50,13 @@
// FIXME: workaround for pad.lv/1531016 (gridview juddery)
anchors {
+ bottom: parent.bottom
+ left: parent.left
fill: undefined
+ top: parent.top
}
- height: mainView.height
- width: mainView.width
+ height: mainPageStack.height
+ width: mainPageStack.width
// Hack for autopilot otherwise Artists appears as MusicPage
// due to bug 1341671 it is required that there is a property so that
=== modified file 'app/ui/Genres.qml'
--- app/ui/Genres.qml 2016-01-12 00:30:08 +0000
+++ app/ui/Genres.qml 2016-03-04 03:23:11 +0000
@@ -46,10 +46,13 @@
// FIXME: workaround for pad.lv/1531016 (gridview juddery)
anchors {
+ bottom: parent.bottom
+ left: parent.left
fill: undefined
+ top: parent.top
}
- height: mainView.height
- width: mainView.width
+ height: mainPageStack.height
+ width: mainPageStack.width
// Hack for autopilot otherwise Albums appears as MusicPage
// due to bug 1341671 it is required that there is a property so that
=== modified file 'app/ui/NowPlaying.qml'
--- app/ui/NowPlaying.qml 2016-01-16 01:30:31 +0000
+++ app/ui/NowPlaying.qml 2016-03-04 03:23:11 +0000
@@ -30,8 +30,40 @@
flickable: isListView ? queueListLoader.item : null // Ensures that the header is shown in fullview
objectName: "nowPlayingPage"
showToolbar: false
+ state: isListView && queueListLoader.item.state === "multiselectable" ? "selection" : "default"
+ states: [
+ QueueHeadState {
+ thisHeader {
+ sections {
+ model: defaultStateSections.model
+ selectedIndex: defaultStateSections.selectedIndex
+ onSelectedIndexChanged: isListView = !isListView
+ }
+ }
+ thisPage: nowPlaying
+ },
+ MultiSelectHeadState {
+ addToQueue: false
+ listview: queueListLoader.item
+ removable: true
+ thisHeader {
+ sections {
+ model: defaultStateSections.model
+ selectedIndex: defaultStateSections.selectedIndex
+ onSelectedIndexChanged: isListView = !isListView
+ }
+ }
+ thisPage: nowPlaying
+
+ onRemoved: {
+ // Remove the tracks from the queue
+ // Use slice() to copy the list
+ // so that the indexes don't change as they are removed
+ player.mediaPlayer.playlist.removeItemsWrapper(selectedIndices.slice());
+ }
+ }
+ ]
title: nowPlayingTitle
- visible: false
property bool isListView: false
// TRANSLATORS: this appears in the header with limited space (around 20 characters)
@@ -59,6 +91,26 @@
}
}
+ onVisibleChanged: {
+ if (wideAspect) {
+ popWaitTimer.start()
+ }
+ }
+
+ Timer { // FIXME: workaround for when entering wideAspect coming back from a stacked page (AddToPlaylist) and the page being deleted breaks the stacked page
+ id: popWaitTimer
+ interval: 250
+ onTriggered: mainPageStack.popPage(nowPlaying);
+ }
+
+ PageHeadSections {
+ id: defaultStateSections
+ model: [fullViewTitle, queueTitle]
+
+ // Set at startup to avoid binding loop
+ Component.onCompleted: selectedIndex = isListView ? 1 : 0
+ }
+
// Ensure that the listview has loaded before attempting to positionAt
function ensureListViewLoaded() {
if (queueListLoader.item.count === player.mediaPlayer.playlist.itemCount) {
@@ -77,82 +129,16 @@
queueListLoader.item.positionViewAtIndex(index, ListView.Center);
}
- PageHeadSections {
- id: defaultStateSections
- model: [fullViewTitle, queueTitle]
- selectedIndex: isListView
- }
-
- head {
- sections {
- model: defaultStateSections.model
- selectedIndex: defaultStateSections.selectedIndex
- onSelectedIndexChanged: isListView = !isListView
- }
- }
-
- state: isListView && queueListLoader.item.state === "multiselectable" ? "selection" : "default"
- states: [
- PageHeadState {
- id: defaultState
-
- name: "default"
- actions: [
- Action {
- enabled: !player.mediaPlayer.playlist.empty
- iconName: "add-to-playlist"
- // TRANSLATORS: this action appears in the overflow drawer with limited space (around 18 characters)
- text: i18n.tr("Add to playlist")
- visible: !isListView
-
- onTriggered: {
- var items = []
-
- items.push(makeDict(player.metaForSource(player.mediaPlayer.playlist.currentItemSource)));
-
- mainPageStack.push(Qt.resolvedUrl("AddToPlaylist.qml"),
- {"chosenElements": items})
- }
- },
- Action {
- enabled: !player.mediaPlayer.playlist.empty
- iconName: "delete"
- objectName: "clearQueue"
- // TRANSLATORS: this action appears in the overflow drawer with limited space (around 18 characters)
- text: i18n.tr("Clear queue")
- visible: isListView
-
- onTriggered: player.mediaPlayer.playlist.clearWrapper()
- }
- ]
- PropertyChanges {
- target: nowPlaying.head
- backAction: defaultState.backAction
- actions: defaultState.actions
- }
- },
- MultiSelectHeadState {
- addToQueue: false
- listview: queueListLoader.item
- removable: true
- thisPage: nowPlaying
-
- onRemoved: {
- // Remove the tracks from the queue
- // Use slice() to copy the list
- // so that the indexes don't change as they are removed
- player.mediaPlayer.playlist.removeItemsWrapper(selectedIndices.slice());
- }
- }
- ]
+ function setListView(listView) {
+ defaultStateSections.selectedIndex = listView ? 1 : 0;
+ }
Loader {
anchors {
bottom: nowPlayingToolbarLoader.top
left: parent.left
right: parent.right
- top: parent.top
- topMargin: headerHeight
+ top: nowPlaying.header.bottom
}
property real headerHeight: units.gu(10.125) // FIXME: 10.125 is the header.height with the page sections
@@ -165,11 +151,9 @@
id: queueListLoader
anchors {
bottom: nowPlayingToolbarLoader.top
- bottomMargin: units.gu(2)
left: parent.left
right: parent.right
- top: parent.top
- topMargin: units.gu(2)
+ top: parent.top // Don't use header.bottom otherwise flickery
}
asynchronous: true
source: "../components/Queue.qml"
@@ -188,6 +172,16 @@
}
Connections {
+ target: mainView
+ onWideAspectChanged: {
+ // Do not pop if not visible (eg on AddToPlaylist)
+ if (wideAspect && nowPlaying.visible) {
+ mainPageStack.popPage(nowPlaying);
+ }
+ }
+ }
+
+ Connections {
target: player.mediaPlayer.playlist
onEmptyChanged: {
if (player.mediaPlayer.playlist.empty) {
=== modified file 'app/ui/Playlists.qml'
--- app/ui/Playlists.qml 2016-01-29 02:20:55 +0000
+++ app/ui/Playlists.qml 2016-03-04 03:23:11 +0000
@@ -51,10 +51,13 @@
// FIXME: workaround for pad.lv/1531016 (gridview juddery)
anchors {
+ bottom: parent.bottom
+ left: parent.left
fill: undefined
+ top: parent.top
}
- height: mainView.height
- width: mainView.width
+ height: mainPageStack.height
+ width: mainPageStack.width
property bool changed: false
property bool childrenChanged: false
=== modified file 'app/ui/Recent.qml'
--- app/ui/Recent.qml 2016-01-12 01:06:16 +0000
+++ app/ui/Recent.qml 2016-03-04 03:23:11 +0000
@@ -31,14 +31,52 @@
MusicPage {
id: recentPage
objectName: "recentPage"
+ header: PageHeader {
+ flickable: recentPage.flickable
+ leadingActionBar {
+ actions: {
+ if (mainPageStack.currentPage === tabs) {
+ tabs.tabActions
+ } else if (mainPageStack.depth > 1) {
+ backActionComponent
+ }
+ }
+ }
+ title: recentPage.title
+ trailingActionBar {
+ actions: [
+ Action {
+ enabled: recentModel.model.count > 0
+ iconName: "delete"
+ onTriggered: {
+ Library.clearRecentHistory()
+ recentModel.filterRecent()
+ }
+ }
+ ]
+ }
+
+ Action {
+ id: backActionComponent
+ iconName: "back"
+ onTriggered: mainPageStack.pop()
+ }
+
+ StyleHints {
+ backgroundColor: mainView.headerColor
+ }
+ }
title: i18n.tr("Recent")
// FIXME: workaround for pad.lv/1531016 (gridview juddery)
anchors {
+ bottom: parent.bottom
+ left: parent.left
fill: undefined
+ top: parent.top
}
- height: mainView.height
- width: mainView.width
+ height: mainPageStack.height
+ width: mainPageStack.width
property bool changed: false
property bool childrenChanged: false
@@ -56,19 +94,6 @@
onTriggered: recentModel.filterRecent()
}
- head {
- actions: [
- Action {
- enabled: recentModel.model.count > 0
- iconName: "delete"
- onTriggered: {
- Library.clearRecentHistory()
- recentModel.filterRecent()
- }
- }
- ]
- }
-
MusicGridView {
id: recentGridView
itemWidth: units.gu(15)
=== modified file 'debian/changelog'
--- debian/changelog 2016-02-28 01:51:17 +0000
+++ debian/changelog 2016-03-04 03:23:11 +0000
@@ -11,6 +11,7 @@
[ Andrew Hayzen ]
* Fix so that a press and hold cannot disable selection in the ContentHubExport.qml (LP: #1538838)
+ * Implement convergent mode with now playing and queue as a sidebar (LP: #1253761)
-- Victor <victor@victor-virtual-machine> Wed, 27 Jan 2016 19:40:55 -0600
Follow ups
-
[Merge] lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app
From: noreply, 2016-04-06
-
[Merge] lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app
From: Victor Thompson, 2016-04-06
-
Re: [Merge] lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app
From: Victor Thompson, 2016-04-06
-
Re: [Merge] lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app
From: Jenkins Bot, 2016-04-06
-
Re: [Merge] lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app
From: Victor Thompson, 2016-04-06
-
Re: [Merge] lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app
From: Victor Thompson, 2016-04-06
-
Re: [Merge] lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app
From: Jenkins Bot, 2016-03-28
-
Re: [Merge] lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app
From: Andrew Hayzen, 2016-03-28
-
Re: [Merge] lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app
From: Jenkins Bot, 2016-03-28
-
Re: [Merge] lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app
From: Jenkins Bot, 2016-03-28
-
Re: [Merge] lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app
From: Andrew Hayzen, 2016-03-28
-
Re: [Merge] lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app
From: Victor Thompson, 2016-03-24
-
Re: [Merge] lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app
From: Victor Thompson, 2016-03-19
-
Re: [Merge] lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app
From: Jenkins Bot, 2016-03-07
-
Re: [Merge] lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app
From: Andrew Hayzen, 2016-03-07
-
Re: [Merge] lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app
From: Victor Thompson, 2016-03-05
-
Re: [Merge] lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app
From: Victor Thompson, 2016-03-05
-
Re: [Merge] lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app
From: Jenkins Bot, 2016-03-04
-
Re: [Merge] lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app
From: Jenkins Bot, 2016-03-04
-
Re: [Merge] lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app
From: Jenkins Bot, 2016-03-04
-
Re: [Merge] lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app
From: Jenkins Bot, 2016-03-04