Merge lp:~nik90/podbird/devel-branch-sync-2 into lp:podbird

Proposed by Nekhelesh Ramananthan
Status: Merged
Approved by: Michael Sheldon
Approved revision: 192
Merged at revision: 152
Proposed branch: lp:~nik90/podbird/devel-branch-sync-2
Merge into: lp:podbird
Prerequisite: lp:~nik90/podbird/devel-branch-sync-1
Diff against target: 2860 lines (+1178/-1166)
15 files modified
app/components/Card.qml (+62/-117)
app/components/CardView.qml (+15/-20)
app/components/ColumnFlow.qml (+0/-494)
app/components/CustomSectionHeader.qml (+47/-0)
app/components/TabsList.qml (+0/-52)
app/podbird.qml (+148/-77)
app/podcasts.js (+102/-2)
app/settings/About.qml (+0/-1)
app/ui/EpisodesPage.qml (+21/-19)
app/ui/EpisodesTab.qml (+275/-100)
app/ui/NowPlayingPage.qml (+329/-224)
app/ui/PlayerControls.qml (+6/-6)
app/ui/PodcastsTab.qml (+4/-3)
app/ui/Queue.qml (+88/-0)
po/com.mikeasoft.podbird.pot (+81/-51)
To merge this branch: bzr merge lp:~nik90/podbird/devel-branch-sync-2
Reviewer Review Type Date Requested Status
Michael Sheldon Approve
Review via email: mp+289564@code.launchpad.net

Description of the change

This is a pretty heavy MP. It adds the following,
- Queue Support
- Migration to DownloadManager
- New GridView
- Multi-select view for the EpisodesTab alone (others will follow in the next MP)

To post a comment you must log in.
192. By Nekhelesh Ramananthan

merged prerequisite

Revision history for this message
Michael Sheldon (michael-sheldon) wrote :

Looks good

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'app/components/Card.qml'
2--- app/components/Card.qml 2016-02-25 11:09:44 +0000
3+++ app/components/Card.qml 2016-03-28 22:39:33 +0000
4@@ -1,12 +1,13 @@
5 /*
6- * Copyright (C) 2014-2016
7- * Andrew Hayzen <ahayzen@gmail.com>
8- *
9- * This program is free software; you can redistribute it and/or modify
10+ * Copyright 2016 Podbird Team
11+ *
12+ * This file is part of Podbird.
13+ *
14+ * Podbird is free software; you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation; version 3.
17 *
18- * This program is distributed in the hope that it will be useful,
19+ * Podbird is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23@@ -18,122 +19,66 @@
24 import QtQuick 2.4
25 import Ubuntu.Components 1.3
26
27-Item {
28+AbstractButton {
29 id: card
30
31- /* Required by ColumnFlow */
32- property int index
33- property var model
34+ height: parent.parent.cellHeight
35+ width: parent.parent.cellWidth
36
37 property alias coverArt: imgFrame.source
38 property alias primaryText: primaryLabel.text
39- property alias secondaryText: secondaryLabel.text
40- property alias secondaryTextVisible: secondaryLabel.visible
41-
42- signal clicked(var mouse)
43- signal pressAndHold(var mouse)
44-
45- height: cardColumn.childrenRect.height + 2 * bg.anchors.margins
46-
47- /* Background for card */
48- Rectangle {
49- id: bg
50- anchors.fill: parent
51- anchors.margins: units.gu(1)
52- color: podbird.appTheme.hightlightListView
53- }
54-
55- /* Column containing image and labels */
56- Column {
57- id: cardColumn
58-
59- anchors.fill: bg
60- spacing: units.gu(0.5)
61-
62- Image {
63- id: imgFrame
64- width: parent.width
65- height: width
66- sourceSize.height: width
67- sourceSize.width: width
68- }
69-
70- Item {
71- height: units.gu(1)
72- width: units.gu(1)
73- }
74-
75- Label {
76- id: primaryLabel
77- anchors {
78- left: parent.left
79- right: parent.right
80- margins: units.gu(1)
81- }
82- color: podbird.appTheme.baseText
83- elide: Text.ElideRight
84- textSize: Label.Small
85- opacity: 1.0
86- wrapMode: Text.WordWrap
87- horizontalAlignment: Text.AlignHCenter
88- }
89-
90- Label {
91- id: secondaryLabel
92- anchors {
93- left: parent.left
94- leftMargin: units.gu(1)
95- right: parent.right
96- rightMargin: units.gu(1)
97- }
98- color: podbird.appTheme.baseSubText
99- elide: Text.ElideRight
100- textSize: Label.Small
101- opacity: 1.0
102- wrapMode: Text.WordWrap
103- horizontalAlignment: Text.AlignHCenter
104- }
105-
106- Item {
107- height: units.gu(1.5)
108- width: units.gu(1)
109- }
110- }
111-
112- /* Overlay for when card is pressed */
113- Rectangle {
114- id: overlay
115- anchors.fill: bg
116- color: "#000"
117- opacity: 0
118-
119- Behavior on opacity {
120- UbuntuNumberAnimation {}
121- }
122- }
123-
124- /* Capture mouse events */
125- MouseArea {
126- anchors.fill: parent
127- onClicked: card.clicked(mouse)
128- onPressAndHold: card.pressAndHold(mouse)
129- onPressedChanged: overlay.opacity = pressed ? 0.3 : 0
130- }
131-
132- /* Animations */
133- Behavior on height {
134- UbuntuNumberAnimation {}
135- }
136-
137- Behavior on width {
138- UbuntuNumberAnimation {}
139- }
140-
141- Behavior on x {
142- UbuntuNumberAnimation {}
143- }
144-
145- Behavior on y {
146- UbuntuNumberAnimation {}
147+ property string secondaryText: ""
148+
149+ Image {
150+ id: imgFrame
151+ width: parent.width/1.2
152+ height: width
153+ anchors.top: parent.top
154+ anchors.horizontalCenter: parent.horizontalCenter
155+ sourceSize.height: width
156+ sourceSize.width: width
157+
158+ Loader {
159+ id: hintLoader
160+ anchors.verticalCenter: parent.top
161+ anchors.right: parent.right
162+ anchors.rightMargin: units.gu(-0.5)
163+ sourceComponent: secondaryText !== "" ? hintComponent : undefined
164+ }
165+
166+ Component {
167+ id: hintComponent
168+ Rectangle {
169+ color: podbird.appTheme.focusText
170+ width: secondaryLabel.implicitWidth + units.gu(1)
171+ height: secondaryLabel.implicitHeight + units.gu(1)
172+ radius: units.gu(0.5)
173+ visible: secondaryLabel.text !== ""
174+ Label {
175+ id: secondaryLabel
176+ anchors.centerIn: parent
177+ text: secondaryText
178+ visible: text !== ""
179+ textSize: Label.Small
180+ color: "White"
181+ }
182+ }
183+ }
184+ }
185+
186+ Label {
187+ id: primaryLabel
188+ anchors {
189+ top: imgFrame.bottom
190+ left: imgFrame.left
191+ right: imgFrame.right
192+ margins: units.gu(1)
193+ }
194+ color: podbird.appTheme.baseText
195+ elide: Text.ElideRight
196+ textSize: Label.Small
197+ wrapMode: Text.WordWrap
198+ maximumLineCount: 2
199+ horizontalAlignment: Text.AlignHCenter
200 }
201 }
202
203=== modified file 'app/components/CardView.qml'
204--- app/components/CardView.qml 2016-02-25 11:09:44 +0000
205+++ app/components/CardView.qml 2016-03-28 22:39:33 +0000
206@@ -18,33 +18,28 @@
207 import QtQuick 2.4
208 import Ubuntu.Components 1.3
209
210-Flickable {
211- id: cardViewFlickable
212+GridView {
213+ id: gridView
214+
215 anchors {
216 fill: parent
217 margins: units.gu(1)
218 }
219
220- // dont use flow.contentHeight as it is inaccurate due to height of labels
221- // changing as they load
222- contentHeight: flow.contentHeight + flow.anchors.margins * 2 + units.gu(8)
223- contentWidth: width
224-
225- property alias count: flow.count
226- property alias delegate: flow.delegate
227- property var getter
228- property alias model: flow.model
229- property real itemWidth: units.gu(15)
230-
231- onGetterChanged: flow.getter = getter // cannot use alias to set a function (must be var)
232-
233- ColumnFlow {
234- id: flow
235- anchors.fill: parent
236- columns: parseInt(cardViewFlickable.width / itemWidth) || 1 // never drop to 0
237- flickable: cardViewFlickable
238+ cellHeight: cellSize + heightOffset
239+ cellWidth: cellSize + widthOffset
240+
241+ header: Item {
242+ width: parent.width
243+ height: units.gu(2)
244 }
245
246+ readonly property int columns: parseInt(width / itemWidth) || 1 // never drop to 0
247+ readonly property int cellSize: width / columns
248+ property int itemWidth: units.gu(15)
249+ property int heightOffset: 0
250+ property int widthOffset: 0
251+
252 Component.onCompleted: {
253 // FIXME: workaround for qtubuntu not returning values depending on the grid unit definition
254 // for Flickable.maximumFlickVelocity and Flickable.flickDeceleration
255
256=== removed file 'app/components/ColumnFlow.qml'
257--- app/components/ColumnFlow.qml 2016-02-25 11:09:44 +0000
258+++ app/components/ColumnFlow.qml 1970-01-01 00:00:00 +0000
259@@ -1,494 +0,0 @@
260-/*
261- * Copyright (C) 2014-2016
262- * Andrew Hayzen <ahayzen@gmail.com>
263- *
264- * This program is free software; you can redistribute it and/or modify
265- * it under the terms of the GNU General Public License as published by
266- * the Free Software Foundation; version 3.
267- *
268- * This program is distributed in the hope that it will be useful,
269- * but WITHOUT ANY WARRANTY; without even the implied warranty of
270- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
271- * GNU General Public License for more details.
272- *
273- * You should have received a copy of the GNU General Public License
274- * along with this program. If not, see <http://www.gnu.org/licenses/>.
275- */
276-
277-import QtQuick 2.4
278-
279-Item {
280- id: columnFlow
281- property int columns: 1
282- property Flickable flickable
283- property var model
284- property Component delegate
285-
286- property var getter: function (i) { return model.get(i); } // optional getter override (useful for music-app ms2 models)
287-
288- property int buffer: units.gu(20)
289- property var columnHeights: []
290- property var columnHeightsMax: []
291- property int columnWidth: parent.width / columns
292- property int contentHeight: 0
293- property int count: model === undefined ? 0 : model.count
294- property int delayRebuildIndex: -1
295- property var incubating: ({}) // incubating objects
296- property var items: ({})
297- property var itemToColumn: ({}) // cache of the columns of indexes
298- property int lastIndex: 0 // the furtherest index loaded
299- property bool removing: false
300- property bool restoring: false // is the view restoring?
301- property var restoreItems: ({}) // when rebuilding items are stored here temporarily
302-
303- onColumnWidthChanged: {
304- if (restoring) {
305- return;
306- } else if (columns != columnHeights.length && visible) {
307- // number of columns has changed so rebuild the columns
308- rebuildColumns()
309- } else { // column width has changed update visible items properties linked to columnWidth
310- for (var column=0; column < columnHeights.length; column++) {
311- for (var i in columnHeights[column]) {
312- if (columnHeights[column].hasOwnProperty(i) && items.hasOwnProperty(i)) {
313- items[i].width = columnWidth;
314- items[i].x = column * columnWidth;
315- }
316- }
317- }
318-
319- ensureItemsVisible()
320- }
321- }
322-
323- onVisibleChanged: {
324- if (visible && delayRebuildIndex !== -1) { // restore from count change
325- if (delayRebuildIndex === 0) {
326- reset()
327- } else {
328- removeIndex(delayRebuildIndex)
329- }
330-
331- delayRebuildIndex = -1
332- append(true)
333- }
334-
335- // number of columns has changed while invisible so reset if not already restoring
336- if (visible && !restoring && columns != columnHeights.length) {
337- rebuildColumns()
338- }
339- }
340-
341- ListModel { // fakemodel for connections to link to when there is no model
342- id: fakeModel
343- }
344-
345- Connections {
346- target: model === undefined ? fakeModel : model
347- onModelReset: {
348- if (!visible && lastIndex > 0) {
349- delayRebuildIndex = 0
350- } else {
351- reset()
352- append()
353- }
354- }
355- onRowsInserted: {
356- if (!visible && lastIndex > 0) {
357- setDelayRebuildIndex(first)
358- } else {
359- if (first <= lastIndex) {
360- if (first === 0) {
361- reset()
362- } else {
363- removeIndex(first) // remove earliest index and all items after
364- }
365- }
366-
367- // Supply last index if larger as count is not updated until after insertion
368- append(true, last > count ? last : count)
369- }
370- }
371- onRowsRemoved: {
372- if (!visible) {
373- setDelayRebuildIndex(first)
374- } else {
375- if (first <= lastIndex) {
376- if (first === 0) {
377- reset()
378- } else {
379- removeIndex(first) // remove earliest index and all items after
380- }
381-
382- // count is not updated until after removal, so send insertMax
383- // insertMax is count - removal region inclusive - 1 (lastIndex is 1 infront)
384-
385- append(true, count - (1 + last - first) - 1) // rebuild any items on screen or before
386- }
387- }
388- }
389- }
390-
391-
392- Connections {
393- target: flickable
394- onContentYChanged: {
395- append() // Append any new items (scrolling down)
396-
397- ensureItemsVisible()
398- }
399- }
400-
401- // Append a new row of items if possible
402- function append(loadBefore, insertMax)
403- {
404- // Do not allow append to run if incubating
405- if (isIncubating() || restoring || removing) {
406- return;
407- }
408-
409- // get the columns in order
410- var columnsByHeight = getColumnsByHeight();
411- var workDone = false;
412-
413- // check if a new item in each column is possible
414- for (var i=0; i < columnsByHeight.length; i++) {
415- var y = columnHeightsMax[columnsByHeight[i]];
416-
417- // build new object in column if possible
418- // if insertMax is undefined then allow if there is work todo (from the count in the model)
419- // otherwise use the insertMax as the count to compare with the lastIndex added to the columnFlow
420- // and
421- // allow if the y position is within the viewport
422- // or if loadBefore is true then allow if the y position is before the viewport
423- if (((count > 0 && lastIndex < count && insertMax === undefined) || (insertMax !== undefined && lastIndex <= insertMax)) && (inViewport(y, 0) || (loadBefore === true && beforeViewport(y)))) {
424- incubateObject(lastIndex++, columnsByHeight[i], getMaxInColumn(columnsByHeight[i]), append);
425- workDone = true
426- } else {
427- break;
428- }
429- }
430-
431- if (!workDone) { // last iteration over append so visible ensure items are correct
432- ensureItemsVisible();
433- }
434- }
435-
436- // Detect if a loaded object is before the viewport with a buffer
437- function beforeViewport(y)
438- {
439- return y <= flickable.contentY - buffer;
440- }
441-
442- // Cache the size of the columns for use later
443- function cacheColumnHeights()
444- {
445- columnHeightsMax = [];
446-
447- for (var i=0; i < columnHeights.length; i++) {
448- var sum = 0;
449-
450- for (var j in columnHeights[i]) {
451- sum += columnHeights[i][j];
452- }
453-
454- columnHeightsMax.push(sum);
455- }
456-
457- if (!restoring) { // when not restoring otherwise user will be pushed to the top of the view
458- // set the height of columnFlow to max column (for flickable contentHeight)
459- contentHeight = Math.max.apply(null, columnHeightsMax);
460- }
461- }
462-
463- // Recache the visible items heights (due to a change in their height)
464- function cacheVisibleItemsHeights()
465- {
466- for (var i in items) {
467- if (items.hasOwnProperty(i)) {
468- columnHeights[itemToColumn[i]][i] = items[i].height;
469- }
470- }
471-
472- cacheColumnHeights();
473- }
474-
475- // Ensures that the correct items are visible
476- function ensureItemsVisible()
477- {
478- for (var i in items) {
479- if (items.hasOwnProperty(i)) {
480- items[i].visible = inViewport(items[i].y, items[i].height)
481- }
482- }
483- }
484-
485- // Return if there are incubating objects
486- function isIncubating()
487- {
488- for (var i in incubating) {
489- if (incubating.hasOwnProperty(i)) {
490- return true;
491- }
492- }
493-
494- return false;
495- }
496-
497- // Run after incubation to store new column height and call any further append/restores
498- function finishIncubation(index, callback)
499- {
500- var obj = incubating[index].object;
501- delete incubating[index];
502-
503- obj.heightChanged.connect(cacheVisibleItemsHeights) // if the height changes recache
504-
505- // Ensure properties linked to columnWidth are correct (as width may still be changing)
506- obj.x = itemToColumn[index] * columnWidth;
507- obj.width = columnWidth;
508-
509- items[index] = obj;
510-
511- columnHeights[itemToColumn[index]][index] = obj.height; // ensure height is the latest
512-
513- if (!isIncubating()) {
514- cacheColumnHeights();
515-
516- // Check if there is any more work to be done (append or restore)
517- callback();
518- }
519- }
520-
521- // Force any incubation to finish
522- function forceIncubationCompletion()
523- {
524- for (var i in incubating) {
525- if (incubating.hasOwnProperty(i)) {
526- incubating[i].forceCompletion()
527- }
528- }
529- }
530-
531- // Get the column index in order of height
532- function getColumnsByHeight()
533- {
534- var columnsByHeight = [];
535-
536- for (var i=0; i < columnHeightsMax.length; i++) {
537- var min = undefined;
538- var index = -1;
539-
540- // Find the smallest column that has not been found yet
541- for (var j=0; j < columnHeightsMax.length; j++) {
542- if (columnsByHeight.indexOf(j) === -1 && (min === undefined || columnHeightsMax[j] < min)) {
543- min = columnHeightsMax[j];
544- index = j;
545- }
546- }
547-
548- columnsByHeight.push(index);
549- }
550-
551- return columnsByHeight;
552- }
553-
554- // Get the highest index for a column
555- function getMaxInColumn(column)
556- {
557- var max;
558-
559- for (var i in columnHeights[column]) {
560- if (columnHeights[column].hasOwnProperty(i)) {
561- i = parseInt(i);
562-
563- if (items.hasOwnProperty(i)) {
564- if (i > max || max === undefined) {
565- max = i;
566- }
567- }
568- }
569- }
570-
571- return max;
572- }
573-
574- // Incubate an object for creation
575- function incubateObject(index, column, anchorIndex, callback)
576- {
577- // Load parameters to send to the object on creation
578- var params = {
579- "anchors.top": anchorIndex === undefined ? parent.top : items[anchorIndex].bottom,
580- index: index,
581- model: getter(index),
582- width: columnWidth,
583- x: column * columnWidth
584- };
585-
586- // Start incubating and cache the column
587- incubating[index] = delegate.incubateObject(parent, params);
588- itemToColumn[index] = column;
589-
590- if (incubating[index].status != Component.Ready) {
591- incubating[index].onStatusChanged = function(status) {
592- if (status == Component.Ready) {
593- finishIncubation(index, callback)
594- }
595- }
596- } else {
597- finishIncubation(index, callback)
598- }
599- }
600-
601- // Detect if a loaded object is in the viewport with a buffer
602- function inViewport(y, height)
603- {
604- return flickable.contentY - buffer < y + height && y < flickable.contentY + flickable.height + buffer;
605- }
606-
607- // Number of columns has changed rebuild with live items
608- function rebuildColumns()
609- {
610- restoring = true;
611- var i;
612-
613- forceIncubationCompletion()
614-
615- columnHeights = []
616- columnHeightsMax = []
617-
618- for (i=0; i < columns; i++) {
619- columnHeights.push({});
620- columnHeightsMax.push(0);
621- }
622-
623- lastIndex = 0;
624-
625- restoreItems = items;
626- items = {};
627-
628- restoreExisting()
629-
630- restoring = false;
631-
632- cacheColumnHeights(); // rebuilds contentHeight
633-
634- // If the columns have changed while the view was locked rerun
635- if (columns != columnHeights.length && visible) {
636- rebuildColumns()
637- } else {
638- append() // check if any new items can be added
639- }
640- }
641-
642- // Remove an index from the model (invalidating anything after)
643- function removeIndex(index)
644- {
645- removing = true
646-
647- forceIncubationCompletion()
648-
649- for (var i in items) {
650- if (i >= index && items.hasOwnProperty(i)) {
651- delete columnHeights[itemToColumn[i]][i]
652- delete itemToColumn[i]
653-
654- items[i].destroy()
655- delete items[i]
656- }
657- }
658-
659- lastIndex = index
660- removing = false
661-
662- cacheColumnHeights()
663- }
664-
665- // Restores existing items into potentially new positions
666- function restoreExisting()
667- {
668- var i;
669-
670- // get the columns in order
671- var columnsByHeight = getColumnsByHeight();
672- var workDone = false;
673-
674- // check if a new item in each column is possible
675- for (i=0; i < columnsByHeight.length; i++) {
676- var column = columnsByHeight[i];
677-
678- // build new object in column if possible
679- if (count > 0 && lastIndex < count) {
680- if (restoreItems.hasOwnProperty(lastIndex)) {
681- var item = restoreItems[lastIndex];
682- var maxInColumn = getMaxInColumn(column); // get lowest item in column
683-
684- itemToColumn[lastIndex] = column;
685- columnHeights[column][lastIndex] = item.height; // ensure height is the latest
686-
687- // Rebuild item properties
688- item.anchors.bottom = undefined
689- item.anchors.top = maxInColumn === undefined ? parent.top : items[maxInColumn].bottom;
690- item.x = column * columnWidth;
691- item.visible = inViewport(item.y, item.height);
692-
693- // Migrate item from restoreItems to items
694- items[lastIndex] = item;
695- delete restoreItems[lastIndex];
696-
697- // set after restore as height will likely change causing cacheVisibleItemsHeights to be run
698- item.width = columnWidth;
699-
700- cacheColumnHeights(); // ensure column heights are up to date
701-
702- lastIndex++;
703- workDone = true;
704- }
705- } else {
706- break;
707- }
708- }
709-
710- if (workDone) {
711- restoreExisting() // if work done then check if any more is needed
712- } else {
713- restoreItems = {}; // ensure restoreItems is empty
714- }
715- }
716-
717- // Reset the column flow
718- function reset()
719- {
720- forceIncubationCompletion()
721-
722- // Destroy any old items
723- for (var j in items) {
724- if (items.hasOwnProperty(j)) {
725- items[j].destroy()
726- }
727- }
728-
729- // Reset and rebuild the variables
730- items = ({})
731- itemToColumn = ({})
732- lastIndex = 0
733-
734- columnHeights = []
735-
736- for (var k=0; k < columns; k++) {
737- columnHeights.push({})
738- }
739-
740- cacheColumnHeights()
741-
742- contentHeight = 0
743- }
744-
745- function setDelayRebuildIndex(index)
746- {
747- if (delayRebuildIndex === -1 || index < lastIndex) {
748- delayRebuildIndex = index
749- }
750- }
751-
752- Component.onCompleted: append(true)
753-}
754
755=== added file 'app/components/CustomSectionHeader.qml'
756--- app/components/CustomSectionHeader.qml 1970-01-01 00:00:00 +0000
757+++ app/components/CustomSectionHeader.qml 2016-03-28 22:39:33 +0000
758@@ -0,0 +1,47 @@
759+/*
760+ * Copyright 2016 Podbird Team
761+ *
762+ * This file is part of Podbird.
763+ *
764+ * Podbird is free software; you can redistribute it and/or modify
765+ * it under the terms of the GNU General Public License as published by
766+ * the Free Software Foundation; version 3.
767+ *
768+ * Podbird is distributed in the hope that it will be useful,
769+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
770+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
771+ * GNU General Public License for more details.
772+ *
773+ * You should have received a copy of the GNU General Public License
774+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
775+ */
776+
777+import QtQuick 2.4
778+import Ubuntu.Components 1.3
779+
780+Item {
781+ id: customSectionHeader
782+
783+ property alias title: headerText.text
784+
785+ height: headerText.text !== "" ? headerText.implicitHeight + divider.height + headerText.anchors.topMargin + headerText.anchors.bottomMargin
786+ : units.gu(0)
787+
788+ anchors { left: parent.left; right: parent.right; margins: units.gu(2) }
789+
790+ Label {
791+ id: headerText
792+ color: podbird.appTheme.baseText
793+ font.weight: Font.DemiBold
794+ anchors { top: parent.top; topMargin: units.gu(2); bottom: parent.bottom; bottomMargin: units.gu(2) }
795+ width: parent.width
796+ }
797+
798+ Rectangle {
799+ id: divider
800+ color: settings.themeName === "Dark.qml" ? "#888888" : "#cdcdcd"
801+ width: parent.width
802+ height: units.dp(1)
803+ anchors.bottom: parent.bottom
804+ }
805+}
806
807=== removed file 'app/components/TabsList.qml'
808--- app/components/TabsList.qml 2016-03-28 22:39:32 +0000
809+++ app/components/TabsList.qml 1970-01-01 00:00:00 +0000
810@@ -1,52 +0,0 @@
811-/*
812- * Copyright 2015-2016 Podbird Team
813- *
814- * This file is part of Podbird.
815- *
816- * Podbird is free software; you can redistribute it and/or modify
817- * it under the terms of the GNU General Public License as published by
818- * the Free Software Foundation; version 3.
819- *
820- * Podbird is distributed in the hope that it will be useful,
821- * but WITHOUT ANY WARRANTY; without even the implied warranty of
822- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
823- * GNU General Public License for more details.
824- *
825- * You should have received a copy of the GNU General Public License
826- * along with this program. If not, see <http://www.gnu.org/licenses/>.
827- */
828-
829-import QtQuick 2.4
830-import Ubuntu.Components 1.3
831-
832-ActionList {
833- id: tabsList
834-
835- property int currentTab: tabs.selectedTabIndex
836-
837- children: [
838- Action {
839- text: i18n.tr("Episodes")
840- visible: currentTab !== 0
841- onTriggered: {
842- tabs.selectedTabIndex = 0
843- }
844- },
845-
846- Action {
847- text: i18n.tr("Podcasts")
848- visible: currentTab !== 1
849- onTriggered: {
850- tabs.selectedTabIndex = 1
851- }
852- },
853-
854- Action {
855- text: i18n.tr("Settings")
856- visible: currentTab !== 2
857- onTriggered: {
858- tabs.selectedTabIndex = 2
859- }
860- }
861- ]
862-}
863
864=== modified file 'app/podbird.qml'
865--- app/podbird.qml 2016-03-28 22:39:32 +0000
866+++ app/podbird.qml 2016-03-28 22:39:33 +0000
867@@ -18,13 +18,12 @@
868
869 import QtQuick 2.4
870 import Podbird 1.0
871-import UserMetrics 0.1
872-import QtMultimedia 5.4
873+import QtMultimedia 5.6
874 import Ubuntu.Connectivity 1.0
875 import Qt.labs.settings 1.0
876 import Ubuntu.Components 1.3
877 import QtQuick.LocalStorage 2.0
878-import Ubuntu.DownloadManager 0.1
879+import Ubuntu.DownloadManager 1.2
880 import "ui"
881 import "themes" as Themes
882 import "podcasts.js" as Podcasts
883@@ -45,11 +44,11 @@
884
885 Component.onDestruction: {
886 console.log("[LOG]: Download cancelled");
887- downloader.cancel();
888 var db = Podcasts.init()
889 db.transaction(function (tx) {
890 tx.executeSql('UPDATE Episode SET queued=0 WHERE queued=1');
891 })
892+ Podcasts.clearQueue()
893 }
894
895 // RefreshModel function to call refreshModel() function of the tab currently
896@@ -138,84 +137,156 @@
897 }
898 }
899
900- SingleDownload {
901+ Component {
902+ id: singleDownloadComponent
903+ SingleDownload {
904+ id: singleDownloadObject
905+ property string image
906+ property string title
907+ property string guid
908+ metadata: Metadata {
909+ showInIndicator: true
910+ title: singleDownloadObject.title
911+ }
912+ }
913+ }
914+
915+ function downloadEpisode(image, title, guid, url) {
916+ var singleDownload = singleDownloadComponent.createObject(podbird, {"image": image, "title": title, "guid": guid})
917+ singleDownload.download(url)
918+ }
919+
920+ DownloadManager {
921 id: downloader
922- property var queue: []
923- property string downloadingGuid
924-
925- onFinished: {
926+
927+ property string downloadingGuid: downloads.length > 0 ? downloads[0].guid : "NULL"
928+ property int progress: downloads.length > 0 ? downloads[0].progress : 0
929+
930+ cleanDownloads: true
931+ onDownloadFinished: {
932 var db = Podcasts.init();
933 var finalLocation = fileManager.saveDownload(path);
934 db.transaction(function (tx) {
935- tx.executeSql("UPDATE Episode SET downloadedfile=?, queued=0 WHERE guid=?", [finalLocation, downloadingGuid]);
936- queue.shift();
937- if (queue.length > 0) {
938- downloadingGuid = queue[0][0];
939- download(queue[0][1]);
940- } else {
941- downloadingGuid = "";
942- }
943+ tx.executeSql("UPDATE Episode SET downloadedfile=?, queued=0 WHERE guid=?", [finalLocation, download.guid]);
944 });
945 }
946
947- function addDownload(guid, url) {
948- queue.push([guid, url]);
949- if (queue.length == 1) {
950- downloadingGuid = guid;
951- download(url);
952- }
953- }
954- }
955-
956- // UserMetrics to show Podbird stats on welcome screen
957- Metric {
958- id: podcastsMetric
959- name: "podcast-metrics"
960- // TRANSLATORS: this refers to a number of songs greater than one. The actual number will be prepended to the string automatically (plural forms are not yet fully supported in usermetrics, the library that displays that string)
961- format: i18n.tr("Podcasts listened to today: <b>%1</b>")
962- emptyFormat: i18n.tr("No podcasts listened to today")
963- domain: "com.mikeasoft.podbird"
964- }
965-
966- // Load the media player only when the user starts to play some media. This
967- // should improve app-startup slightly.
968- Loader {
969- id: playerLoader
970- sourceComponent: currentUrl != "" ? playerComponent : undefined
971- }
972-
973- Component {
974- id: playerComponent
975- MediaPlayer {
976- id: player
977-
978- property bool podcastCounted: false
979-
980- source: currentUrl
981-
982- onSourceChanged: {
983- podcastCounted = false
984- }
985-
986- onPositionChanged: {
987- if (currentGuid == "" || duration <= 0) {
988- return;
989- }
990-
991- if (position > 10000 && !podcastCounted) {
992- podcastCounted = true
993- podcastsMetric.increment()
994- console.log("[LOG]: Podcast User metric incremented")
995- }
996-
997- var db = Podcasts.init();
998- db.transaction(function (tx) {
999- tx.executeSql("UPDATE Episode SET position=? WHERE guid=?", [position >= duration ? 120 : position, currentGuid]);
1000- if (position >= duration - 120) {
1001- tx.executeSql("UPDATE Episode SET listened = 1 WHERE guid=?", [currentGuid]);
1002- }
1003- });
1004- }
1005+ onErrorFound: {
1006+ console.log("[ERROR]: " + download.errorMessage)
1007+ }
1008+ }
1009+
1010+ MediaPlayer {
1011+ id: player
1012+
1013+ // Wrapper function around decodeURIComponent() to prevent exceptions
1014+ // from bubbling up to the app.
1015+ function decodeFileURI(filename)
1016+ {
1017+ var newFilename = "";
1018+ try {
1019+ newFilename = decodeURIComponent(filename);
1020+ } catch (e) {
1021+ newFilename = filename;
1022+ console.log("Unicode decoding error:", filename, e.message)
1023+ }
1024+
1025+ return newFilename;
1026+ }
1027+
1028+ function metaForSource(source) {
1029+ var blankMeta = {
1030+ name: "",
1031+ artist: "",
1032+ image: "",
1033+ guid: "",
1034+ }
1035+
1036+ source = source.toString()
1037+
1038+ return Podcasts.lookup(decodeFileURI(source)) || blankMeta;
1039+ }
1040+
1041+ function toggle() {
1042+ if (playbackState === MediaPlayer.PlayingState) {
1043+ pause()
1044+ } else {
1045+ play()
1046+ }
1047+ }
1048+
1049+ function playEpisode(guid, image, name, artist, url) {
1050+ // Clear current queue
1051+ player.playlist.clear()
1052+ Podcasts.clearQueue()
1053+
1054+ // Add episode to queue
1055+ Podcasts.addItemToQueue(guid, image, name, artist, url)
1056+ player.playlist.addItem(url)
1057+
1058+ // Play episode
1059+ player.play()
1060+ }
1061+
1062+ function addEpisodeToQueue(guid, image, name, artist, url) {
1063+ Podcasts.addItemToQueue(guid, image, name, artist, url)
1064+ player.playlist.addItem(url)
1065+
1066+ // If added episode is the first one in the queue, then set the current metadata
1067+ // so that the bottom player controls will be shown, allowing the user to play
1068+ // the episode if he chooses to.
1069+ if (player.playlist.itemCount === 0) {
1070+ currentGuid = guid
1071+ currentName = name
1072+ currentArtist = artist
1073+ currentImage = image
1074+ currentUrl = url
1075+ }
1076+ }
1077+
1078+ property bool endOfMedia: false
1079+ property double progress: 0
1080+
1081+ playlist: Playlist {
1082+ playbackMode: Playlist.Sequential
1083+
1084+ readonly property bool canGoPrevious: currentIndex !== 0
1085+ readonly property bool canGoNext: currentIndex !== itemCount - 1
1086+
1087+ onCurrentItemSourceChanged: {
1088+ var meta = player.metaForSource(currentItemSource)
1089+ currentGuid = "";
1090+ currentName = meta.name
1091+ currentArtist = meta.artist
1092+ currentImage = meta.image
1093+ currentGuid = meta.guid
1094+ }
1095+ }
1096+
1097+ onStatusChanged: {
1098+ if (status === MediaPlayer.EndOfMedia) {
1099+ console.log("[LOG]: End of Media. Stopping.")
1100+ endOfMedia = true
1101+ stop()
1102+ }
1103+ }
1104+
1105+ onStopped: {
1106+ if (playlist.itemCount > 0) {
1107+ if (endOfMedia) {
1108+ // We just ended media, so jump to start of playlist
1109+ playlist.currentIndex = 0;
1110+
1111+ // Play then pause otherwise when we come from EndOfMedia
1112+ // it calls next() until EndOfMedia again.
1113+ play()
1114+ }
1115+
1116+ pause()
1117+ }
1118+
1119+ // Always reset endOfMedia
1120+ endOfMedia = false
1121 }
1122 }
1123
1124@@ -290,13 +361,13 @@
1125 states: [
1126 State {
1127 name: "shown"
1128- when: currentUrl != "" && !mainStack.currentPage.isNowPlayingPage
1129+ when: player.playlist.itemCount !== 0 && !mainStack.currentPage.isNowPlayingPage
1130 PropertyChanges { target: playerControlLoader; anchors.bottomMargin: 0 }
1131 },
1132
1133 State {
1134 name: "hidden"
1135- when: currentUrl == "" || mainStack.currentPage.isNowPlayingPage || !playerControl.visible
1136+ when: player.playlist.itemCount === 0 || mainStack.currentPage.isNowPlayingPage || !playerControl.visible
1137 PropertyChanges { target: playerControlLoader; anchors.bottomMargin: -units.gu(7) }
1138 }
1139 ]
1140
1141=== modified file 'app/podcasts.js'
1142--- app/podcasts.js 2016-03-28 22:39:32 +0000
1143+++ app/podcasts.js 2016-03-28 22:39:33 +0000
1144@@ -22,6 +22,7 @@
1145 db.transaction(function(tx) {
1146 tx.executeSql('CREATE TABLE IF NOT EXISTS Podcast(artist TEXT, name TEXT, description TEXT, feed TEXT, image TEXT, lastupdate TIMESTAMP)');
1147 tx.executeSql('CREATE TABLE IF NOT EXISTS Episode(guid TEXT, podcast INTEGER, name TEXT, subtitle TEXT, description TEXT, duration INTEGER, audiourl TEXT, downloadedfile TEXT, published TIMESTAMP, queued BOOLEAN, listened BOOLEAN, favourited BOOLEAN, position INTEGER, FOREIGN KEY(podcast) REFERENCES Podcast(rowid))');
1148+ tx.executeSql('CREATE TABLE IF NOT EXISTS Queue(ind INTEGER NOT NULL, guid TEXT, image TEXT, name TEXT, artist TEXT, url TEXT)');
1149 });
1150
1151 try {
1152@@ -55,6 +56,105 @@
1153 return db;
1154 }
1155
1156+// Function to add item to queue
1157+function addItemToQueue(guid, image, name, artist, url) {
1158+ var db = init()
1159+
1160+ db.transaction(function(tx) {
1161+ var ind = getNextIndex(tx);
1162+ var rs = tx.executeSql("INSERT OR REPLACE INTO Queue (ind, guid, image, name, artist, url) VALUES (?, ?, ?, ?, ?, ?)", [ind, guid, image, name, artist, url]);
1163+ if (rs.rowsAffected > 0) {
1164+ console.log("[LOG]: QUEUE add OK")
1165+ console.log("[LOG]: URL Added to queue: " + url)
1166+ } else {
1167+ console.log("[LOG]: QUEUE add FAIL")
1168+ }
1169+ });
1170+}
1171+
1172+function removeItemFromQueue(source) {
1173+ var db = init()
1174+
1175+ db.transaction(function(tx) {
1176+ // Remove selected source from the queue
1177+ tx.executeSql("DELETE FROM Queue WHERE url = ?", source)
1178+
1179+ // Rebuild queue in order
1180+ var rs = tx.executeSql("SELECT ind FROM Queue ORDER BY ind ASC")
1181+
1182+ for (var i=0; i<rs.rows.length; i++) {
1183+ tx.executeSql("UPDATE Queue SET ind = ? WHERE ind = ?", [i, rs.rows.item(i).ind])
1184+ }
1185+ })
1186+}
1187+
1188+// Function to clear the queue
1189+function clearQueue() {
1190+ var db = init();
1191+ db.transaction(function(tx) {
1192+ tx.executeSql("DELETE FROM Queue");
1193+ });
1194+}
1195+
1196+function lookup(source) {
1197+ var db = init();
1198+ var meta = {
1199+ name: "",
1200+ artist: "",
1201+ image: "",
1202+ guid: "",
1203+ }
1204+
1205+ db.transaction(function(tx) {
1206+ var rs = tx.executeSql("SELECT * FROM Queue ORDER BY ind ASC");
1207+ for(var i = 0; i < rs.rows.length; i++) {
1208+ var episode = rs.rows.item(i);
1209+ if (source === episode.url) {
1210+ meta.name = episode.name
1211+ meta.artist = episode.artist
1212+ meta.image = episode.image
1213+ meta.guid = episode.guid
1214+ break
1215+ }
1216+ }
1217+ });
1218+
1219+ return meta
1220+}
1221+
1222+// Function to get the next index for the queue
1223+function getNextIndex(tx) {
1224+ var ind;
1225+
1226+ if (tx === undefined) {
1227+ var db = init();
1228+ db.transaction(function(tx) {
1229+ ind = getNextIndex(tx);
1230+ });
1231+ } else {
1232+ var rs = tx.executeSql('SELECT MAX(ind) FROM Queue')
1233+ ind = isQueueEmpty(tx) ? 0 : rs.rows.item(0)["MAX(ind)"] + 1
1234+ }
1235+
1236+ return ind;
1237+}
1238+
1239+function isQueueEmpty(tx) {
1240+ var empty = false;
1241+
1242+ if (tx === undefined) {
1243+ var db = init();
1244+ db.transaction( function(tx) {
1245+ empty = isQueueEmpty(tx)
1246+ });
1247+ } else {
1248+ var rs = tx.executeSql("SELECT count(*) as value FROM Queue")
1249+ empty = rs.rows.item(0).value === 0
1250+ }
1251+
1252+ return empty
1253+}
1254+
1255 function subscribe(artist, name, feed, img) {
1256 var db = init();
1257 db.transaction(function(tx) {
1258@@ -259,8 +359,8 @@
1259 var rs2 = tx.executeSql("SELECT rowid, * FROM Episode WHERE podcast=? ORDER BY published DESC", [rs.rows.item(i).rowid]);
1260 var loopCount = maxEpisodeDownload > rs2.rows.length ? rs2.rows.length : maxEpisodeDownload
1261 for (var j=0; j < loopCount; j++) {
1262- if (!rs2.rows.item(j).downloadedfile && !rs2.rows.item(j).listened) {
1263- downloader.addDownload(rs2.rows.item(j).guid, rs2.rows.item(j).audiourl)
1264+ if (!rs2.rows.item(j).downloadedfile && !rs2.rows.item(j).listened && rs2.rows.item(j).audiourl) {
1265+ podbird.downloadEpisode(rs.rows.item(i).image, rs2.rows.item(j).name, rs2.rows.item(j).guid, rs2.rows.item(j).audiourl)
1266 tx.executeSql("UPDATE Episode SET queued=1 WHERE guid = ?", [rs2.rows.item(j).guid]);
1267 }
1268 }
1269
1270=== modified file 'app/settings/About.qml'
1271--- app/settings/About.qml 2016-02-25 11:09:44 +0000
1272+++ app/settings/About.qml 2016-03-28 22:39:33 +0000
1273@@ -36,7 +36,6 @@
1274
1275 anchors {
1276 left: parent.left
1277- leftMargin: units.gu(2)
1278 bottom: parent.bottom
1279 }
1280
1281
1282=== modified file 'app/ui/EpisodesPage.qml'
1283--- app/ui/EpisodesPage.qml 2016-03-28 22:39:32 +0000
1284+++ app/ui/EpisodesPage.qml 2016-03-28 22:39:33 +0000
1285@@ -17,10 +17,10 @@
1286 */
1287
1288 import QtQuick 2.4
1289-import QtMultimedia 5.4
1290+import QtMultimedia 5.6
1291 import Ubuntu.Components 1.3
1292 import QtQuick.LocalStorage 2.0
1293-import Ubuntu.DownloadManager 0.1
1294+import Ubuntu.DownloadManager 1.2
1295 import Ubuntu.Components.Popups 1.3
1296 import "../podcasts.js" as Podcasts
1297 import "../components"
1298@@ -442,8 +442,8 @@
1299 id: listItemLayout
1300
1301 title.text: model.name !== undefined ? model.name.trim() : "Undefined"
1302- title.color: currentGuid === model.guid || downloader.downloadingGuid === model.guid ? podbird.appTheme.focusText
1303- : podbird.appTheme.baseText
1304+ title.color: downloader.downloadingGuid === model.guid ? podbird.appTheme.focusText
1305+ : podbird.appTheme.baseText
1306 // #FIXME: Change this 2 to prevent title eliding when UITK is updated to rev > 1800
1307 title.maximumLineCount: 1
1308
1309@@ -505,12 +505,24 @@
1310 tx.executeSql("UPDATE Episode SET queued=1 WHERE guid = ?", [model.guid]);
1311 });
1312 episodeModel.setProperty(model.index, "queued", 1)
1313- downloader.addDownload(model.guid, model.audiourl);
1314+ if (model.audiourl) {
1315+ podbird.downloadEpisode(model.image, model.name, model.guid, model.audiourl)
1316+ } else {
1317+ console.log("[ERROR]: Invalid download url: " + model.audiourl)
1318+ }
1319 }
1320 }
1321 },
1322
1323 Action {
1324+ iconName: "add-to-playlist"
1325+ onTriggered: {
1326+ var url = model.downloadedfile ? "file://" + model.downloadedfile : model.audiourl
1327+ player.addEpisodeToQueue(model.guid, model.image, model.name, model.artist, url)
1328+ }
1329+ },
1330+
1331+ Action {
1332 iconName: model.favourited ? "unlike" : "like"
1333 onTriggered: {
1334 var db = Podcasts.init();
1335@@ -536,20 +548,10 @@
1336
1337 onClicked: {
1338 Haptics.play()
1339- var db = Podcasts.init();
1340- db.transaction(function (tx) {
1341- if (currentGuid !== model.guid) {
1342- currentGuid = "";
1343- currentUrl = model.downloadedfile ? model.downloadedfile : model.audiourl;
1344- var rs = tx.executeSql("SELECT position FROM Episode WHERE guid=?", [model.guid]);
1345- playerLoader.item.play();
1346- playerLoader.item.seek(rs.rows.item(0).position);
1347- currentName = model.name;
1348- currentArtist = model.artist;
1349- currentImage = model.image;
1350- currentGuid = model.guid;
1351- }
1352- });
1353+ if (currentGuid !== model.guid) {
1354+ currentUrl = model.downloadedfile ? "file://" + model.downloadedfile : model.audiourl;
1355+ player.playEpisode(model.guid, model.image, model.name, model.artist, currentUrl)
1356+ }
1357 }
1358 }
1359
1360
1361=== modified file 'app/ui/EpisodesTab.qml'
1362--- app/ui/EpisodesTab.qml 2016-03-28 22:39:32 +0000
1363+++ app/ui/EpisodesTab.qml 2016-03-28 22:39:33 +0000
1364@@ -17,10 +17,10 @@
1365 */
1366
1367 import QtQuick 2.4
1368-import QtMultimedia 5.4
1369+import QtMultimedia 5.6
1370 import Ubuntu.Components 1.3
1371 import QtQuick.LocalStorage 2.0
1372-import Ubuntu.DownloadManager 0.1
1373+import Ubuntu.DownloadManager 1.2
1374 import Ubuntu.Components.Popups 1.3
1375 import "../podcasts.js" as Podcasts
1376 import "../components"
1377@@ -64,58 +64,6 @@
1378 episodesPage.header = searchHeader
1379 searchField.item.forceActiveFocus()
1380 }
1381- },
1382-
1383- Action {
1384- iconName: "select"
1385- visible: episodesPageHeaderSections.selectedIndex === 0
1386- text: i18n.tr("Mark all listened")
1387- onTriggered: {
1388- var db = Podcasts.init();
1389- db.transaction(function (tx) {
1390- for (var i=0; i<episodesModel.count; i++) {
1391- tx.executeSql("UPDATE Episode SET listened=1 WHERE guid=?", [episodesModel.get(i).guid]);
1392- }
1393- episodesModel.clear()
1394- });
1395- }
1396- },
1397-
1398- Action {
1399- iconName: "save"
1400- visible: episodesPageHeaderSections.selectedIndex === 0
1401- text: i18n.tr("Download all")
1402- onTriggered: {
1403- var db = Podcasts.init();
1404- db.transaction(function (tx) {
1405- for (var i=0; i<episodesModel.count; i++) {
1406- if (!episodesModel.get(i).downloadedfile) {
1407- episodesModel.setProperty(i, "queued", 1)
1408- tx.executeSql("UPDATE Episode SET queued=1 WHERE guid = ?", [episodesModel.get(i).guid]);
1409- downloader.addDownload(episodesModel.get(i).guid, episodesModel.get(i).audiourl);
1410- }
1411- }
1412- });
1413- }
1414- },
1415-
1416- Action {
1417- iconName: "delete"
1418- text: i18n.tr("Delete all")
1419- visible: episodesPageHeaderSections.selectedIndex === 1
1420- onTriggered: {
1421- var db = Podcasts.init();
1422- db.transaction(function (tx) {
1423- for (var i=0; i<episodesModel.count; i++) {
1424- if (episodesModel.get(i).downloadedfile) {
1425- fileManager.deleteFile(episodesModel.get(i).downloadedfile);
1426- tx.executeSql("UPDATE Episode SET downloadedfile = NULL WHERE guid = ?", [episodesModel.get(i).guid]);
1427- episodesModel.setProperty(i, "downloadedfile", "")
1428- }
1429- }
1430- });
1431- refreshModel();
1432- }
1433 }
1434 ]
1435
1436@@ -124,7 +72,6 @@
1437
1438 anchors {
1439 left: parent.left
1440- leftMargin: units.gu(2)
1441 bottom: parent.bottom
1442 }
1443
1444@@ -132,7 +79,7 @@
1445 selectedSectionColor: podbird.appTheme.focusText
1446 }
1447
1448- model: [i18n.tr("Recent"), i18n.tr("Downloaded"), i18n.tr("Favourites")]
1449+ model: [i18n.tr("Recent"), i18n.tr("Downloads"), i18n.tr("Favourites")]
1450 onSelectedIndexChanged: {
1451 refreshModel();
1452 }
1453@@ -174,6 +121,169 @@
1454 }
1455 }
1456
1457+ PageHeader {
1458+ id: selectionHeader
1459+ visible: episodeList.ViewItems.selectMode
1460+
1461+ onVisibleChanged: {
1462+ if (visible) {
1463+ episodesPage.header = selectionHeader
1464+ }
1465+ }
1466+
1467+ StyleHints {
1468+ backgroundColor: podbird.appTheme.background
1469+ }
1470+
1471+ leadingActionBar.actions: [
1472+ Action {
1473+ iconName: "back"
1474+ text: i18n.tr("Back")
1475+ onTriggered: {
1476+ episodeList.closeSelection()
1477+ }
1478+ }
1479+ ]
1480+
1481+ trailingActionBar {
1482+ numberOfSlots: 6
1483+ actions: [
1484+ Action {
1485+ iconName: "select"
1486+ text: i18n.tr("Mark Listened")
1487+ enabled: episodeList.ViewItems.selectedIndices.length !== 0
1488+ onTriggered: {
1489+ var db = Podcasts.init();
1490+ db.transaction(function (tx) {
1491+ for (var i=0; i<episodeList.ViewItems.selectedIndices.length; i++) {
1492+ var index = episodeList.ViewItems.selectedIndices[i]
1493+ tx.executeSql("UPDATE Episode SET listened=1 WHERE guid=?", [episodesModel.get(index).guid]);
1494+ }
1495+ });
1496+
1497+ refreshModel();
1498+ episodeList.closeSelection()
1499+ }
1500+ },
1501+
1502+ Action {
1503+ iconName: "save"
1504+ text: i18n.tr("Download episode(s)")
1505+ enabled: episodeList.ViewItems.selectedIndices.length !== 0
1506+ visible: episodesPageHeaderSections.selectedIndex !== 1
1507+
1508+ onTriggered: {
1509+ var db = Podcasts.init();
1510+ db.transaction(function (tx) {
1511+ for (var i=0; i<episodeList.ViewItems.selectedIndices.length; i++) {
1512+ var index = episodeList.ViewItems.selectedIndices[i]
1513+ if (!episodesModel.get(index).downloadedfile) {
1514+ episodesModel.setProperty(index, "queued", 1)
1515+ tx.executeSql("UPDATE Episode SET queued=1 WHERE guid = ?", [episodesModel.get(index).guid]);
1516+ if (episodesModel.get(index).audiourl) {
1517+ podbird.downloadEpisode(episodesModel.get(index).image, episodesModel.get(index).name, episodesModel.get(index).guid, episodesModel.get(index).audiourl)
1518+ } else {
1519+ console.log("[ERROR]: Invalid download url: " + episodesModel.get(index).audiourl)
1520+ }
1521+ }
1522+ }
1523+ });
1524+
1525+ refreshModel();
1526+ episodeList.closeSelection()
1527+ }
1528+ },
1529+
1530+ Action {
1531+ iconName: "delete"
1532+ text: i18n.tr("Delete episode(s)")
1533+ enabled: episodeList.ViewItems.selectedIndices.length !== 0
1534+
1535+ onTriggered: {
1536+ var db = Podcasts.init();
1537+ db.transaction(function (tx) {
1538+ for (var i=0; i<episodeList.ViewItems.selectedIndices.length; i++) {
1539+ var index = episodeList.ViewItems.selectedIndices[i]
1540+ if (episodesModel.get(index).downloadedfile) {
1541+ fileManager.deleteFile(episodesModel.get(index).downloadedfile);
1542+ tx.executeSql("UPDATE Episode SET downloadedfile = NULL WHERE guid = ?", [episodesModel.get(index).guid]);
1543+ episodesModel.setProperty(index, "downloadedfile", "")
1544+ }
1545+ }
1546+ });
1547+
1548+ refreshModel();
1549+ episodeList.closeSelection()
1550+ }
1551+ },
1552+
1553+ Action {
1554+ iconName: "like"
1555+ text: i18n.tr("Favourite episode(s)")
1556+ visible: episodesPageHeaderSections.selectedIndex !== 2
1557+ enabled: episodeList.ViewItems.selectedIndices.length !== 0
1558+
1559+ onTriggered: {
1560+ var db = Podcasts.init();
1561+ db.transaction(function (tx) {
1562+ for (var i=0; i<episodeList.ViewItems.selectedIndices.length; i++) {
1563+ var index = episodeList.ViewItems.selectedIndices[i]
1564+ if (!episodesModel.get(index).favourited) {
1565+ tx.executeSql("UPDATE Episode SET favourited=1 WHERE guid=?", [episodesModel.get(index).guid])
1566+ episodesModel.setProperty(index, "favourited", 1)
1567+ }
1568+ }
1569+ });
1570+
1571+ refreshModel();
1572+ episodeList.closeSelection()
1573+ }
1574+ },
1575+
1576+ Action {
1577+ iconName: "unlike"
1578+ text: i18n.tr("Unfavourite episode(s)")
1579+ visible: episodesPageHeaderSections.selectedIndex === 2
1580+ enabled: episodeList.ViewItems.selectedIndices.length !== 0
1581+
1582+ onTriggered: {
1583+ var db = Podcasts.init();
1584+ db.transaction(function (tx) {
1585+ for (var i=0; i<episodeList.ViewItems.selectedIndices.length; i++) {
1586+ var index = episodeList.ViewItems.selectedIndices[i]
1587+ if (episodesModel.get(index).favourited) {
1588+ tx.executeSql("UPDATE Episode SET favourited=0 WHERE guid=?", [episodesModel.get(index).guid])
1589+ episodesModel.setProperty(index, "favourited", 0)
1590+ }
1591+ }
1592+ });
1593+
1594+ refreshModel();
1595+ episodeList.closeSelection()
1596+ }
1597+ },
1598+
1599+ Action {
1600+ iconName: "add-to-playlist"
1601+ text: i18n.tr("Add to queue")
1602+ enabled: episodeList.ViewItems.selectedIndices.length !== 0
1603+
1604+ onTriggered: {
1605+ for (var i=0; i<episodeList.ViewItems.selectedIndices.length; i++) {
1606+ var index = episodeList.ViewItems.selectedIndices[i]
1607+ if (episodesModel.get(index).audiourl) {
1608+ var url = episodesModel.get(index).downloadedfile ? "file://" + episodesModel.get(index).downloadedfile : episodesModel.get(index).audiourl
1609+ player.addEpisodeToQueue(episodesModel.get(index).guid, episodesModel.get(index).image, episodesModel.get(index).name, episodesModel.get(index).artist, url)
1610+ }
1611+ }
1612+
1613+ episodeList.closeSelection()
1614+ }
1615+ }
1616+ ]
1617+ }
1618+ }
1619+
1620 Loader {
1621 id: emptyState
1622
1623@@ -185,7 +295,7 @@
1624 verticalCenterOffset: Qt.inputMethod.visible ? units.gu(4) : 0
1625 }
1626
1627- sourceComponent: episodesModel.count === 0 || sortedEpisodeModel.count === 0 ? emptyStateComponent : undefined
1628+ sourceComponent: (episodesModel.count === 0 || sortedEpisodeModel.count === 0) && downloader.downloads.length === 0 ? emptyStateComponent : undefined
1629 }
1630
1631 Component {
1632@@ -315,6 +425,9 @@
1633 ListView {
1634 id: episodeList
1635
1636+ signal clearSelection()
1637+ signal closeSelection()
1638+
1639 Component.onCompleted: {
1640 // FIXME: workaround for qtubuntu not returning values depending on the grid unit definition
1641 // for Flickable.maximumFlickVelocity and Flickable.flickDeceleration
1642@@ -332,35 +445,77 @@
1643
1644 clip: true
1645 model: sortedEpisodeModel
1646+
1647+ header: Column {
1648+ width: episodeList.width
1649+ visible: height !== 0
1650+ height: downloader.downloads.length > 0 && episodesPageHeaderSections.selectedIndex === 1 ? childrenRect.height : 0
1651+
1652+ CustomSectionHeader {
1653+ title: i18n.tr("Downloads in progress")
1654+ }
1655+
1656+ Repeater {
1657+ model: downloader.downloads
1658+ delegate: ListItem {
1659+ divider.visible: false
1660+ height: inProgressLayout.height
1661+ SlotsLayout {
1662+ id: inProgressLayout
1663+
1664+ Image {
1665+ height: width
1666+ width: units.gu(6)
1667+ source: modelData.image !== undefined ? modelData.image : Qt.resolvedUrl("../graphics/podbird.png")
1668+ SlotsLayout.position: SlotsLayout.Leading
1669+ sourceSize { width: width; height: height }
1670+ }
1671+
1672+ mainSlot: Column {
1673+ spacing: units.gu(0.5)
1674+
1675+ Label {
1676+ text: modelData.title
1677+ width: parent.width
1678+ elide: Text.ElideRight
1679+ }
1680+
1681+ CustomProgressBar {
1682+ width: parent.width
1683+ height: modelData.progress > 0 ? units.dp(5) : 0
1684+ progress: modelData.progress
1685+ indeterminateProgress: modelData.progress < 0 || modelData.progress > 100
1686+ }
1687+ }
1688+ }
1689+ }
1690+ }
1691+
1692+ CustomSectionHeader {
1693+ title: i18n.tr("Downloaded episodes")
1694+ visible: sortedEpisodeModel.count !== 0 || episodesModel.count !== 0
1695+ }
1696+ }
1697+
1698 section.property: "diff"
1699 section.labelPositioning: ViewSection.InlineLabels
1700-
1701- section.delegate: ListItem {
1702- height: headerText.title.text !== "" ? headerText.height + divider.height : units.gu(0)
1703- divider.anchors.leftMargin: units.gu(2)
1704- divider.anchors.rightMargin: units.gu(2)
1705-
1706- ListItemLayout {
1707- id: headerText
1708- title.text: {
1709- if (section === "Today") {
1710- return i18n.tr("Today")
1711- }
1712-
1713- else if (section === "Yesterday") {
1714- return i18n.tr("Yesterday")
1715- }
1716-
1717- else if (section === "Older") {
1718- return i18n.tr("Older")
1719- }
1720-
1721- else {
1722- return ""
1723- }
1724- }
1725- title.color: podbird.appTheme.baseText
1726- title.font.weight: Font.DemiBold
1727+ section.delegate: CustomSectionHeader {
1728+ title: {
1729+ if (section === "Today") {
1730+ return i18n.tr("Today")
1731+ }
1732+
1733+ else if (section === "Yesterday") {
1734+ return i18n.tr("Yesterday")
1735+ }
1736+
1737+ else if (section === "Older") {
1738+ return i18n.tr("Older")
1739+ }
1740+
1741+ else {
1742+ return ""
1743+ }
1744 }
1745 }
1746
1747@@ -380,8 +535,8 @@
1748 id: listItemLayout
1749
1750 title.text: model.name !== undefined ? model.name.trim() : "Undefined"
1751- title.color: currentGuid === model.guid || downloader.downloadingGuid === model.guid ? podbird.appTheme.focusText
1752- : podbird.appTheme.baseText
1753+ title.color: downloader.downloadingGuid === model.guid ? podbird.appTheme.focusText
1754+ : podbird.appTheme.baseText
1755 // #FIXME: Change this 2 to prevent title eliding when UITK is updated to rev > 1800
1756 title.maximumLineCount: 1
1757
1758@@ -439,7 +594,11 @@
1759 tx.executeSql("UPDATE Episode SET queued=1 WHERE guid = ?", [model.guid]);
1760 });
1761 episodesModel.setProperty(model.index, "queued", 1)
1762- downloader.addDownload(model.guid, model.audiourl);
1763+ if (model.audiourl) {
1764+ podbird.downloadEpisode(model.image, model.name, model.guid, model.audiourl)
1765+ } else {
1766+ console.log("[ERROR]: Invalid download url: " + model.audiourl)
1767+ }
1768 }
1769 }
1770 },
1771@@ -465,6 +624,14 @@
1772 },
1773
1774 Action {
1775+ iconName: "add-to-playlist"
1776+ onTriggered: {
1777+ var url = model.downloadedfile ? "file://" + model.downloadedfile : model.audiourl
1778+ player.addEpisodeToQueue(model.guid, model.image, model.name, model.artist, url)
1779+ }
1780+ },
1781+
1782+ Action {
1783 iconName: model.favourited ? "unlike" : "like"
1784 onTriggered: {
1785 var db = Podcasts.init();
1786@@ -496,21 +663,29 @@
1787
1788 onClicked: {
1789 Haptics.play()
1790- var db = Podcasts.init();
1791- db.transaction(function (tx) {
1792+ if (selectMode) {
1793+ selected = !selected
1794+ } else {
1795 if (currentGuid !== model.guid) {
1796- currentGuid = "";
1797- currentUrl = model.downloadedfile ? model.downloadedfile : model.audiourl;
1798- var rs = tx.executeSql("SELECT position FROM Episode WHERE guid=?", [model.guid]);
1799- playerLoader.item.play();
1800- playerLoader.item.seek(rs.rows.item(0).position);
1801- currentName = model.name;
1802- currentArtist = model.artist;
1803- currentImage = model.image;
1804- currentGuid = model.guid;
1805+ currentUrl = model.downloadedfile ? "file://" + model.downloadedfile : model.audiourl;
1806+ player.playEpisode(model.guid, model.image, model.name, model.artist, currentUrl)
1807 }
1808- });
1809- }
1810+ }
1811+ }
1812+
1813+ onPressAndHold: {
1814+ ListView.view.ViewItems.selectMode = !ListView.view.ViewItems.selectMode
1815+ }
1816+ }
1817+
1818+ onClearSelection: {
1819+ ViewItems.selectedIndices = []
1820+ }
1821+
1822+ onCloseSelection: {
1823+ clearSelection()
1824+ ViewItems.selectMode = false
1825+ episodesPage.header = standardHeader
1826 }
1827
1828 Scrollbar {
1829
1830=== modified file 'app/ui/NowPlayingPage.qml'
1831--- app/ui/NowPlayingPage.qml 2016-02-25 11:09:44 +0000
1832+++ app/ui/NowPlayingPage.qml 2016-03-28 22:39:33 +0000
1833@@ -17,8 +17,9 @@
1834 */
1835
1836 import QtQuick 2.4
1837-import QtMultimedia 5.4
1838+import QtMultimedia 5.6
1839 import Ubuntu.Components 1.3
1840+import QtQuick.LocalStorage 2.0
1841 import "../podcasts.js" as Podcasts
1842 import "../components"
1843
1844@@ -26,245 +27,349 @@
1845 id: nowPlayingPage
1846
1847 visible: false
1848- title: i18n.tr("Now Playing")
1849
1850 property bool isNowPlayingPage: true
1851- property bool isLandscapeMode: width > height
1852-
1853- // Landscape rule
1854- states: [
1855- State {
1856- name: "landscape"
1857- when: isLandscapeMode
1858-
1859- PropertyChanges {
1860- target: blurredBackground
1861- width: parent.width/2.2
1862- height: parent.height
1863- }
1864-
1865- AnchorChanges {
1866- target: blurredBackground
1867+
1868+ header: PageHeader {
1869+ title: i18n.tr("Now Playing")
1870+
1871+ StyleHints {
1872+ backgroundColor: podbird.appTheme.background
1873+ }
1874+
1875+ trailingActionBar.actions: Action {
1876+ iconName: "delete"
1877+ visible: nowPlayingPageSections.selectedIndex === 1
1878+ onTriggered: {
1879+ Podcasts.clearQueue()
1880+ player.playlist.clear()
1881+ mainStack.pop()
1882+ }
1883+ }
1884+
1885+ extension: Sections {
1886+ id: nowPlayingPageSections
1887+
1888+ anchors {
1889+ left: parent.left
1890+ bottom: parent.bottom
1891+ }
1892+
1893+ StyleHints {
1894+ selectedSectionColor: podbird.appTheme.focusText
1895+ }
1896+ model: [i18n.tr("Full view"), i18n.tr("Queue")]
1897+ }
1898+ }
1899+
1900+ VisualItemModel {
1901+ id: tabs
1902+
1903+ Item {
1904+ id: nowPlayingItem
1905+
1906+ width: tabView.width
1907+ height: tabView.height
1908+
1909+ property bool isLandscapeMode: nowPlayingPage.width > nowPlayingPage.height
1910+
1911+ // Landscape rule
1912+ states: [
1913+ State {
1914+ name: "landscape"
1915+ when: nowPlayingItem.isLandscapeMode
1916+
1917+ PropertyChanges {
1918+ target: blurredBackground
1919+ width: nowPlayingPage.width/2.2
1920+ height: nowPlayingPage.height
1921+ }
1922+
1923+ AnchorChanges {
1924+ target: blurredBackground
1925+ anchors {
1926+ top: nowPlayingItem.top
1927+ left: parent.left
1928+ right: undefined
1929+ }
1930+ }
1931+
1932+ AnchorChanges {
1933+ target: dataContainer
1934+ anchors {
1935+ top: nowPlayingItem.top
1936+ left: blurredBackground.right
1937+ right: parent.right
1938+ bottom: parent.bottom
1939+ }
1940+ }
1941+ }
1942+ ]
1943+
1944+ BlurredBackground {
1945+ id: blurredBackground
1946+
1947+ anchors.left: parent.left
1948+ anchors.top: nowPlayingItem.top
1949+ anchors.right: parent.right
1950+ height: title.lineCount === 1 ? nowPlayingPage.height/2.3 + units.gu(3)
1951+ : nowPlayingPage.height/2.3
1952+ art: currentImage
1953+
1954+ Image {
1955+ width: Math.min(nowPlayingPage.width/2, nowPlayingPage.height/2)
1956+ height: width
1957+ sourceSize.height: width
1958+ sourceSize.width: width
1959+ source: currentImage
1960+ asynchronous: true
1961+ anchors.centerIn: parent
1962+ }
1963+ }
1964+
1965+ Item {
1966+ id: dataContainer
1967+
1968 anchors {
1969- top: parent.top
1970+ top: blurredBackground.bottom
1971 left: parent.left
1972- right: undefined
1973- }
1974- }
1975-
1976- AnchorChanges {
1977- target: dataContainer
1978- anchors {
1979- top: parent.top
1980- left: blurredBackground.right
1981 right: parent.right
1982 bottom: parent.bottom
1983+ margins: units.gu(2)
1984+ bottomMargin: nowPlayingItem.isLandscapeMode ? units.gu(4) : units.gu(2)
1985+ }
1986+
1987+ Label {
1988+ id: title
1989+ anchors.left: parent.left
1990+ anchors.right: parent.right
1991+ anchors.top: parent.top
1992+ text: currentName
1993+ elide: Text.ElideRight
1994+ textSize: Label.Large
1995+ maximumLineCount: 2
1996+ wrapMode: Text.WordWrap
1997+ color: podbird.appTheme.baseText
1998+ }
1999+
2000+ Label {
2001+ id: artist
2002+ anchors.left: title.left
2003+ anchors.right: title.right
2004+ anchors.top: title.bottom
2005+ anchors.topMargin: units.gu(1)
2006+ text: currentArtist
2007+ elide: Text.ElideRight
2008+ textSize: Label.Small
2009+ color: podbird.appTheme.baseSubText
2010+ }
2011+
2012+ Slider {
2013+ id: scrubber
2014+
2015+ anchors {
2016+ left: parent.left
2017+ right: parent.right
2018+ bottom: controls.top
2019+ bottomMargin: nowPlayingItem.isLandscapeMode && title.lineCount < 2 ? units.gu(4) : units.gu(2)
2020+ }
2021+
2022+ live: true
2023+ minimumValue: 0
2024+ maximumValue: player.duration
2025+ value: player.position
2026+ height: units.gu(2)
2027+
2028+ onValueChanged: {
2029+ if (pressed) {
2030+ player.seek(value);
2031+ }
2032+ }
2033+
2034+ function formatValue(v) { return Podcasts.formatTime(v/1000); }
2035+ StyleHints { foregroundColor: podbird.appTheme.focusText }
2036+ }
2037+
2038+ Connections {
2039+ target: player
2040+ onPositionChanged: scrubber.value = player.position
2041+ }
2042+
2043+ Label {
2044+ id: startTime
2045+ textSize: Label.Small
2046+ anchors.left: scrubber.left
2047+ anchors.top: scrubber.bottom
2048+ color: podbird.appTheme.baseText
2049+ text: Podcasts.formatTime(player.position / 1000)
2050+ }
2051+
2052+ Label {
2053+ id: endTime
2054+ textSize: Label.Small
2055+ anchors.right: scrubber.right
2056+ anchors.top: scrubber.bottom
2057+ color: podbird.appTheme.baseText
2058+ text: Podcasts.formatTime(player.duration / 1000)
2059+ }
2060+
2061+ Row {
2062+ id: controls
2063+
2064+ anchors.bottom: parent.bottom
2065+ anchors.horizontalCenter: parent.horizontalCenter
2066+ spacing: units.gu(1)
2067+
2068+ AbstractButton {
2069+ id: mediaBackwardButton
2070+ width: units.gu(6)
2071+ height: width
2072+ anchors.verticalCenter: parent.verticalCenter
2073+ enabled: player.playlist.canGoPrevious
2074+ opacity: enabled ? 1.0 : 0.4
2075+ onClicked: player.playlist.previous()
2076+
2077+ Icon {
2078+ id: mediaBackwardIcon
2079+ width: units.gu(3)
2080+ height: width
2081+ anchors.centerIn: parent
2082+ color: podbird.appTheme.baseIcon
2083+ name: "media-skip-backward"
2084+ }
2085+ }
2086+
2087+ AbstractButton {
2088+ id: skipBackwardButton
2089+ width: units.gu(6)
2090+ height: width
2091+ anchors.verticalCenter: parent.verticalCenter
2092+ opacity: player.position === 0 ? 0.4 : 1.0
2093+ onClicked: {
2094+ if (player.position > 0) {
2095+ player.seek(player.position - podbird.settings.skipBack * 1000);
2096+ }
2097+ }
2098+
2099+ Row {
2100+ spacing: units.gu(1)
2101+ anchors.centerIn: parent
2102+
2103+ Label {
2104+ // TRANSLATORS: The string shown in the UI is -15s to denote the number of seconds that the podcast playback will skip backward.
2105+ // xgettext: no-c-format
2106+ text: i18n.tr("-%1s").arg(podbird.settings.skipBack)
2107+ textSize: Label.XxSmall
2108+ color: podbird.appTheme.baseText
2109+ anchors.verticalCenter: skipBackwardIcon.verticalCenter
2110+ }
2111+
2112+ Icon {
2113+ id: skipBackwardIcon
2114+ width: units.gu(3)
2115+ height: width
2116+ name: "media-seek-backward"
2117+ color: podbird.appTheme.baseIcon
2118+ }
2119+ }
2120+ }
2121+
2122+ AbstractButton {
2123+ id: playButton
2124+ width: units.gu(10)
2125+ height: width
2126+ opacity: playButton.pressed ? 0.4 : 1.0
2127+ onClicked: player.playbackState === MediaPlayer.PlayingState ? player.pause() : player.play()
2128+
2129+ Icon {
2130+ id: playIcon
2131+ width: units.gu(6)
2132+ height: width
2133+ anchors.centerIn: parent
2134+ color: podbird.appTheme.baseIcon
2135+ name: player.playbackState === MediaPlayer.PlayingState ? "media-playback-pause"
2136+ : "media-playback-start"
2137+ }
2138+ }
2139+
2140+ AbstractButton {
2141+ id: skipForwardButton
2142+ width: units.gu(6)
2143+ height: width
2144+ anchors.verticalCenter: parent.verticalCenter
2145+ opacity: player.position === 0 ? 0.4 : 1.0
2146+ onClicked: {
2147+ if (player.position > 0) {
2148+ player.seek(player.position + podbird.settings.skipForward * 1000);
2149+ }
2150+ }
2151+
2152+ Row {
2153+ spacing: units.gu(1)
2154+ anchors.centerIn: parent
2155+
2156+ Icon {
2157+ id: skipForwardIcon
2158+ width: units.gu(3)
2159+ height: width
2160+ name: "media-seek-forward"
2161+ color: podbird.appTheme.baseIcon
2162+ }
2163+
2164+ Label {
2165+ // TRANSLATORS: The string shown in the UI is +15s to denote the number of seconds that the podcast playback will skip forward.
2166+ // xgettext: no-c-format
2167+ text: i18n.tr("+%1s").arg(podbird.settings.skipForward)
2168+ textSize: Label.XxSmall
2169+ color: podbird.appTheme.baseText
2170+ anchors.verticalCenter: skipForwardIcon.verticalCenter
2171+ }
2172+ }
2173+ }
2174+
2175+ AbstractButton {
2176+ id: mediaForwardButton
2177+ width: units.gu(6)
2178+ height: width
2179+ anchors.verticalCenter: parent.verticalCenter
2180+ enabled: player.playlist.canGoNext
2181+ opacity: enabled ? 1.0 : 0.4
2182+ onClicked: player.playlist.next()
2183+
2184+ Icon {
2185+ id: mediaForwardIcon
2186+ width: units.gu(3)
2187+ height: width
2188+ anchors.centerIn: parent
2189+ color: podbird.appTheme.baseIcon
2190+ name: "media-skip-forward"
2191+ }
2192+ }
2193 }
2194 }
2195 }
2196- ]
2197-
2198- BlurredBackground {
2199- id: blurredBackground
2200-
2201- anchors.left: parent.left
2202- anchors.top: parent.top
2203- anchors.right: parent.right
2204- height: title.lineCount === 1 ? parent.height/2 + units.gu(3)
2205- : parent.height/2
2206- art: currentImage
2207-
2208- Image {
2209- width: Math.min(parent.width/2, parent.height)
2210- height: width
2211- sourceSize.height: width
2212- sourceSize.width: width
2213- source: currentImage
2214- asynchronous: true
2215- anchors.centerIn: parent
2216+
2217+ Queue {
2218+ width: tabView.width
2219+ height: tabView.height
2220 }
2221 }
2222
2223- Item {
2224- id: dataContainer
2225+ ListView {
2226+ id: tabView
2227+ model: tabs
2228+ interactive: false
2229
2230 anchors {
2231- top: blurredBackground.bottom
2232+ top: nowPlayingPage.header.bottom
2233 left: parent.left
2234 right: parent.right
2235 bottom: parent.bottom
2236- margins: units.gu(2)
2237- bottomMargin: isLandscapeMode ? units.gu(4) : units.gu(2)
2238- }
2239-
2240- Label {
2241- id: title
2242- anchors.left: parent.left
2243- anchors.right: parent.right
2244- anchors.top: parent.top
2245- text: currentName
2246- elide: Text.ElideRight
2247- textSize: Label.Large
2248- maximumLineCount: 2
2249- wrapMode: Text.WordWrap
2250- color: podbird.appTheme.baseText
2251- }
2252-
2253- Label {
2254- id: artist
2255- anchors.left: title.left
2256- anchors.right: title.right
2257- anchors.top: title.bottom
2258- anchors.topMargin: units.gu(1)
2259- text: currentArtist
2260- elide: Text.ElideRight
2261- textSize: Label.Small
2262- color: podbird.appTheme.baseSubText
2263- }
2264-
2265- Slider {
2266- id: scrubber
2267-
2268- anchors {
2269- left: parent.left
2270- right: parent.right
2271- bottom: controls.top
2272- bottomMargin: isLandscapeMode && title.lineCount < 2 ? units.gu(4) : units.gu(2)
2273- }
2274-
2275- live: true
2276- minimumValue: 0
2277- maximumValue: playerLoader.item.duration
2278- value: playerLoader.item.position
2279- height: units.gu(2)
2280-
2281- onValueChanged: {
2282- if (pressed) {
2283- playerLoader.item.seek(value);
2284- }
2285- }
2286-
2287- function formatValue(v) { return Podcasts.formatTime(v/1000); }
2288- StyleHints { foregroundColor: podbird.appTheme.focusText }
2289- }
2290-
2291- Connections {
2292- target: playerLoader.item
2293- onPositionChanged: scrubber.value = playerLoader.item.position
2294- }
2295-
2296- Label {
2297- id: startTime
2298- textSize: Label.Small
2299- anchors.left: scrubber.left
2300- anchors.top: scrubber.bottom
2301- color: podbird.appTheme.baseText
2302- text: Podcasts.formatTime(playerLoader.item.position / 1000)
2303- }
2304-
2305- Label {
2306- id: endTime
2307- textSize: Label.Small
2308- anchors.right: scrubber.right
2309- anchors.top: scrubber.bottom
2310- color: podbird.appTheme.baseText
2311- text: Podcasts.formatTime(playerLoader.item.duration / 1000)
2312- }
2313-
2314- Row {
2315- id: controls
2316-
2317- anchors.bottom: parent.bottom
2318- anchors.horizontalCenter: parent.horizontalCenter
2319- spacing: units.gu(2)
2320-
2321- AbstractButton {
2322- id: skipBackwardButton
2323- width: units.gu(6)
2324- height: width
2325- anchors.verticalCenter: parent.verticalCenter
2326- opacity: playerLoader.item.position === 0 ? 0.4 : 1.0
2327- onClicked: {
2328- if (playerLoader.item.position > 0) {
2329- playerLoader.item.seek(playerLoader.item.position - podbird.settings.skipBack * 1000);
2330- }
2331- }
2332-
2333- Row {
2334- spacing: units.gu(1)
2335- anchors.centerIn: parent
2336-
2337- Label {
2338- // TRANSLATORS: The string shown in the UI is -15s to denote the number of seconds that the podcast playback will skip backward.
2339- // xgettext: no-c-format
2340- text: i18n.tr("-%1s").arg(podbird.settings.skipBack)
2341- textSize: Label.XxSmall
2342- color: podbird.appTheme.baseText
2343- anchors.verticalCenter: skipBackwardIcon.verticalCenter
2344- }
2345-
2346- Icon {
2347- id: skipBackwardIcon
2348- width: units.gu(3)
2349- height: width
2350- name: "media-seek-backward"
2351- color: podbird.appTheme.baseIcon
2352- }
2353- }
2354- }
2355-
2356- AbstractButton {
2357- id: playButton
2358- width: units.gu(10)
2359- height: width
2360- opacity: playButton.pressed ? 0.4 : 1.0
2361- onClicked: playerLoader.item.playbackState === MediaPlayer.PlayingState ? playerLoader.item.pause() : playerLoader.item.play()
2362-
2363- Icon {
2364- id: playIcon
2365- width: units.gu(6)
2366- height: width
2367- anchors.centerIn: parent
2368- color: podbird.appTheme.baseIcon
2369- name: playerLoader.item.playbackState === MediaPlayer.PlayingState ? "media-playback-pause"
2370- : "media-playback-start"
2371- }
2372- }
2373-
2374- AbstractButton {
2375- id: skipForwardButton
2376- width: units.gu(6)
2377- height: width
2378- anchors.verticalCenter: parent.verticalCenter
2379- opacity: playerLoader.item.position === 0 ? 0.4 : 1.0
2380- onClicked: {
2381- if (playerLoader.item.position > 0) {
2382- playerLoader.item.seek(playerLoader.item.position + podbird.settings.skipForward * 1000);
2383- }
2384- }
2385-
2386- Row {
2387- spacing: units.gu(1)
2388- anchors.centerIn: parent
2389-
2390- Icon {
2391- id: skipForwardIcon
2392- width: units.gu(3)
2393- height: width
2394- name: "media-seek-forward"
2395- color: podbird.appTheme.baseIcon
2396- }
2397-
2398- Label {
2399- // TRANSLATORS: The string shown in the UI is +15s to denote the number of seconds that the podcast playback will skip forward.
2400- // xgettext: no-c-format
2401- text: i18n.tr("+%1s").arg(podbird.settings.skipForward)
2402- textSize: Label.XxSmall
2403- color: podbird.appTheme.baseText
2404- anchors.verticalCenter: skipForwardIcon.verticalCenter
2405- }
2406- }
2407- }
2408- }
2409+ }
2410+
2411+ orientation: Qt.Horizontal
2412+ snapMode: ListView.SnapOneItem
2413+ currentIndex: nowPlayingPageSections.selectedIndex
2414+ highlightMoveDuration: UbuntuAnimation.SlowDuration
2415 }
2416 }
2417
2418=== modified file 'app/ui/PlayerControls.qml'
2419--- app/ui/PlayerControls.qml 2016-02-25 11:09:44 +0000
2420+++ app/ui/PlayerControls.qml 2016-03-28 22:39:33 +0000
2421@@ -17,7 +17,7 @@
2422 */
2423
2424 import QtQuick 2.4
2425-import QtMultimedia 5.4
2426+import QtMultimedia 5.6
2427 import Ubuntu.Components 1.3
2428
2429 Rectangle {
2430@@ -49,7 +49,7 @@
2431 anchors.top: cover.bottom
2432 color: podbird.appTheme.focusText
2433 height: units.gu(0.25)
2434- width: playerLoader.item.duration > 0 ? (playerLoader.item.position / playerLoader.item.duration) * parent.width : 0
2435+ width: player.duration > 0 ? (player.position / player.duration) * parent.width : 0
2436 }
2437
2438 Column {
2439@@ -97,16 +97,16 @@
2440 visible: playButton.pressed
2441 }
2442
2443- onClicked: playerLoader.item.playbackState === MediaPlayer.PlayingState ? playerLoader.item.pause()
2444- : playerLoader.item.play()
2445+ onClicked: player.playbackState === MediaPlayer.PlayingState ? player.pause()
2446+ : player.play()
2447
2448 Icon {
2449 color: "white"
2450 width: units.gu(3)
2451 height: width
2452 anchors.centerIn: playButtonBackground
2453- name: playerLoader.item.playbackState === MediaPlayer.PlayingState ? "media-playback-pause"
2454- : "media-playback-start"
2455+ name: player.playbackState === MediaPlayer.PlayingState ? "media-playback-pause"
2456+ : "media-playback-start"
2457 opacity: playButton.pressed ? 0.4 : 1.0
2458 }
2459 }
2460
2461=== modified file 'app/ui/PodcastsTab.qml'
2462--- app/ui/PodcastsTab.qml 2016-03-28 22:39:32 +0000
2463+++ app/ui/PodcastsTab.qml 2016-03-28 22:39:33 +0000
2464@@ -17,10 +17,10 @@
2465 */
2466
2467 import QtQuick 2.4
2468-import QtMultimedia 5.4
2469+import QtMultimedia 5.6
2470 import QtQuick.LocalStorage 2.0
2471 import Ubuntu.Components 1.3
2472-import Ubuntu.DownloadManager 0.1
2473+import Ubuntu.DownloadManager 1.2
2474 import Ubuntu.Components.Popups 1.0
2475 import "../podcasts.js" as Podcasts
2476 import "../components"
2477@@ -166,12 +166,13 @@
2478 CardView {
2479 id: cardView
2480 clip: true
2481+ heightOffset: units.gu(4)
2482 model: sortedPodcastModel
2483 delegate: Card {
2484 id: albumCard
2485 coverArt: model.image !== undefined ? model.image : Qt.resolvedUrl("../graphics/podbird.png")
2486 primaryText: model.name !== undefined ? model.name.trim() : "Undefined"
2487- secondaryText: model.episodeCount > 0 ? i18n.tr("%1 unheard episode", "%1 unheard episodes", model.episodeCount).arg(model.episodeCount)
2488+ secondaryText: model.episodeCount > 0 ? model.episodeCount
2489 : ""
2490 onClicked: {
2491 if(podcastPage.header === searchHeader) {
2492
2493=== added file 'app/ui/Queue.qml'
2494--- app/ui/Queue.qml 1970-01-01 00:00:00 +0000
2495+++ app/ui/Queue.qml 2016-03-28 22:39:33 +0000
2496@@ -0,0 +1,88 @@
2497+/*
2498+ * Copyright 2016 Podbird Team
2499+ *
2500+ * This file is part of Podbird.
2501+ *
2502+ * Podbird is free software; you can redistribute it and/or modify
2503+ * it under the terms of the GNU General Public License as published by
2504+ * the Free Software Foundation; version 3.
2505+ *
2506+ * Podbird is distributed in the hope that it will be useful,
2507+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2508+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2509+ * GNU General Public License for more details.
2510+ *
2511+ * You should have received a copy of the GNU General Public License
2512+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2513+ */
2514+
2515+import QtQuick 2.4
2516+import Ubuntu.Components 1.3
2517+import "../podcasts.js" as Podcasts
2518+import "../components"
2519+
2520+Item {
2521+ id: queuePage
2522+
2523+ ListView {
2524+ id: queueList
2525+
2526+ anchors.fill: parent
2527+ model: player.playlist
2528+
2529+ delegate: ListItem {
2530+ id: listItem
2531+
2532+ height: layout.height
2533+ divider.visible: false
2534+
2535+ ListItemLayout {
2536+ id: layout
2537+
2538+ // Grab the metaData for the current index using its unique source url
2539+ property var metaModel: player.metaForSource(model.source)
2540+
2541+ Image {
2542+ id: imgFrame
2543+ width: units.gu(6)
2544+ height: width
2545+ source: Qt.resolvedUrl(layout.metaModel.image)
2546+ sourceSize.height: width
2547+ sourceSize.width: width
2548+ SlotsLayout.position: SlotsLayout.First
2549+ }
2550+
2551+ title.text: layout.metaModel.name
2552+ // #FIXME: Change this 2 to prevent title eliding when UITK is updated to rev > 1800
2553+ title.maximumLineCount: 1
2554+ title.color: player.playlist.currentIndex === index ? podbird.appTheme.focusText
2555+ : podbird.appTheme.baseText
2556+
2557+ subtitle.text: layout.metaModel.artist
2558+ subtitle.color: podbird.appTheme.baseSubText
2559+ }
2560+
2561+ leadingActions: ListItemActions {
2562+ actions: [
2563+ Action {
2564+ iconName: "delete"
2565+ onTriggered: {
2566+ player.playlist.removeItem(index)
2567+ var source = model.source
2568+ source = source.toString()
2569+ Podcasts.removeItemFromQueue(source)
2570+ }
2571+ }
2572+ ]
2573+ }
2574+
2575+ onClicked: {
2576+ if (player.playlist.currentIndex === index) {
2577+ player.toggle()
2578+ } else {
2579+ player.playlist.currentIndex = index
2580+ }
2581+ }
2582+ }
2583+ }
2584+}
2585
2586=== modified file 'po/com.mikeasoft.podbird.pot'
2587--- po/com.mikeasoft.podbird.pot 2016-03-28 22:39:32 +0000
2588+++ po/com.mikeasoft.podbird.pot 2016-03-28 22:39:33 +0000
2589@@ -8,7 +8,7 @@
2590 msgstr ""
2591 "Project-Id-Version: \n"
2592 "Report-Msgid-Bugs-To: \n"
2593-"POT-Creation-Date: 2016-03-19 07:47+0530\n"
2594+"POT-Creation-Date: 2016-03-19 19:13+0530\n"
2595 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
2596 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
2597 "Language-Team: LANGUAGE <LL@li.org>\n"
2598@@ -34,52 +34,42 @@
2599 msgid "Skip"
2600 msgstr ""
2601
2602-#. TRANSLATORS: this refers to a number of songs greater than one. The actual number will be prepended to the string automatically (plural forms are not yet fully supported in usermetrics, the library that displays that string)
2603-#: ../app/podbird.qml:175
2604-#, qt-format
2605-msgid "Podcasts listened to today: <b>%1</b>"
2606-msgstr ""
2607-
2608-#: ../app/podbird.qml:176
2609-msgid "No podcasts listened to today"
2610-msgstr ""
2611-
2612-#: ../app/podcasts.js:182
2613+#: ../app/podcasts.js:282
2614 #, no-c-format, qt-format
2615 msgid "%1 hr %2 min"
2616 msgstr ""
2617
2618-#: ../app/podcasts.js:191
2619+#: ../app/podcasts.js:291
2620 #, no-c-format, qt-format
2621 msgid "%1 hr"
2622 msgstr ""
2623
2624-#: ../app/podcasts.js:199
2625+#: ../app/podcasts.js:299
2626 #, no-c-format, qt-format
2627 msgid "%1 min"
2628 msgstr ""
2629
2630 #. TRANSLATORS: About as in information about the app
2631-#: ../app/settings/About.qml:28 ../app/settings/About.qml:47
2632+#: ../app/settings/About.qml:28 ../app/settings/About.qml:46
2633 #: ../app/ui/SettingsPage.qml:222
2634 msgid "About"
2635 msgstr ""
2636
2637-#: ../app/settings/About.qml:47
2638+#: ../app/settings/About.qml:46
2639 msgid "Credits"
2640 msgstr ""
2641
2642 #. TRANSLATORS: Podbird version number e.g Version 0.7
2643-#: ../app/settings/About.qml:93
2644+#: ../app/settings/About.qml:92
2645 #, qt-format
2646 msgid "Version %1"
2647 msgstr ""
2648
2649-#: ../app/settings/About.qml:114
2650+#: ../app/settings/About.qml:113
2651 msgid "Released under the terms of the GNU GPL v3"
2652 msgstr ""
2653
2654-#: ../app/settings/About.qml:124
2655+#: ../app/settings/About.qml:123
2656 #, qt-format
2657 msgid "Source code available on %1"
2658 msgstr ""
2659@@ -168,7 +158,7 @@
2660 msgid "Search Episode"
2661 msgstr ""
2662
2663-#: ../app/ui/EpisodesPage.qml:82 ../app/ui/EpisodesTab.qml:72
2664+#: ../app/ui/EpisodesPage.qml:82
2665 msgid "Mark all listened"
2666 msgstr ""
2667
2668@@ -177,7 +167,7 @@
2669 msgid "Unsubscribe"
2670 msgstr ""
2671
2672-#: ../app/ui/EpisodesPage.qml:136 ../app/ui/EpisodesTab.qml:173
2673+#: ../app/ui/EpisodesPage.qml:136 ../app/ui/EpisodesTab.qml:120
2674 msgid "Search episode"
2675 msgstr ""
2676
2677@@ -195,11 +185,11 @@
2678 msgid "Cancel"
2679 msgstr ""
2680
2681-#: ../app/ui/EpisodesPage.qml:223 ../app/ui/EpisodesTab.qml:295
2682+#: ../app/ui/EpisodesPage.qml:223 ../app/ui/EpisodesTab.qml:405
2683 msgid "Episode Description"
2684 msgstr ""
2685
2686-#: ../app/ui/EpisodesPage.qml:234 ../app/ui/EpisodesTab.qml:306
2687+#: ../app/ui/EpisodesPage.qml:234 ../app/ui/EpisodesTab.qml:416
2688 #: ../app/ui/SearchPage.qml:170
2689 msgid "Close"
2690 msgstr ""
2691@@ -220,82 +210,122 @@
2692 msgid "Listened"
2693 msgstr ""
2694
2695-#: ../app/ui/EpisodesPage.qml:413 ../app/ui/EpisodesTab.qml:135
2696+#: ../app/ui/EpisodesPage.qml:413
2697 msgid "Downloaded"
2698 msgstr ""
2699
2700-#: ../app/ui/EpisodesTab.qml:87
2701-msgid "Download all"
2702-msgstr ""
2703-
2704-#: ../app/ui/EpisodesTab.qml:104
2705-msgid "Delete all"
2706-msgstr ""
2707-
2708-#: ../app/ui/EpisodesTab.qml:135
2709+#: ../app/ui/EpisodesTab.qml:82
2710 msgid "Recent"
2711 msgstr ""
2712
2713-#: ../app/ui/EpisodesTab.qml:135
2714+#: ../app/ui/EpisodesTab.qml:82
2715+msgid "Downloads"
2716+msgstr ""
2717+
2718+#: ../app/ui/EpisodesTab.qml:82
2719 msgid "Favourites"
2720 msgstr ""
2721
2722-#: ../app/ui/EpisodesTab.qml:198
2723+#: ../app/ui/EpisodesTab.qml:141
2724+msgid "Back"
2725+msgstr ""
2726+
2727+#: ../app/ui/EpisodesTab.qml:153
2728+msgid "Mark Listened"
2729+msgstr ""
2730+
2731+#: ../app/ui/EpisodesTab.qml:171
2732+msgid "Download episode(s)"
2733+msgstr ""
2734+
2735+#: ../app/ui/EpisodesTab.qml:199
2736+msgid "Delete episode(s)"
2737+msgstr ""
2738+
2739+#: ../app/ui/EpisodesTab.qml:222
2740+msgid "Favourite episode(s)"
2741+msgstr ""
2742+
2743+#: ../app/ui/EpisodesTab.qml:245
2744+msgid "Unfavourite episode(s)"
2745+msgstr ""
2746+
2747+#: ../app/ui/EpisodesTab.qml:268
2748+msgid "Add to queue"
2749+msgstr ""
2750+
2751+#: ../app/ui/EpisodesTab.qml:308
2752 msgid "No New Episodes"
2753 msgstr ""
2754
2755-#: ../app/ui/EpisodesTab.qml:200
2756+#: ../app/ui/EpisodesTab.qml:310
2757 msgid "No Downloaded Episodes"
2758 msgstr ""
2759
2760-#: ../app/ui/EpisodesTab.qml:202
2761+#: ../app/ui/EpisodesTab.qml:312
2762 msgid "No Favourited Episodes"
2763 msgstr ""
2764
2765-#: ../app/ui/EpisodesTab.qml:204
2766+#: ../app/ui/EpisodesTab.qml:314
2767 msgid "No Episodes Found"
2768 msgstr ""
2769
2770-#: ../app/ui/EpisodesTab.qml:210
2771+#: ../app/ui/EpisodesTab.qml:320
2772 msgid "No more episodes to listen to!"
2773 msgstr ""
2774
2775-#: ../app/ui/EpisodesTab.qml:212
2776+#: ../app/ui/EpisodesTab.qml:322
2777 msgid "No episodes have been downloaded for offline listening"
2778 msgstr ""
2779
2780-#: ../app/ui/EpisodesTab.qml:214
2781+#: ../app/ui/EpisodesTab.qml:324
2782 msgid "No episodes have been favourited."
2783 msgstr ""
2784
2785-#: ../app/ui/EpisodesTab.qml:216
2786+#: ../app/ui/EpisodesTab.qml:326
2787 msgid "No Episodes found matching the search term."
2788 msgstr ""
2789
2790-#: ../app/ui/EpisodesTab.qml:347
2791+#: ../app/ui/EpisodesTab.qml:455
2792+msgid "Downloads in progress"
2793+msgstr ""
2794+
2795+#: ../app/ui/EpisodesTab.qml:495
2796+msgid "Downloaded episodes"
2797+msgstr ""
2798+
2799+#: ../app/ui/EpisodesTab.qml:505
2800 msgid "Today"
2801 msgstr ""
2802
2803-#: ../app/ui/EpisodesTab.qml:351
2804+#: ../app/ui/EpisodesTab.qml:509
2805 msgid "Yesterday"
2806 msgstr ""
2807
2808-#: ../app/ui/EpisodesTab.qml:355
2809+#: ../app/ui/EpisodesTab.qml:513
2810 msgid "Older"
2811 msgstr ""
2812
2813-#: ../app/ui/NowPlayingPage.qml:29
2814+#: ../app/ui/NowPlayingPage.qml:34
2815 msgid "Now Playing"
2816 msgstr ""
2817
2818+#: ../app/ui/NowPlayingPage.qml:61
2819+msgid "Full view"
2820+msgstr ""
2821+
2822+#: ../app/ui/NowPlayingPage.qml:61
2823+msgid "Queue"
2824+msgstr ""
2825+
2826 #. TRANSLATORS: The string shown in the UI is -15s to denote the number of seconds that the podcast playback will skip backward.
2827-#: ../app/ui/NowPlayingPage.qml:200
2828+#: ../app/ui/NowPlayingPage.qml:261
2829 #, no-c-format, qt-format
2830 msgid "-%1s"
2831 msgstr ""
2832
2833 #. TRANSLATORS: The string shown in the UI is +15s to denote the number of seconds that the podcast playback will skip forward.
2834-#: ../app/ui/NowPlayingPage.qml:261
2835+#: ../app/ui/NowPlayingPage.qml:322
2836 #, no-c-format, qt-format
2837 msgid "+%1s"
2838 msgstr ""
2839@@ -331,7 +361,7 @@
2840 msgid "No podcasts found matching the search term."
2841 msgstr ""
2842
2843-#: ../app/ui/PodcastsTab.qml:174 ../app/ui/PodcastsTab.qml:221
2844+#: ../app/ui/PodcastsTab.qml:222
2845 #, qt-format
2846 msgid "%1 unheard episode"
2847 msgid_plural "%1 unheard episodes"
2848@@ -529,10 +559,10 @@
2849 msgid "Finish"
2850 msgstr ""
2851
2852-#: /home/krnekhelesh/Development/devel-branch-sync-1-build/po/Podbird.desktop.in.h:1
2853+#: /home/krnekhelesh/Development/devel-branch-sync-2-build/po/Podbird.desktop.in.h:1
2854 msgid "The chirpiest podcast manager for Ubuntu"
2855 msgstr ""
2856
2857-#: /home/krnekhelesh/Development/devel-branch-sync-1-build/po/Podbird.desktop.in.h:2
2858+#: /home/krnekhelesh/Development/devel-branch-sync-2-build/po/Podbird.desktop.in.h:2
2859 msgid "podcast;audio;itunes;broadcast;digital;stream;podcatcher;video;vodcast;"
2860 msgstr ""

Subscribers

People subscribed via source and target branches