git.iko.soy/maciek134/podphoenix
on main help sign in
Add queue support. Use DownloadManager to handle download queues and unfinished downloads when app is closed. Switch to new GridView. Add multi-select for Episodes tab.
commit1f570cdf2d4baf353e9025f38c4c15188e10643e authorNekhelesh Ramananthan <krnekhelesh@gmail.com> dateTue 29 Mar 2016, 00:19 (11 years ago) parent5ca91384dc936b
14 files changed+1117−1053
app/components/Card.qml+54−109 app/components/CardView.qml+14−19 app/components/ColumnFlow.qml−494 app/components/CustomSectionHeader.qml+47 app/podbird.qml+133−62 app/podcasts.js+102−2 app/settings/About.qml−1 app/ui/EpisodesPage.qml+21−19 app/ui/EpisodesTab.qml+269−94 app/ui/NowPlayingPage.qml+300−195 app/ui/PlayerControls.qml+6−6 app/ui/PodcastsTab.qml+4−3 app/ui/Queue.qml+88 po/com.mikeasoft.podbird.pot+79−49
app/components/Card.qml+54−109
@@ -1,12 +1,13 @@
11/*
2− * Copyright (C) 2014-2016
3− * Andrew Hayzen <ahayzen@gmail.com>
2+ * Copyright 2016 Podbird Team
43 *
5− * This program is free software; you can redistribute it and/or modify
4+ * This file is part of Podbird.
5+ *
6+ * Podbird is free software; you can redistribute it and/or modify
67 * it under the terms of the GNU General Public License as published by
78 * the Free Software Foundation; version 3.
89 *
9− * This program is distributed in the hope that it will be useful,
10+ * Podbird is distributed in the hope that it will be useful,
1011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
1112 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1213 * GNU General Public License for more details.
@@ -18,122 +19,66 @@
1819import QtQuick 2.4
1920import Ubuntu.Components 1.3
2021
21−Item {
22+AbstractButton {
2223 id: card
2324
24− /* Required by ColumnFlow */
25− property int index
26− property var model
25+ height: parent.parent.cellHeight
26+ width: parent.parent.cellWidth
2727
2828 property alias coverArt: imgFrame.source
2929 property alias primaryText: primaryLabel.text
30− property alias secondaryText: secondaryLabel.text
31− property alias secondaryTextVisible: secondaryLabel.visible
32−
33− signal clicked(var mouse)
34− signal pressAndHold(var mouse)
35−
36− height: cardColumn.childrenRect.height + 2 * bg.anchors.margins
37−
38− /* Background for card */
39− Rectangle {
40− id: bg
41− anchors.fill: parent
42− anchors.margins: units.gu(1)
43− color: podbird.appTheme.hightlightListView
44− }
45−
46− /* Column containing image and labels */
47− Column {
48− id: cardColumn
49−
50− anchors.fill: bg
51− spacing: units.gu(0.5)
52−
53− Image {
54− id: imgFrame
55− width: parent.width
56− height: width
57− sourceSize.height: width
58− sourceSize.width: width
59− }
60−
61− Item {
62− height: units.gu(1)
63− width: units.gu(1)
64− }
65−
66− Label {
67− id: primaryLabel
68− anchors {
69− left: parent.left
70− right: parent.right
71− margins: units.gu(1)
72− }
73− color: podbird.appTheme.baseText
74− elide: Text.ElideRight
75− textSize: Label.Small
76− opacity: 1.0
77− wrapMode: Text.WordWrap
78− horizontalAlignment: Text.AlignHCenter
30+ property string secondaryText: ""
31+
32+ Image {
33+ id: imgFrame
34+ width: parent.width/1.2
35+ height: width
36+ anchors.top: parent.top
37+ anchors.horizontalCenter: parent.horizontalCenter
38+ sourceSize.height: width
39+ sourceSize.width: width
40+
41+ Loader {
42+ id: hintLoader
43+ anchors.verticalCenter: parent.top
44+ anchors.right: parent.right
45+ anchors.rightMargin: units.gu(-0.5)
46+ sourceComponent: secondaryText !== "" ? hintComponent : undefined
7947 }
8048
81− Label {
82− id: secondaryLabel
83− anchors {
84− left: parent.left
85− leftMargin: units.gu(1)
86− right: parent.right
87− rightMargin: units.gu(1)
49+ Component {
50+ id: hintComponent
51+ Rectangle {
52+ color: podbird.appTheme.focusText
53+ width: secondaryLabel.implicitWidth + units.gu(1)
54+ height: secondaryLabel.implicitHeight + units.gu(1)
55+ radius: units.gu(0.5)
56+ visible: secondaryLabel.text !== ""
57+ Label {
58+ id: secondaryLabel
59+ anchors.centerIn: parent
60+ text: secondaryText
61+ visible: text !== ""
62+ textSize: Label.Small
63+ color: "White"
64+ }
8865 }
89− color: podbird.appTheme.baseSubText
90− elide: Text.ElideRight
91− textSize: Label.Small
92− opacity: 1.0
93− wrapMode: Text.WordWrap
94− horizontalAlignment: Text.AlignHCenter
95− }
96−
97− Item {
98− height: units.gu(1.5)
99− width: units.gu(1)
10066 }
10167 }
10268
103− /* Overlay for when card is pressed */
104− Rectangle {
105− id: overlay
106− anchors.fill: bg
107− color: "#000"
108− opacity: 0
109−
110− Behavior on opacity {
111− UbuntuNumberAnimation {}
69+ Label {
70+ id: primaryLabel
71+ anchors {
72+ top: imgFrame.bottom
73+ left: imgFrame.left
74+ right: imgFrame.right
75+ margins: units.gu(1)
11276 }
113− }
114−
115− /* Capture mouse events */
116− MouseArea {
117− anchors.fill: parent
118− onClicked: card.clicked(mouse)
119− onPressAndHold: card.pressAndHold(mouse)
120− onPressedChanged: overlay.opacity = pressed ? 0.3 : 0
121− }
122−
123− /* Animations */
124− Behavior on height {
125− UbuntuNumberAnimation {}
126− }
127−
128− Behavior on width {
129− UbuntuNumberAnimation {}
130− }
131−
132− Behavior on x {
133− UbuntuNumberAnimation {}
134− }
135−
136− Behavior on y {
137− UbuntuNumberAnimation {}
77+ color: podbird.appTheme.baseText
78+ elide: Text.ElideRight
79+ textSize: Label.Small
80+ wrapMode: Text.WordWrap
81+ maximumLineCount: 2
82+ horizontalAlignment: Text.AlignHCenter
13883 }
13984}
app/components/CardView.qml+14−19
@@ -18,33 +18,28 @@
1818import QtQuick 2.4
1919import Ubuntu.Components 1.3
2020
21−Flickable {
22− id: cardViewFlickable
21+GridView {
22+ id: gridView
23+
2324 anchors {
2425 fill: parent
2526 margins: units.gu(1)
2627 }
2728
28− // dont use flow.contentHeight as it is inaccurate due to height of labels
29− // changing as they load
30− contentHeight: flow.contentHeight + flow.anchors.margins * 2 + units.gu(8)
31− contentWidth: width
32−
33− property alias count: flow.count
34− property alias delegate: flow.delegate
35− property var getter
36− property alias model: flow.model
37− property real itemWidth: units.gu(15)
29+ cellHeight: cellSize + heightOffset
30+ cellWidth: cellSize + widthOffset
3831
39− onGetterChanged: flow.getter = getter // cannot use alias to set a function (must be var)
40−
41− ColumnFlow {
42− id: flow
43− anchors.fill: parent
44− columns: parseInt(cardViewFlickable.width / itemWidth) || 1 // never drop to 0
45− flickable: cardViewFlickable
32+ header: Item {
33+ width: parent.width
34+ height: units.gu(2)
4635 }
4736
37+ readonly property int columns: parseInt(width / itemWidth) || 1 // never drop to 0
38+ readonly property int cellSize: width / columns
39+ property int itemWidth: units.gu(15)
40+ property int heightOffset: 0
41+ property int widthOffset: 0
42+
4843 Component.onCompleted: {
4944 // FIXME: workaround for qtubuntu not returning values depending on the grid unit definition
5045 // for Flickable.maximumFlickVelocity and Flickable.flickDeceleration
app/components/ColumnFlow.qmldeleted−494
@@ -1,494 +0,0 @@
1−/*
2− * Copyright (C) 2014-2016
3− * Andrew Hayzen <ahayzen@gmail.com>
4− *
5− * This program is free software; you can redistribute it and/or modify
6− * it under the terms of the GNU General Public License as published by
7− * the Free Software Foundation; version 3.
8− *
9− * This program is distributed in the hope that it will be useful,
10− * but WITHOUT ANY WARRANTY; without even the implied warranty of
11− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12− * GNU General Public License for more details.
13− *
14− * You should have received a copy of the GNU General Public License
15− * along with this program. If not, see <http://www.gnu.org/licenses/>.
16− */
17−
18−import QtQuick 2.4
19−
20−Item {
21− id: columnFlow
22− property int columns: 1
23− property Flickable flickable
24− property var model
25− property Component delegate
26−
27− property var getter: function (i) { return model.get(i); } // optional getter override (useful for music-app ms2 models)
28−
29− property int buffer: units.gu(20)
30− property var columnHeights: []
31− property var columnHeightsMax: []
32− property int columnWidth: parent.width / columns
33− property int contentHeight: 0
34− property int count: model === undefined ? 0 : model.count
35− property int delayRebuildIndex: -1
36− property var incubating: ({}) // incubating objects
37− property var items: ({})
38− property var itemToColumn: ({}) // cache of the columns of indexes
39− property int lastIndex: 0 // the furtherest index loaded
40− property bool removing: false
41− property bool restoring: false // is the view restoring?
42− property var restoreItems: ({}) // when rebuilding items are stored here temporarily
43−
44− onColumnWidthChanged: {
45− if (restoring) {
46− return;
47− } else if (columns != columnHeights.length && visible) {
48− // number of columns has changed so rebuild the columns
49− rebuildColumns()
50− } else { // column width has changed update visible items properties linked to columnWidth
51− for (var column=0; column < columnHeights.length; column++) {
52− for (var i in columnHeights[column]) {
53− if (columnHeights[column].hasOwnProperty(i) && items.hasOwnProperty(i)) {
54− items[i].width = columnWidth;
55− items[i].x = column * columnWidth;
56− }
57− }
58− }
59−
60− ensureItemsVisible()
61− }
62− }
63−
64− onVisibleChanged: {
65− if (visible && delayRebuildIndex !== -1) { // restore from count change
66− if (delayRebuildIndex === 0) {
67− reset()
68− } else {
69− removeIndex(delayRebuildIndex)
70− }
71−
72− delayRebuildIndex = -1
73− append(true)
74− }
75−
76− // number of columns has changed while invisible so reset if not already restoring
77− if (visible && !restoring && columns != columnHeights.length) {
78− rebuildColumns()
79− }
80− }
81−
82− ListModel { // fakemodel for connections to link to when there is no model
83− id: fakeModel
84− }
85−
86− Connections {
87− target: model === undefined ? fakeModel : model
88− onModelReset: {
89− if (!visible && lastIndex > 0) {
90− delayRebuildIndex = 0
91− } else {
92− reset()
93− append()
94− }
95− }
96− onRowsInserted: {
97− if (!visible && lastIndex > 0) {
98− setDelayRebuildIndex(first)
99− } else {
100− if (first <= lastIndex) {
101− if (first === 0) {
102− reset()
103− } else {
104− removeIndex(first) // remove earliest index and all items after
105− }
106− }
107−
108− // Supply last index if larger as count is not updated until after insertion
109− append(true, last > count ? last : count)
110− }
111− }
112− onRowsRemoved: {
113− if (!visible) {
114− setDelayRebuildIndex(first)
115− } else {
116− if (first <= lastIndex) {
117− if (first === 0) {
118− reset()
119− } else {
120− removeIndex(first) // remove earliest index and all items after
121− }
122−
123− // count is not updated until after removal, so send insertMax
124− // insertMax is count - removal region inclusive - 1 (lastIndex is 1 infront)
125−
126− append(true, count - (1 + last - first) - 1) // rebuild any items on screen or before
127− }
128− }
129− }
130− }
131−
132−
133− Connections {
134− target: flickable
135− onContentYChanged: {
136− append() // Append any new items (scrolling down)
137−
138− ensureItemsVisible()
139− }
140− }
141−
142− // Append a new row of items if possible
143− function append(loadBefore, insertMax)
144− {
145− // Do not allow append to run if incubating
146− if (isIncubating() || restoring || removing) {
147− return;
148− }
149−
150− // get the columns in order
151− var columnsByHeight = getColumnsByHeight();
152− var workDone = false;
153−
154− // check if a new item in each column is possible
155− for (var i=0; i < columnsByHeight.length; i++) {
156− var y = columnHeightsMax[columnsByHeight[i]];
157−
158− // build new object in column if possible
159− // if insertMax is undefined then allow if there is work todo (from the count in the model)
160− // otherwise use the insertMax as the count to compare with the lastIndex added to the columnFlow
161− // and
162− // allow if the y position is within the viewport
163− // or if loadBefore is true then allow if the y position is before the viewport
164− if (((count > 0 && lastIndex < count && insertMax === undefined) || (insertMax !== undefined && lastIndex <= insertMax)) && (inViewport(y, 0) || (loadBefore === true && beforeViewport(y)))) {
165− incubateObject(lastIndex++, columnsByHeight[i], getMaxInColumn(columnsByHeight[i]), append);
166− workDone = true
167− } else {
168− break;
169− }
170− }
171−
172− if (!workDone) { // last iteration over append so visible ensure items are correct
173− ensureItemsVisible();
174− }
175− }
176−
177− // Detect if a loaded object is before the viewport with a buffer
178− function beforeViewport(y)
179− {
180− return y <= flickable.contentY - buffer;
181− }
182−
183− // Cache the size of the columns for use later
184− function cacheColumnHeights()
185− {
186− columnHeightsMax = [];
187−
188− for (var i=0; i < columnHeights.length; i++) {
189− var sum = 0;
190−
191− for (var j in columnHeights[i]) {
192− sum += columnHeights[i][j];
193− }
194−
195− columnHeightsMax.push(sum);
196− }
197−
198− if (!restoring) { // when not restoring otherwise user will be pushed to the top of the view
199− // set the height of columnFlow to max column (for flickable contentHeight)
200− contentHeight = Math.max.apply(null, columnHeightsMax);
201− }
202− }
203−
204− // Recache the visible items heights (due to a change in their height)
205− function cacheVisibleItemsHeights()
206− {
207− for (var i in items) {
208− if (items.hasOwnProperty(i)) {
209− columnHeights[itemToColumn[i]][i] = items[i].height;
210− }
211− }
212−
213− cacheColumnHeights();
214− }
215−
216− // Ensures that the correct items are visible
217− function ensureItemsVisible()
218− {
219− for (var i in items) {
220− if (items.hasOwnProperty(i)) {
221− items[i].visible = inViewport(items[i].y, items[i].height)
222− }
223− }
224− }
225−
226− // Return if there are incubating objects
227− function isIncubating()
228− {
229− for (var i in incubating) {
230− if (incubating.hasOwnProperty(i)) {
231− return true;
232− }
233− }
234−
235− return false;
236− }
237−
238− // Run after incubation to store new column height and call any further append/restores
239− function finishIncubation(index, callback)
240− {
241− var obj = incubating[index].object;
242− delete incubating[index];
243−
244− obj.heightChanged.connect(cacheVisibleItemsHeights) // if the height changes recache
245−
246− // Ensure properties linked to columnWidth are correct (as width may still be changing)
247− obj.x = itemToColumn[index] * columnWidth;
248− obj.width = columnWidth;
249−
250− items[index] = obj;
251−
252− columnHeights[itemToColumn[index]][index] = obj.height; // ensure height is the latest
253−
254− if (!isIncubating()) {
255− cacheColumnHeights();
256−
257− // Check if there is any more work to be done (append or restore)
258− callback();
259− }
260− }
261−
262− // Force any incubation to finish
263− function forceIncubationCompletion()
264− {
265− for (var i in incubating) {
266− if (incubating.hasOwnProperty(i)) {
267− incubating[i].forceCompletion()
268− }
269− }
270− }
271−
272− // Get the column index in order of height
273− function getColumnsByHeight()
274− {
275− var columnsByHeight = [];
276−
277− for (var i=0; i < columnHeightsMax.length; i++) {
278− var min = undefined;
279− var index = -1;
280−
281− // Find the smallest column that has not been found yet
282− for (var j=0; j < columnHeightsMax.length; j++) {
283− if (columnsByHeight.indexOf(j) === -1 && (min === undefined || columnHeightsMax[j] < min)) {
284− min = columnHeightsMax[j];
285− index = j;
286− }
287− }
288−
289− columnsByHeight.push(index);
290− }
291−
292− return columnsByHeight;
293− }
294−
295− // Get the highest index for a column
296− function getMaxInColumn(column)
297− {
298− var max;
299−
300− for (var i in columnHeights[column]) {
301− if (columnHeights[column].hasOwnProperty(i)) {
302− i = parseInt(i);
303−
304− if (items.hasOwnProperty(i)) {
305− if (i > max || max === undefined) {
306− max = i;
307− }
308− }
309− }
310− }
311−
312− return max;
313− }
314−
315− // Incubate an object for creation
316− function incubateObject(index, column, anchorIndex, callback)
317− {
318− // Load parameters to send to the object on creation
319− var params = {
320− "anchors.top": anchorIndex === undefined ? parent.top : items[anchorIndex].bottom,
321− index: index,
322− model: getter(index),
323− width: columnWidth,
324− x: column * columnWidth
325− };
326−
327− // Start incubating and cache the column
328− incubating[index] = delegate.incubateObject(parent, params);
329− itemToColumn[index] = column;
330−
331− if (incubating[index].status != Component.Ready) {
332− incubating[index].onStatusChanged = function(status) {
333− if (status == Component.Ready) {
334− finishIncubation(index, callback)
335− }
336− }
337− } else {
338− finishIncubation(index, callback)
339− }
340− }
341−
342− // Detect if a loaded object is in the viewport with a buffer
343− function inViewport(y, height)
344− {
345− return flickable.contentY - buffer < y + height && y < flickable.contentY + flickable.height + buffer;
346− }
347−
348− // Number of columns has changed rebuild with live items
349− function rebuildColumns()
350− {
351− restoring = true;
352− var i;
353−
354− forceIncubationCompletion()
355−
356− columnHeights = []
357− columnHeightsMax = []
358−
359− for (i=0; i < columns; i++) {
360− columnHeights.push({});
361− columnHeightsMax.push(0);
362− }
363−
364− lastIndex = 0;
365−
366− restoreItems = items;
367− items = {};
368−
369− restoreExisting()
370−
371− restoring = false;
372−
373− cacheColumnHeights(); // rebuilds contentHeight
374−
375− // If the columns have changed while the view was locked rerun
376− if (columns != columnHeights.length && visible) {
377− rebuildColumns()
378− } else {
379− append() // check if any new items can be added
380− }
381− }
382−
383− // Remove an index from the model (invalidating anything after)
384− function removeIndex(index)
385− {
386− removing = true
387−
388− forceIncubationCompletion()
389−
390− for (var i in items) {
391− if (i >= index && items.hasOwnProperty(i)) {
392− delete columnHeights[itemToColumn[i]][i]
393− delete itemToColumn[i]
394−
395− items[i].destroy()
396− delete items[i]
397− }
398− }
399−
400− lastIndex = index
401− removing = false
402−
403− cacheColumnHeights()
404− }
405−
406− // Restores existing items into potentially new positions
407− function restoreExisting()
408− {
409− var i;
410−
411− // get the columns in order
412− var columnsByHeight = getColumnsByHeight();
413− var workDone = false;
414−
415− // check if a new item in each column is possible
416− for (i=0; i < columnsByHeight.length; i++) {
417− var column = columnsByHeight[i];
418−
419− // build new object in column if possible
420− if (count > 0 && lastIndex < count) {
421− if (restoreItems.hasOwnProperty(lastIndex)) {
422− var item = restoreItems[lastIndex];
423− var maxInColumn = getMaxInColumn(column); // get lowest item in column
424−
425− itemToColumn[lastIndex] = column;
426− columnHeights[column][lastIndex] = item.height; // ensure height is the latest
427−
428− // Rebuild item properties
429− item.anchors.bottom = undefined
430− item.anchors.top = maxInColumn === undefined ? parent.top : items[maxInColumn].bottom;
431− item.x = column * columnWidth;
432− item.visible = inViewport(item.y, item.height);
433−
434− // Migrate item from restoreItems to items
435− items[lastIndex] = item;
436− delete restoreItems[lastIndex];
437−
438− // set after restore as height will likely change causing cacheVisibleItemsHeights to be run
439− item.width = columnWidth;
440−
441− cacheColumnHeights(); // ensure column heights are up to date
442−
443− lastIndex++;
444− workDone = true;
445− }
446− } else {
447− break;
448− }
449− }
450−
451− if (workDone) {
452− restoreExisting() // if work done then check if any more is needed
453− } else {
454− restoreItems = {}; // ensure restoreItems is empty
455− }
456− }
457−
458− // Reset the column flow
459− function reset()
460− {
461− forceIncubationCompletion()
462−
463− // Destroy any old items
464− for (var j in items) {
465− if (items.hasOwnProperty(j)) {
466− items[j].destroy()
467− }
468− }
469−
470− // Reset and rebuild the variables
471− items = ({})
472− itemToColumn = ({})
473− lastIndex = 0
474−
475− columnHeights = []
476−
477− for (var k=0; k < columns; k++) {
478− columnHeights.push({})
479− }
480−
481− cacheColumnHeights()
482−
483− contentHeight = 0
484− }
485−
486− function setDelayRebuildIndex(index)
487− {
488− if (delayRebuildIndex === -1 || index < lastIndex) {
489− delayRebuildIndex = index
490− }
491− }
492−
493− Component.onCompleted: append(true)
494−}
app/components/CustomSectionHeader.qmlnew file+47
@@ -0,0 +1,47 @@
1+/*
2+ * Copyright 2016 Podbird Team
3+ *
4+ * This file is part of Podbird.
5+ *
6+ * Podbird is free software; you can redistribute it and/or modify
7+ * it under the terms of the GNU General Public License as published by
8+ * the Free Software Foundation; version 3.
9+ *
10+ * Podbird is distributed in the hope that it will be useful,
11+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
12+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+ * GNU General Public License for more details.
14+ *
15+ * You should have received a copy of the GNU General Public License
16+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
17+ */
18+
19+import QtQuick 2.4
20+import Ubuntu.Components 1.3
21+
22+Item {
23+ id: customSectionHeader
24+
25+ property alias title: headerText.text
26+
27+ height: headerText.text !== "" ? headerText.implicitHeight + divider.height + headerText.anchors.topMargin + headerText.anchors.bottomMargin
28+ : units.gu(0)
29+
30+ anchors { left: parent.left; right: parent.right; margins: units.gu(2) }
31+
32+ Label {
33+ id: headerText
34+ color: podbird.appTheme.baseText
35+ font.weight: Font.DemiBold
36+ anchors { top: parent.top; topMargin: units.gu(2); bottom: parent.bottom; bottomMargin: units.gu(2) }
37+ width: parent.width
38+ }
39+
40+ Rectangle {
41+ id: divider
42+ color: settings.themeName === "Dark.qml" ? "#888888" : "#cdcdcd"
43+ width: parent.width
44+ height: units.dp(1)
45+ anchors.bottom: parent.bottom
46+ }
47+}
app/podbird.qml+133−62
@@ -18,13 +18,12 @@
1818
1919import QtQuick 2.4
2020import Podbird 1.0
21−import UserMetrics 0.1
22−import QtMultimedia 5.4
21+import QtMultimedia 5.6
2322import Ubuntu.Connectivity 1.0
2423import Qt.labs.settings 1.0
2524import Ubuntu.Components 1.3
2625import QtQuick.LocalStorage 2.0
27−import Ubuntu.DownloadManager 0.1
26+import Ubuntu.DownloadManager 1.2
2827import "ui"
2928import "themes" as Themes
3029import "podcasts.js" as Podcasts
@@ -45,11 +44,11 @@ MainView {
4544
4645 Component.onDestruction: {
4746 console.log("[LOG]: Download cancelled");
48− downloader.cancel();
4947 var db = Podcasts.init()
5048 db.transaction(function (tx) {
5149 tx.executeSql('UPDATE Episode SET queued=0 WHERE queued=1');
5250 })
51+ Podcasts.clearQueue()
5352 }
5453
5554 // RefreshModel function to call refreshModel() function of the tab currently
@@ -138,84 +137,156 @@ MainView {
138137 }
139138 }
140139
141− SingleDownload {
140+ Component {
141+ id: singleDownloadComponent
142+ SingleDownload {
143+ id: singleDownloadObject
144+ property string image
145+ property string title
146+ property string guid
147+ metadata: Metadata {
148+ showInIndicator: true
149+ title: singleDownloadObject.title
150+ }
151+ }
152+ }
153+
154+ function downloadEpisode(image, title, guid, url) {
155+ var singleDownload = singleDownloadComponent.createObject(podbird, {"image": image, "title": title, "guid": guid})
156+ singleDownload.download(url)
157+ }
158+
159+ DownloadManager {
142160 id: downloader
143− property var queue: []
144− property string downloadingGuid
145161
146− onFinished: {
162+ property string downloadingGuid: downloads.length > 0 ? downloads[0].guid : "NULL"
163+ property int progress: downloads.length > 0 ? downloads[0].progress : 0
164+
165+ cleanDownloads: true
166+ onDownloadFinished: {
147167 var db = Podcasts.init();
148168 var finalLocation = fileManager.saveDownload(path);
149169 db.transaction(function (tx) {
150− tx.executeSql("UPDATE Episode SET downloadedfile=?, queued=0 WHERE guid=?", [finalLocation, downloadingGuid]);
151− queue.shift();
152− if (queue.length > 0) {
153− downloadingGuid = queue[0][0];
154− download(queue[0][1]);
155− } else {
156− downloadingGuid = "";
157− }
170+ tx.executeSql("UPDATE Episode SET downloadedfile=?, queued=0 WHERE guid=?", [finalLocation, download.guid]);
158171 });
159172 }
160173
161− function addDownload(guid, url) {
162− queue.push([guid, url]);
163− if (queue.length == 1) {
164− downloadingGuid = guid;
165− download(url);
166− }
174+ onErrorFound: {
175+ console.log("[ERROR]: " + download.errorMessage)
167176 }
168177 }
169178
170− // UserMetrics to show Podbird stats on welcome screen
171− Metric {
172− id: podcastsMetric
173− name: "podcast-metrics"
174− // 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)
175− format: i18n.tr("Podcasts listened to today: <b>%1</b>")
176− emptyFormat: i18n.tr("No podcasts listened to today")
177− domain: "com.mikeasoft.podbird"
178− }
179+ MediaPlayer {
180+ id: player
181+
182+ // Wrapper function around decodeURIComponent() to prevent exceptions
183+ // from bubbling up to the app.
184+ function decodeFileURI(filename)
185+ {
186+ var newFilename = "";
187+ try {
188+ newFilename = decodeURIComponent(filename);
189+ } catch (e) {
190+ newFilename = filename;
191+ console.log("Unicode decoding error:", filename, e.message)
192+ }
179193
180− // Load the media player only when the user starts to play some media. This
181− // should improve app-startup slightly.
182− Loader {
183− id: playerLoader
184− sourceComponent: currentUrl != "" ? playerComponent : undefined
185− }
194+ return newFilename;
195+ }
186196
187− Component {
188− id: playerComponent
189− MediaPlayer {
190− id: player
197+ function metaForSource(source) {
198+ var blankMeta = {
199+ name: "",
200+ artist: "",
201+ image: "",
202+ guid: "",
203+ }
204+
205+ source = source.toString()
191206
192− property bool podcastCounted: false
207+ return Podcasts.lookup(decodeFileURI(source)) || blankMeta;
208+ }
209+
210+ function toggle() {
211+ if (playbackState === MediaPlayer.PlayingState) {
212+ pause()
213+ } else {
214+ play()
215+ }
216+ }
193217
194− source: currentUrl
218+ function playEpisode(guid, image, name, artist, url) {
219+ // Clear current queue
220+ player.playlist.clear()
221+ Podcasts.clearQueue()
195222
196− onSourceChanged: {
197− podcastCounted = false
223+ // Add episode to queue
224+ Podcasts.addItemToQueue(guid, image, name, artist, url)
225+ player.playlist.addItem(url)
226+
227+ // Play episode
228+ player.play()
229+ }
230+
231+ function addEpisodeToQueue(guid, image, name, artist, url) {
232+ Podcasts.addItemToQueue(guid, image, name, artist, url)
233+ player.playlist.addItem(url)
234+
235+ // If added episode is the first one in the queue, then set the current metadata
236+ // so that the bottom player controls will be shown, allowing the user to play
237+ // the episode if he chooses to.
238+ if (player.playlist.itemCount === 0) {
239+ currentGuid = guid
240+ currentName = name
241+ currentArtist = artist
242+ currentImage = image
243+ currentUrl = url
198244 }
245+ }
199246
200− onPositionChanged: {
201− if (currentGuid == "" || duration <= 0) {
202− return;
203− }
247+ property bool endOfMedia: false
248+ property double progress: 0
204249
205− if (position > 10000 && !podcastCounted) {
206− podcastCounted = true
207− podcastsMetric.increment()
208− console.log("[LOG]: Podcast User metric incremented")
250+ playlist: Playlist {
251+ playbackMode: Playlist.Sequential
252+
253+ readonly property bool canGoPrevious: currentIndex !== 0
254+ readonly property bool canGoNext: currentIndex !== itemCount - 1
255+
256+ onCurrentItemSourceChanged: {
257+ var meta = player.metaForSource(currentItemSource)
258+ currentGuid = "";
259+ currentName = meta.name
260+ currentArtist = meta.artist
261+ currentImage = meta.image
262+ currentGuid = meta.guid
263+ }
264+ }
265+
266+ onStatusChanged: {
267+ if (status === MediaPlayer.EndOfMedia) {
268+ console.log("[LOG]: End of Media. Stopping.")
269+ endOfMedia = true
270+ stop()
271+ }
272+ }
273+
274+ onStopped: {
275+ if (playlist.itemCount > 0) {
276+ if (endOfMedia) {
277+ // We just ended media, so jump to start of playlist
278+ playlist.currentIndex = 0;
279+
280+ // Play then pause otherwise when we come from EndOfMedia
281+ // it calls next() until EndOfMedia again.
282+ play()
209283 }
210284
211− var db = Podcasts.init();
212− db.transaction(function (tx) {
213− tx.executeSql("UPDATE Episode SET position=? WHERE guid=?", [position >= duration ? 120 : position, currentGuid]);
214− if (position >= duration - 120) {
215− tx.executeSql("UPDATE Episode SET listened = 1 WHERE guid=?", [currentGuid]);
216− }
217− });
285+ pause()
218286 }
287+
288+ // Always reset endOfMedia
289+ endOfMedia = false
219290 }
220291 }
221292
@@ -290,13 +361,13 @@ MainView {
290361 states: [
291362 State {
292363 name: "shown"
293− when: currentUrl != "" && !mainStack.currentPage.isNowPlayingPage
364+ when: player.playlist.itemCount !== 0 && !mainStack.currentPage.isNowPlayingPage
294365 PropertyChanges { target: playerControlLoader; anchors.bottomMargin: 0 }
295366 },
296367
297368 State {
298369 name: "hidden"
299− when: currentUrl == "" || mainStack.currentPage.isNowPlayingPage || !playerControl.visible
370+ when: player.playlist.itemCount === 0 || mainStack.currentPage.isNowPlayingPage || !playerControl.visible
300371 PropertyChanges { target: playerControlLoader; anchors.bottomMargin: -units.gu(7) }
301372 }
302373 ]
app/podcasts.js+102−2
@@ -22,6 +22,7 @@ function init() {
2222 db.transaction(function(tx) {
2323 tx.executeSql('CREATE TABLE IF NOT EXISTS Podcast(artist TEXT, name TEXT, description TEXT, feed TEXT, image TEXT, lastupdate TIMESTAMP)');
2424 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))');
25+ tx.executeSql('CREATE TABLE IF NOT EXISTS Queue(ind INTEGER NOT NULL, guid TEXT, image TEXT, name TEXT, artist TEXT, url TEXT)');
2526 });
2627
2728 try {
@@ -55,6 +56,105 @@ function init() {
5556 return db;
5657}
5758
59+// Function to add item to queue
60+function addItemToQueue(guid, image, name, artist, url) {
61+ var db = init()
62+
63+ db.transaction(function(tx) {
64+ var ind = getNextIndex(tx);
65+ var rs = tx.executeSql("INSERT OR REPLACE INTO Queue (ind, guid, image, name, artist, url) VALUES (?, ?, ?, ?, ?, ?)", [ind, guid, image, name, artist, url]);
66+ if (rs.rowsAffected > 0) {
67+ console.log("[LOG]: QUEUE add OK")
68+ console.log("[LOG]: URL Added to queue: " + url)
69+ } else {
70+ console.log("[LOG]: QUEUE add FAIL")
71+ }
72+ });
73+}
74+
75+function removeItemFromQueue(source) {
76+ var db = init()
77+
78+ db.transaction(function(tx) {
79+ // Remove selected source from the queue
80+ tx.executeSql("DELETE FROM Queue WHERE url = ?", source)
81+
82+ // Rebuild queue in order
83+ var rs = tx.executeSql("SELECT ind FROM Queue ORDER BY ind ASC")
84+
85+ for (var i=0; i<rs.rows.length; i++) {
86+ tx.executeSql("UPDATE Queue SET ind = ? WHERE ind = ?", [i, rs.rows.item(i).ind])
87+ }
88+ })
89+}
90+
91+// Function to clear the queue
92+function clearQueue() {
93+ var db = init();
94+ db.transaction(function(tx) {
95+ tx.executeSql("DELETE FROM Queue");
96+ });
97+}
98+
99+function lookup(source) {
100+ var db = init();
101+ var meta = {
102+ name: "",
103+ artist: "",
104+ image: "",
105+ guid: "",
106+ }
107+
108+ db.transaction(function(tx) {
109+ var rs = tx.executeSql("SELECT * FROM Queue ORDER BY ind ASC");
110+ for(var i = 0; i < rs.rows.length; i++) {
111+ var episode = rs.rows.item(i);
112+ if (source === episode.url) {
113+ meta.name = episode.name
114+ meta.artist = episode.artist
115+ meta.image = episode.image
116+ meta.guid = episode.guid
117+ break
118+ }
119+ }
120+ });
121+
122+ return meta
123+}
124+
125+// Function to get the next index for the queue
126+function getNextIndex(tx) {
127+ var ind;
128+
129+ if (tx === undefined) {
130+ var db = init();
131+ db.transaction(function(tx) {
132+ ind = getNextIndex(tx);
133+ });
134+ } else {
135+ var rs = tx.executeSql('SELECT MAX(ind) FROM Queue')
136+ ind = isQueueEmpty(tx) ? 0 : rs.rows.item(0)["MAX(ind)"] + 1
137+ }
138+
139+ return ind;
140+}
141+
142+function isQueueEmpty(tx) {
143+ var empty = false;
144+
145+ if (tx === undefined) {
146+ var db = init();
147+ db.transaction( function(tx) {
148+ empty = isQueueEmpty(tx)
149+ });
150+ } else {
151+ var rs = tx.executeSql("SELECT count(*) as value FROM Queue")
152+ empty = rs.rows.item(0).value === 0
153+ }
154+
155+ return empty
156+}
157+
58158function subscribe(artist, name, feed, img) {
59159 var db = init();
60160 db.transaction(function(tx) {
@@ -259,8 +359,8 @@ function autoDownloadEpisodes(maxEpisodeDownload) {
259359 var rs2 = tx.executeSql("SELECT rowid, * FROM Episode WHERE podcast=? ORDER BY published DESC", [rs.rows.item(i).rowid]);
260360 var loopCount = maxEpisodeDownload > rs2.rows.length ? rs2.rows.length : maxEpisodeDownload
261361 for (var j=0; j < loopCount; j++) {
262− if (!rs2.rows.item(j).downloadedfile && !rs2.rows.item(j).listened) {
263− downloader.addDownload(rs2.rows.item(j).guid, rs2.rows.item(j).audiourl)
362+ if (!rs2.rows.item(j).downloadedfile && !rs2.rows.item(j).listened && rs2.rows.item(j).audiourl) {
363+ podbird.downloadEpisode(rs.rows.item(i).image, rs2.rows.item(j).name, rs2.rows.item(j).guid, rs2.rows.item(j).audiourl)
264364 tx.executeSql("UPDATE Episode SET queued=1 WHERE guid = ?", [rs2.rows.item(j).guid]);
265365 }
266366 }
app/settings/About.qml−1
@@ -36,7 +36,6 @@ Page {
3636
3737 anchors {
3838 left: parent.left
39− leftMargin: units.gu(2)
4039 bottom: parent.bottom
4140 }
4241
app/ui/EpisodesPage.qml+21−19
@@ -17,10 +17,10 @@
1717 */
1818
1919import QtQuick 2.4
20−import QtMultimedia 5.4
20+import QtMultimedia 5.6
2121import Ubuntu.Components 1.3
2222import QtQuick.LocalStorage 2.0
23−import Ubuntu.DownloadManager 0.1
23+import Ubuntu.DownloadManager 1.2
2424import Ubuntu.Components.Popups 1.3
2525import "../podcasts.js" as Podcasts
2626import "../components"
@@ -442,8 +442,8 @@ Page {
442442 id: listItemLayout
443443
444444 title.text: model.name !== undefined ? model.name.trim() : "Undefined"
445− title.color: currentGuid === model.guid || downloader.downloadingGuid === model.guid ? podbird.appTheme.focusText
446− : podbird.appTheme.baseText
445+ title.color: downloader.downloadingGuid === model.guid ? podbird.appTheme.focusText
446+ : podbird.appTheme.baseText
447447 // #FIXME: Change this 2 to prevent title eliding when UITK is updated to rev > 1800
448448 title.maximumLineCount: 1
449449
@@ -505,11 +505,23 @@ Page {
505505 tx.executeSql("UPDATE Episode SET queued=1 WHERE guid = ?", [model.guid]);
506506 });
507507 episodeModel.setProperty(model.index, "queued", 1)
508− downloader.addDownload(model.guid, model.audiourl);
508+ if (model.audiourl) {
509+ podbird.downloadEpisode(model.image, model.name, model.guid, model.audiourl)
510+ } else {
511+ console.log("[ERROR]: Invalid download url: " + model.audiourl)
512+ }
509513 }
510514 }
511515 },
512516
517+ Action {
518+ iconName: "add-to-playlist"
519+ onTriggered: {
520+ var url = model.downloadedfile ? "file://" + model.downloadedfile : model.audiourl
521+ player.addEpisodeToQueue(model.guid, model.image, model.name, model.artist, url)
522+ }
523+ },
524+
513525 Action {
514526 iconName: model.favourited ? "unlike" : "like"
515527 onTriggered: {
@@ -536,20 +548,10 @@ Page {
536548
537549 onClicked: {
538550 Haptics.play()
539− var db = Podcasts.init();
540− db.transaction(function (tx) {
541− if (currentGuid !== model.guid) {
542− currentGuid = "";
543− currentUrl = model.downloadedfile ? model.downloadedfile : model.audiourl;
544− var rs = tx.executeSql("SELECT position FROM Episode WHERE guid=?", [model.guid]);
545− playerLoader.item.play();
546− playerLoader.item.seek(rs.rows.item(0).position);
547− currentName = model.name;
548− currentArtist = model.artist;
549− currentImage = model.image;
550− currentGuid = model.guid;
551− }
552− });
551+ if (currentGuid !== model.guid) {
552+ currentUrl = model.downloadedfile ? "file://" + model.downloadedfile : model.audiourl;
553+ player.playEpisode(model.guid, model.image, model.name, model.artist, currentUrl)
554+ }
553555 }
554556 }
555557
app/ui/EpisodesTab.qml+269−94
@@ -17,10 +17,10 @@
1717 */
1818
1919import QtQuick 2.4
20−import QtMultimedia 5.4
20+import QtMultimedia 5.6
2121import Ubuntu.Components 1.3
2222import QtQuick.LocalStorage 2.0
23−import Ubuntu.DownloadManager 0.1
23+import Ubuntu.DownloadManager 1.2
2424import Ubuntu.Components.Popups 1.3
2525import "../podcasts.js" as Podcasts
2626import "../components"
@@ -64,58 +64,6 @@ Tab {
6464 episodesPage.header = searchHeader
6565 searchField.item.forceActiveFocus()
6666 }
67− },
68−
69− Action {
70− iconName: "select"
71− visible: episodesPageHeaderSections.selectedIndex === 0
72− text: i18n.tr("Mark all listened")
73− onTriggered: {
74− var db = Podcasts.init();
75− db.transaction(function (tx) {
76− for (var i=0; i<episodesModel.count; i++) {
77− tx.executeSql("UPDATE Episode SET listened=1 WHERE guid=?", [episodesModel.get(i).guid]);
78− }
79− episodesModel.clear()
80− });
81− }
82− },
83−
84− Action {
85− iconName: "save"
86− visible: episodesPageHeaderSections.selectedIndex === 0
87− text: i18n.tr("Download all")
88− onTriggered: {
89− var db = Podcasts.init();
90− db.transaction(function (tx) {
91− for (var i=0; i<episodesModel.count; i++) {
92− if (!episodesModel.get(i).downloadedfile) {
93− episodesModel.setProperty(i, "queued", 1)
94− tx.executeSql("UPDATE Episode SET queued=1 WHERE guid = ?", [episodesModel.get(i).guid]);
95− downloader.addDownload(episodesModel.get(i).guid, episodesModel.get(i).audiourl);
96− }
97− }
98− });
99− }
100− },
101−
102− Action {
103− iconName: "delete"
104− text: i18n.tr("Delete all")
105− visible: episodesPageHeaderSections.selectedIndex === 1
106− onTriggered: {
107− var db = Podcasts.init();
108− db.transaction(function (tx) {
109− for (var i=0; i<episodesModel.count; i++) {
110− if (episodesModel.get(i).downloadedfile) {
111− fileManager.deleteFile(episodesModel.get(i).downloadedfile);
112− tx.executeSql("UPDATE Episode SET downloadedfile = NULL WHERE guid = ?", [episodesModel.get(i).guid]);
113− episodesModel.setProperty(i, "downloadedfile", "")
114− }
115− }
116− });
117− refreshModel();
118− }
11967 }
12068 ]
12169
@@ -124,7 +72,6 @@ Tab {
12472
12573 anchors {
12674 left: parent.left
127− leftMargin: units.gu(2)
12875 bottom: parent.bottom
12976 }
13077
@@ -132,7 +79,7 @@ Tab {
13279 selectedSectionColor: podbird.appTheme.focusText
13380 }
13481
135− model: [i18n.tr("Recent"), i18n.tr("Downloaded"), i18n.tr("Favourites")]
82+ model: [i18n.tr("Recent"), i18n.tr("Downloads"), i18n.tr("Favourites")]
13683 onSelectedIndexChanged: {
13784 refreshModel();
13885 }
@@ -174,6 +121,169 @@ Tab {
174121 }
175122 }
176123
124+ PageHeader {
125+ id: selectionHeader
126+ visible: episodeList.ViewItems.selectMode
127+
128+ onVisibleChanged: {
129+ if (visible) {
130+ episodesPage.header = selectionHeader
131+ }
132+ }
133+
134+ StyleHints {
135+ backgroundColor: podbird.appTheme.background
136+ }
137+
138+ leadingActionBar.actions: [
139+ Action {
140+ iconName: "back"
141+ text: i18n.tr("Back")
142+ onTriggered: {
143+ episodeList.closeSelection()
144+ }
145+ }
146+ ]
147+
148+ trailingActionBar {
149+ numberOfSlots: 6
150+ actions: [
151+ Action {
152+ iconName: "select"
153+ text: i18n.tr("Mark Listened")
154+ enabled: episodeList.ViewItems.selectedIndices.length !== 0
155+ onTriggered: {
156+ var db = Podcasts.init();
157+ db.transaction(function (tx) {
158+ for (var i=0; i<episodeList.ViewItems.selectedIndices.length; i++) {
159+ var index = episodeList.ViewItems.selectedIndices[i]
160+ tx.executeSql("UPDATE Episode SET listened=1 WHERE guid=?", [episodesModel.get(index).guid]);
161+ }
162+ });
163+
164+ refreshModel();
165+ episodeList.closeSelection()
166+ }
167+ },
168+
169+ Action {
170+ iconName: "save"
171+ text: i18n.tr("Download episode(s)")
172+ enabled: episodeList.ViewItems.selectedIndices.length !== 0
173+ visible: episodesPageHeaderSections.selectedIndex !== 1
174+
175+ onTriggered: {
176+ var db = Podcasts.init();
177+ db.transaction(function (tx) {
178+ for (var i=0; i<episodeList.ViewItems.selectedIndices.length; i++) {
179+ var index = episodeList.ViewItems.selectedIndices[i]
180+ if (!episodesModel.get(index).downloadedfile) {
181+ episodesModel.setProperty(index, "queued", 1)
182+ tx.executeSql("UPDATE Episode SET queued=1 WHERE guid = ?", [episodesModel.get(index).guid]);
183+ if (episodesModel.get(index).audiourl) {
184+ podbird.downloadEpisode(episodesModel.get(index).image, episodesModel.get(index).name, episodesModel.get(index).guid, episodesModel.get(index).audiourl)
185+ } else {
186+ console.log("[ERROR]: Invalid download url: " + episodesModel.get(index).audiourl)
187+ }
188+ }
189+ }
190+ });
191+
192+ refreshModel();
193+ episodeList.closeSelection()
194+ }
195+ },
196+
197+ Action {
198+ iconName: "delete"
199+ text: i18n.tr("Delete episode(s)")
200+ enabled: episodeList.ViewItems.selectedIndices.length !== 0
201+
202+ onTriggered: {
203+ var db = Podcasts.init();
204+ db.transaction(function (tx) {
205+ for (var i=0; i<episodeList.ViewItems.selectedIndices.length; i++) {
206+ var index = episodeList.ViewItems.selectedIndices[i]
207+ if (episodesModel.get(index).downloadedfile) {
208+ fileManager.deleteFile(episodesModel.get(index).downloadedfile);
209+ tx.executeSql("UPDATE Episode SET downloadedfile = NULL WHERE guid = ?", [episodesModel.get(index).guid]);
210+ episodesModel.setProperty(index, "downloadedfile", "")
211+ }
212+ }
213+ });
214+
215+ refreshModel();
216+ episodeList.closeSelection()
217+ }
218+ },
219+
220+ Action {
221+ iconName: "like"
222+ text: i18n.tr("Favourite episode(s)")
223+ visible: episodesPageHeaderSections.selectedIndex !== 2
224+ enabled: episodeList.ViewItems.selectedIndices.length !== 0
225+
226+ onTriggered: {
227+ var db = Podcasts.init();
228+ db.transaction(function (tx) {
229+ for (var i=0; i<episodeList.ViewItems.selectedIndices.length; i++) {
230+ var index = episodeList.ViewItems.selectedIndices[i]
231+ if (!episodesModel.get(index).favourited) {
232+ tx.executeSql("UPDATE Episode SET favourited=1 WHERE guid=?", [episodesModel.get(index).guid])
233+ episodesModel.setProperty(index, "favourited", 1)
234+ }
235+ }
236+ });
237+
238+ refreshModel();
239+ episodeList.closeSelection()
240+ }
241+ },
242+
243+ Action {
244+ iconName: "unlike"
245+ text: i18n.tr("Unfavourite episode(s)")
246+ visible: episodesPageHeaderSections.selectedIndex === 2
247+ enabled: episodeList.ViewItems.selectedIndices.length !== 0
248+
249+ onTriggered: {
250+ var db = Podcasts.init();
251+ db.transaction(function (tx) {
252+ for (var i=0; i<episodeList.ViewItems.selectedIndices.length; i++) {
253+ var index = episodeList.ViewItems.selectedIndices[i]
254+ if (episodesModel.get(index).favourited) {
255+ tx.executeSql("UPDATE Episode SET favourited=0 WHERE guid=?", [episodesModel.get(index).guid])
256+ episodesModel.setProperty(index, "favourited", 0)
257+ }
258+ }
259+ });
260+
261+ refreshModel();
262+ episodeList.closeSelection()
263+ }
264+ },
265+
266+ Action {
267+ iconName: "add-to-playlist"
268+ text: i18n.tr("Add to queue")
269+ enabled: episodeList.ViewItems.selectedIndices.length !== 0
270+
271+ onTriggered: {
272+ for (var i=0; i<episodeList.ViewItems.selectedIndices.length; i++) {
273+ var index = episodeList.ViewItems.selectedIndices[i]
274+ if (episodesModel.get(index).audiourl) {
275+ var url = episodesModel.get(index).downloadedfile ? "file://" + episodesModel.get(index).downloadedfile : episodesModel.get(index).audiourl
276+ player.addEpisodeToQueue(episodesModel.get(index).guid, episodesModel.get(index).image, episodesModel.get(index).name, episodesModel.get(index).artist, url)
277+ }
278+ }
279+
280+ episodeList.closeSelection()
281+ }
282+ }
283+ ]
284+ }
285+ }
286+
177287 Loader {
178288 id: emptyState
179289
@@ -185,7 +295,7 @@ Tab {
185295 verticalCenterOffset: Qt.inputMethod.visible ? units.gu(4) : 0
186296 }
187297
188− sourceComponent: episodesModel.count === 0 || sortedEpisodeModel.count === 0 ? emptyStateComponent : undefined
298+ sourceComponent: (episodesModel.count === 0 || sortedEpisodeModel.count === 0) && downloader.downloads.length === 0 ? emptyStateComponent : undefined
189299 }
190300
191301 Component {
@@ -315,6 +425,9 @@ Tab {
315425 ListView {
316426 id: episodeList
317427
428+ signal clearSelection()
429+ signal closeSelection()
430+
318431 Component.onCompleted: {
319432 // FIXME: workaround for qtubuntu not returning values depending on the grid unit definition
320433 // for Flickable.maximumFlickVelocity and Flickable.flickDeceleration
@@ -332,35 +445,77 @@ Tab {
332445
333446 clip: true
334447 model: sortedEpisodeModel
335− section.property: "diff"
336− section.labelPositioning: ViewSection.InlineLabels
337448
338− section.delegate: ListItem {
339− height: headerText.title.text !== "" ? headerText.height + divider.height : units.gu(0)
340− divider.anchors.leftMargin: units.gu(2)
341− divider.anchors.rightMargin: units.gu(2)
449+ header: Column {
450+ width: episodeList.width
451+ visible: height !== 0
452+ height: downloader.downloads.length > 0 && episodesPageHeaderSections.selectedIndex === 1 ? childrenRect.height : 0
342453
343− ListItemLayout {
344− id: headerText
345− title.text: {
346− if (section === "Today") {
347− return i18n.tr("Today")
348− }
454+ CustomSectionHeader {
455+ title: i18n.tr("Downloads in progress")
456+ }
349457
350− else if (section === "Yesterday") {
351− return i18n.tr("Yesterday")
352− }
458+ Repeater {
459+ model: downloader.downloads
460+ delegate: ListItem {
461+ divider.visible: false
462+ height: inProgressLayout.height
463+ SlotsLayout {
464+ id: inProgressLayout
465+
466+ Image {
467+ height: width
468+ width: units.gu(6)
469+ source: modelData.image !== undefined ? modelData.image : Qt.resolvedUrl("../graphics/podbird.png")
470+ SlotsLayout.position: SlotsLayout.Leading
471+ sourceSize { width: width; height: height }
472+ }
353473
354− else if (section === "Older") {
355− return i18n.tr("Older")
356− }
474+ mainSlot: Column {
475+ spacing: units.gu(0.5)
476+
477+ Label {
478+ text: modelData.title
479+ width: parent.width
480+ elide: Text.ElideRight
481+ }
357482
358− else {
359− return ""
483+ CustomProgressBar {
484+ width: parent.width
485+ height: modelData.progress > 0 ? units.dp(5) : 0
486+ progress: modelData.progress
487+ indeterminateProgress: modelData.progress < 0 || modelData.progress > 100
488+ }
489+ }
360490 }
361491 }
362− title.color: podbird.appTheme.baseText
363− title.font.weight: Font.DemiBold
492+ }
493+
494+ CustomSectionHeader {
495+ title: i18n.tr("Downloaded episodes")
496+ visible: sortedEpisodeModel.count !== 0 || episodesModel.count !== 0
497+ }
498+ }
499+
500+ section.property: "diff"
501+ section.labelPositioning: ViewSection.InlineLabels
502+ section.delegate: CustomSectionHeader {
503+ title: {
504+ if (section === "Today") {
505+ return i18n.tr("Today")
506+ }
507+
508+ else if (section === "Yesterday") {
509+ return i18n.tr("Yesterday")
510+ }
511+
512+ else if (section === "Older") {
513+ return i18n.tr("Older")
514+ }
515+
516+ else {
517+ return ""
518+ }
364519 }
365520 }
366521
@@ -380,8 +535,8 @@ Tab {
380535 id: listItemLayout
381536
382537 title.text: model.name !== undefined ? model.name.trim() : "Undefined"
383− title.color: currentGuid === model.guid || downloader.downloadingGuid === model.guid ? podbird.appTheme.focusText
384− : podbird.appTheme.baseText
538+ title.color: downloader.downloadingGuid === model.guid ? podbird.appTheme.focusText
539+ : podbird.appTheme.baseText
385540 // #FIXME: Change this 2 to prevent title eliding when UITK is updated to rev > 1800
386541 title.maximumLineCount: 1
387542
@@ -439,7 +594,11 @@ Tab {
439594 tx.executeSql("UPDATE Episode SET queued=1 WHERE guid = ?", [model.guid]);
440595 });
441596 episodesModel.setProperty(model.index, "queued", 1)
442− downloader.addDownload(model.guid, model.audiourl);
597+ if (model.audiourl) {
598+ podbird.downloadEpisode(model.image, model.name, model.guid, model.audiourl)
599+ } else {
600+ console.log("[ERROR]: Invalid download url: " + model.audiourl)
601+ }
443602 }
444603 }
445604 },
@@ -464,6 +623,14 @@ Tab {
464623 }
465624 },
466625
626+ Action {
627+ iconName: "add-to-playlist"
628+ onTriggered: {
629+ var url = model.downloadedfile ? "file://" + model.downloadedfile : model.audiourl
630+ player.addEpisodeToQueue(model.guid, model.image, model.name, model.artist, url)
631+ }
632+ },
633+
467634 Action {
468635 iconName: model.favourited ? "unlike" : "like"
469636 onTriggered: {
@@ -496,21 +663,29 @@ Tab {
496663
497664 onClicked: {
498665 Haptics.play()
499− var db = Podcasts.init();
500− db.transaction(function (tx) {
666+ if (selectMode) {
667+ selected = !selected
668+ } else {
501669 if (currentGuid !== model.guid) {
502− currentGuid = "";
503− currentUrl = model.downloadedfile ? model.downloadedfile : model.audiourl;
504− var rs = tx.executeSql("SELECT position FROM Episode WHERE guid=?", [model.guid]);
505− playerLoader.item.play();
506− playerLoader.item.seek(rs.rows.item(0).position);
507− currentName = model.name;
508− currentArtist = model.artist;
509− currentImage = model.image;
510− currentGuid = model.guid;
670+ currentUrl = model.downloadedfile ? "file://" + model.downloadedfile : model.audiourl;
671+ player.playEpisode(model.guid, model.image, model.name, model.artist, currentUrl)
511672 }
512− });
673+ }
513674 }
675+
676+ onPressAndHold: {
677+ ListView.view.ViewItems.selectMode = !ListView.view.ViewItems.selectMode
678+ }
679+ }
680+
681+ onClearSelection: {
682+ ViewItems.selectedIndices = []
683+ }
684+
685+ onCloseSelection: {
686+ clearSelection()
687+ ViewItems.selectMode = false
688+ episodesPage.header = standardHeader
514689 }
515690
516691 Scrollbar {
app/ui/NowPlayingPage.qml+300−195
@@ -17,8 +17,9 @@
1717 */
1818
1919import QtQuick 2.4
20−import QtMultimedia 5.4
20+import QtMultimedia 5.6
2121import Ubuntu.Components 1.3
22+import QtQuick.LocalStorage 2.0
2223import "../podcasts.js" as Podcasts
2324import "../components"
2425
@@ -26,245 +27,349 @@ Page {
2627 id: nowPlayingPage
2728
2829 visible: false
29− title: i18n.tr("Now Playing")
3030
3131 property bool isNowPlayingPage: true
32− property bool isLandscapeMode: width > height
33−
34− // Landscape rule
35− states: [
36− State {
37− name: "landscape"
38− when: isLandscapeMode
39−
40− PropertyChanges {
41− target: blurredBackground
42− width: parent.width/2.2
43− height: parent.height
32+
33+ header: PageHeader {
34+ title: i18n.tr("Now Playing")
35+
36+ StyleHints {
37+ backgroundColor: podbird.appTheme.background
38+ }
39+
40+ trailingActionBar.actions: Action {
41+ iconName: "delete"
42+ visible: nowPlayingPageSections.selectedIndex === 1
43+ onTriggered: {
44+ Podcasts.clearQueue()
45+ player.playlist.clear()
46+ mainStack.pop()
4447 }
48+ }
4549
46− AnchorChanges {
47− target: blurredBackground
48− anchors {
49− top: parent.top
50− left: parent.left
51− right: undefined
52− }
50+ extension: Sections {
51+ id: nowPlayingPageSections
52+
53+ anchors {
54+ left: parent.left
55+ bottom: parent.bottom
5356 }
5457
55− AnchorChanges {
56− target: dataContainer
57− anchors {
58− top: parent.top
59− left: blurredBackground.right
60− right: parent.right
61− bottom: parent.bottom
62− }
58+ StyleHints {
59+ selectedSectionColor: podbird.appTheme.focusText
6360 }
64− }
65− ]
66−
67− BlurredBackground {
68− id: blurredBackground
69−
70− anchors.left: parent.left
71− anchors.top: parent.top
72− anchors.right: parent.right
73− height: title.lineCount === 1 ? parent.height/2 + units.gu(3)
74− : parent.height/2
75− art: currentImage
76−
77− Image {
78− width: Math.min(parent.width/2, parent.height)
79− height: width
80− sourceSize.height: width
81− sourceSize.width: width
82− source: currentImage
83− asynchronous: true
84− anchors.centerIn: parent
61+ model: [i18n.tr("Full view"), i18n.tr("Queue")]
8562 }
8663 }
8764
88− Item {
89− id: dataContainer
65+ VisualItemModel {
66+ id: tabs
9067
91− anchors {
92− top: blurredBackground.bottom
93− left: parent.left
94− right: parent.right
95− bottom: parent.bottom
96− margins: units.gu(2)
97− bottomMargin: isLandscapeMode ? units.gu(4) : units.gu(2)
98− }
68+ Item {
69+ id: nowPlayingItem
9970
100− Label {
101− id: title
102− anchors.left: parent.left
103− anchors.right: parent.right
104− anchors.top: parent.top
105− text: currentName
106− elide: Text.ElideRight
107− textSize: Label.Large
108− maximumLineCount: 2
109− wrapMode: Text.WordWrap
110− color: podbird.appTheme.baseText
111− }
71+ width: tabView.width
72+ height: tabView.height
11273
113− Label {
114− id: artist
115− anchors.left: title.left
116− anchors.right: title.right
117− anchors.top: title.bottom
118− anchors.topMargin: units.gu(1)
119− text: currentArtist
120− elide: Text.ElideRight
121− textSize: Label.Small
122− color: podbird.appTheme.baseSubText
123− }
74+ property bool isLandscapeMode: nowPlayingPage.width > nowPlayingPage.height
12475
125− Slider {
126− id: scrubber
76+ // Landscape rule
77+ states: [
78+ State {
79+ name: "landscape"
80+ when: nowPlayingItem.isLandscapeMode
12781
128− anchors {
129− left: parent.left
130− right: parent.right
131− bottom: controls.top
132− bottomMargin: isLandscapeMode && title.lineCount < 2 ? units.gu(4) : units.gu(2)
133− }
82+ PropertyChanges {
83+ target: blurredBackground
84+ width: nowPlayingPage.width/2.2
85+ height: nowPlayingPage.height
86+ }
13487
135− live: true
136− minimumValue: 0
137− maximumValue: playerLoader.item.duration
138− value: playerLoader.item.position
139− height: units.gu(2)
88+ AnchorChanges {
89+ target: blurredBackground
90+ anchors {
91+ top: nowPlayingItem.top
92+ left: parent.left
93+ right: undefined
94+ }
95+ }
14096
141− onValueChanged: {
142− if (pressed) {
143− playerLoader.item.seek(value);
97+ AnchorChanges {
98+ target: dataContainer
99+ anchors {
100+ top: nowPlayingItem.top
101+ left: blurredBackground.right
102+ right: parent.right
103+ bottom: parent.bottom
104+ }
105+ }
144106 }
145− }
107+ ]
146108
147− function formatValue(v) { return Podcasts.formatTime(v/1000); }
148− StyleHints { foregroundColor: podbird.appTheme.focusText }
149− }
109+ BlurredBackground {
110+ id: blurredBackground
150111
151− Connections {
152− target: playerLoader.item
153− onPositionChanged: scrubber.value = playerLoader.item.position
154− }
112+ anchors.left: parent.left
113+ anchors.top: nowPlayingItem.top
114+ anchors.right: parent.right
115+ height: title.lineCount === 1 ? nowPlayingPage.height/2.3 + units.gu(3)
116+ : nowPlayingPage.height/2.3
117+ art: currentImage
155118
156− Label {
157− id: startTime
158− textSize: Label.Small
159− anchors.left: scrubber.left
160− anchors.top: scrubber.bottom
161− color: podbird.appTheme.baseText
162− text: Podcasts.formatTime(playerLoader.item.position / 1000)
163− }
119+ Image {
120+ width: Math.min(nowPlayingPage.width/2, nowPlayingPage.height/2)
121+ height: width
122+ sourceSize.height: width
123+ sourceSize.width: width
124+ source: currentImage
125+ asynchronous: true
126+ anchors.centerIn: parent
127+ }
128+ }
164129
165− Label {
166− id: endTime
167− textSize: Label.Small
168− anchors.right: scrubber.right
169− anchors.top: scrubber.bottom
170− color: podbird.appTheme.baseText
171− text: Podcasts.formatTime(playerLoader.item.duration / 1000)
172− }
130+ Item {
131+ id: dataContainer
173132
174− Row {
175− id: controls
176−
177− anchors.bottom: parent.bottom
178− anchors.horizontalCenter: parent.horizontalCenter
179− spacing: units.gu(2)
180−
181− AbstractButton {
182− id: skipBackwardButton
183− width: units.gu(6)
184− height: width
185− anchors.verticalCenter: parent.verticalCenter
186− opacity: playerLoader.item.position === 0 ? 0.4 : 1.0
187− onClicked: {
188− if (playerLoader.item.position > 0) {
189− playerLoader.item.seek(playerLoader.item.position - podbird.settings.skipBack * 1000);
190− }
133+ anchors {
134+ top: blurredBackground.bottom
135+ left: parent.left
136+ right: parent.right
137+ bottom: parent.bottom
138+ margins: units.gu(2)
139+ bottomMargin: nowPlayingItem.isLandscapeMode ? units.gu(4) : units.gu(2)
191140 }
192141
193− Row {
194− spacing: units.gu(1)
195− anchors.centerIn: parent
142+ Label {
143+ id: title
144+ anchors.left: parent.left
145+ anchors.right: parent.right
146+ anchors.top: parent.top
147+ text: currentName
148+ elide: Text.ElideRight
149+ textSize: Label.Large
150+ maximumLineCount: 2
151+ wrapMode: Text.WordWrap
152+ color: podbird.appTheme.baseText
153+ }
196154
197− Label {
198− // TRANSLATORS: The string shown in the UI is -15s to denote the number of seconds that the podcast playback will skip backward.
199− // xgettext: no-c-format
200− text: i18n.tr("-%1s").arg(podbird.settings.skipBack)
201− textSize: Label.XxSmall
202− color: podbird.appTheme.baseText
203− anchors.verticalCenter: skipBackwardIcon.verticalCenter
155+ Label {
156+ id: artist
157+ anchors.left: title.left
158+ anchors.right: title.right
159+ anchors.top: title.bottom
160+ anchors.topMargin: units.gu(1)
161+ text: currentArtist
162+ elide: Text.ElideRight
163+ textSize: Label.Small
164+ color: podbird.appTheme.baseSubText
165+ }
166+
167+ Slider {
168+ id: scrubber
169+
170+ anchors {
171+ left: parent.left
172+ right: parent.right
173+ bottom: controls.top
174+ bottomMargin: nowPlayingItem.isLandscapeMode && title.lineCount < 2 ? units.gu(4) : units.gu(2)
204175 }
205176
206− Icon {
207− id: skipBackwardIcon
208− width: units.gu(3)
209− height: width
210− name: "media-seek-backward"
211− color: podbird.appTheme.baseIcon
177+ live: true
178+ minimumValue: 0
179+ maximumValue: player.duration
180+ value: player.position
181+ height: units.gu(2)
182+
183+ onValueChanged: {
184+ if (pressed) {
185+ player.seek(value);
186+ }
212187 }
188+
189+ function formatValue(v) { return Podcasts.formatTime(v/1000); }
190+ StyleHints { foregroundColor: podbird.appTheme.focusText }
213191 }
214− }
215192
216− AbstractButton {
217− id: playButton
218− width: units.gu(10)
219− height: width
220− opacity: playButton.pressed ? 0.4 : 1.0
221− onClicked: playerLoader.item.playbackState === MediaPlayer.PlayingState ? playerLoader.item.pause() : playerLoader.item.play()
193+ Connections {
194+ target: player
195+ onPositionChanged: scrubber.value = player.position
196+ }
222197
223− Icon {
224− id: playIcon
225− width: units.gu(6)
226− height: width
227− anchors.centerIn: parent
228− color: podbird.appTheme.baseIcon
229− name: playerLoader.item.playbackState === MediaPlayer.PlayingState ? "media-playback-pause"
230− : "media-playback-start"
198+ Label {
199+ id: startTime
200+ textSize: Label.Small
201+ anchors.left: scrubber.left
202+ anchors.top: scrubber.bottom
203+ color: podbird.appTheme.baseText
204+ text: Podcasts.formatTime(player.position / 1000)
231205 }
232− }
233206
234− AbstractButton {
235− id: skipForwardButton
236− width: units.gu(6)
237− height: width
238− anchors.verticalCenter: parent.verticalCenter
239− opacity: playerLoader.item.position === 0 ? 0.4 : 1.0
240− onClicked: {
241− if (playerLoader.item.position > 0) {
242− playerLoader.item.seek(playerLoader.item.position + podbird.settings.skipForward * 1000);
243− }
207+ Label {
208+ id: endTime
209+ textSize: Label.Small
210+ anchors.right: scrubber.right
211+ anchors.top: scrubber.bottom
212+ color: podbird.appTheme.baseText
213+ text: Podcasts.formatTime(player.duration / 1000)
244214 }
245215
246216 Row {
217+ id: controls
218+
219+ anchors.bottom: parent.bottom
220+ anchors.horizontalCenter: parent.horizontalCenter
247221 spacing: units.gu(1)
248− anchors.centerIn: parent
249222
250− Icon {
251− id: skipForwardIcon
252− width: units.gu(3)
223+ AbstractButton {
224+ id: mediaBackwardButton
225+ width: units.gu(6)
253226 height: width
254− name: "media-seek-forward"
255− color: podbird.appTheme.baseIcon
227+ anchors.verticalCenter: parent.verticalCenter
228+ enabled: player.playlist.canGoPrevious
229+ opacity: enabled ? 1.0 : 0.4
230+ onClicked: player.playlist.previous()
231+
232+ Icon {
233+ id: mediaBackwardIcon
234+ width: units.gu(3)
235+ height: width
236+ anchors.centerIn: parent
237+ color: podbird.appTheme.baseIcon
238+ name: "media-skip-backward"
239+ }
256240 }
257241
258− Label {
259− // TRANSLATORS: The string shown in the UI is +15s to denote the number of seconds that the podcast playback will skip forward.
260− // xgettext: no-c-format
261− text: i18n.tr("+%1s").arg(podbird.settings.skipForward)
262− textSize: Label.XxSmall
263− color: podbird.appTheme.baseText
264− anchors.verticalCenter: skipForwardIcon.verticalCenter
242+ AbstractButton {
243+ id: skipBackwardButton
244+ width: units.gu(6)
245+ height: width
246+ anchors.verticalCenter: parent.verticalCenter
247+ opacity: player.position === 0 ? 0.4 : 1.0
248+ onClicked: {
249+ if (player.position > 0) {
250+ player.seek(player.position - podbird.settings.skipBack * 1000);
251+ }
252+ }
253+
254+ Row {
255+ spacing: units.gu(1)
256+ anchors.centerIn: parent
257+
258+ Label {
259+ // TRANSLATORS: The string shown in the UI is -15s to denote the number of seconds that the podcast playback will skip backward.
260+ // xgettext: no-c-format
261+ text: i18n.tr("-%1s").arg(podbird.settings.skipBack)
262+ textSize: Label.XxSmall
263+ color: podbird.appTheme.baseText
264+ anchors.verticalCenter: skipBackwardIcon.verticalCenter
265+ }
266+
267+ Icon {
268+ id: skipBackwardIcon
269+ width: units.gu(3)
270+ height: width
271+ name: "media-seek-backward"
272+ color: podbird.appTheme.baseIcon
273+ }
274+ }
275+ }
276+
277+ AbstractButton {
278+ id: playButton
279+ width: units.gu(10)
280+ height: width
281+ opacity: playButton.pressed ? 0.4 : 1.0
282+ onClicked: player.playbackState === MediaPlayer.PlayingState ? player.pause() : player.play()
283+
284+ Icon {
285+ id: playIcon
286+ width: units.gu(6)
287+ height: width
288+ anchors.centerIn: parent
289+ color: podbird.appTheme.baseIcon
290+ name: player.playbackState === MediaPlayer.PlayingState ? "media-playback-pause"
291+ : "media-playback-start"
292+ }
293+ }
294+
295+ AbstractButton {
296+ id: skipForwardButton
297+ width: units.gu(6)
298+ height: width
299+ anchors.verticalCenter: parent.verticalCenter
300+ opacity: player.position === 0 ? 0.4 : 1.0
301+ onClicked: {
302+ if (player.position > 0) {
303+ player.seek(player.position + podbird.settings.skipForward * 1000);
304+ }
305+ }
306+
307+ Row {
308+ spacing: units.gu(1)
309+ anchors.centerIn: parent
310+
311+ Icon {
312+ id: skipForwardIcon
313+ width: units.gu(3)
314+ height: width
315+ name: "media-seek-forward"
316+ color: podbird.appTheme.baseIcon
317+ }
318+
319+ Label {
320+ // TRANSLATORS: The string shown in the UI is +15s to denote the number of seconds that the podcast playback will skip forward.
321+ // xgettext: no-c-format
322+ text: i18n.tr("+%1s").arg(podbird.settings.skipForward)
323+ textSize: Label.XxSmall
324+ color: podbird.appTheme.baseText
325+ anchors.verticalCenter: skipForwardIcon.verticalCenter
326+ }
327+ }
328+ }
329+
330+ AbstractButton {
331+ id: mediaForwardButton
332+ width: units.gu(6)
333+ height: width
334+ anchors.verticalCenter: parent.verticalCenter
335+ enabled: player.playlist.canGoNext
336+ opacity: enabled ? 1.0 : 0.4
337+ onClicked: player.playlist.next()
338+
339+ Icon {
340+ id: mediaForwardIcon
341+ width: units.gu(3)
342+ height: width
343+ anchors.centerIn: parent
344+ color: podbird.appTheme.baseIcon
345+ name: "media-skip-forward"
346+ }
265347 }
266348 }
267349 }
268350 }
351+
352+ Queue {
353+ width: tabView.width
354+ height: tabView.height
355+ }
356+ }
357+
358+ ListView {
359+ id: tabView
360+ model: tabs
361+ interactive: false
362+
363+ anchors {
364+ top: nowPlayingPage.header.bottom
365+ left: parent.left
366+ right: parent.right
367+ bottom: parent.bottom
368+ }
369+
370+ orientation: Qt.Horizontal
371+ snapMode: ListView.SnapOneItem
372+ currentIndex: nowPlayingPageSections.selectedIndex
373+ highlightMoveDuration: UbuntuAnimation.SlowDuration
269374 }
270375}
app/ui/PlayerControls.qml+6−6
@@ -17,7 +17,7 @@
1717 */
1818
1919import QtQuick 2.4
20−import QtMultimedia 5.4
20+import QtMultimedia 5.6
2121import Ubuntu.Components 1.3
2222
2323Rectangle {
@@ -49,7 +49,7 @@ Rectangle {
4949 anchors.top: cover.bottom
5050 color: podbird.appTheme.focusText
5151 height: units.gu(0.25)
52− width: playerLoader.item.duration > 0 ? (playerLoader.item.position / playerLoader.item.duration) * parent.width : 0
52+ width: player.duration > 0 ? (player.position / player.duration) * parent.width : 0
5353 }
5454
5555 Column {
@@ -97,16 +97,16 @@ Rectangle {
9797 visible: playButton.pressed
9898 }
9999
100− onClicked: playerLoader.item.playbackState === MediaPlayer.PlayingState ? playerLoader.item.pause()
101− : playerLoader.item.play()
100+ onClicked: player.playbackState === MediaPlayer.PlayingState ? player.pause()
101+ : player.play()
102102
103103 Icon {
104104 color: "white"
105105 width: units.gu(3)
106106 height: width
107107 anchors.centerIn: playButtonBackground
108− name: playerLoader.item.playbackState === MediaPlayer.PlayingState ? "media-playback-pause"
109− : "media-playback-start"
108+ name: player.playbackState === MediaPlayer.PlayingState ? "media-playback-pause"
109+ : "media-playback-start"
110110 opacity: playButton.pressed ? 0.4 : 1.0
111111 }
112112 }
app/ui/PodcastsTab.qml+4−3
@@ -17,10 +17,10 @@
1717 */
1818
1919import QtQuick 2.4
20−import QtMultimedia 5.4
20+import QtMultimedia 5.6
2121import QtQuick.LocalStorage 2.0
2222import Ubuntu.Components 1.3
23−import Ubuntu.DownloadManager 0.1
23+import Ubuntu.DownloadManager 1.2
2424import Ubuntu.Components.Popups 1.0
2525import "../podcasts.js" as Podcasts
2626import "../components"
@@ -166,12 +166,13 @@ Page {
166166 CardView {
167167 id: cardView
168168 clip: true
169+ heightOffset: units.gu(4)
169170 model: sortedPodcastModel
170171 delegate: Card {
171172 id: albumCard
172173 coverArt: model.image !== undefined ? model.image : Qt.resolvedUrl("../graphics/podbird.png")
173174 primaryText: model.name !== undefined ? model.name.trim() : "Undefined"
174− secondaryText: model.episodeCount > 0 ? i18n.tr("%1 unheard episode", "%1 unheard episodes", model.episodeCount).arg(model.episodeCount)
175+ secondaryText: model.episodeCount > 0 ? model.episodeCount
175176 : ""
176177 onClicked: {
177178 if(podcastPage.header === searchHeader) {
app/ui/Queue.qmlnew file+88
@@ -0,0 +1,88 @@
1+/*
2+ * Copyright 2016 Podbird Team
3+ *
4+ * This file is part of Podbird.
5+ *
6+ * Podbird is free software; you can redistribute it and/or modify
7+ * it under the terms of the GNU General Public License as published by
8+ * the Free Software Foundation; version 3.
9+ *
10+ * Podbird is distributed in the hope that it will be useful,
11+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
12+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+ * GNU General Public License for more details.
14+ *
15+ * You should have received a copy of the GNU General Public License
16+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
17+ */
18+
19+import QtQuick 2.4
20+import Ubuntu.Components 1.3
21+import "../podcasts.js" as Podcasts
22+import "../components"
23+
24+Item {
25+ id: queuePage
26+
27+ ListView {
28+ id: queueList
29+
30+ anchors.fill: parent
31+ model: player.playlist
32+
33+ delegate: ListItem {
34+ id: listItem
35+
36+ height: layout.height
37+ divider.visible: false
38+
39+ ListItemLayout {
40+ id: layout
41+
42+ // Grab the metaData for the current index using its unique source url
43+ property var metaModel: player.metaForSource(model.source)
44+
45+ Image {
46+ id: imgFrame
47+ width: units.gu(6)
48+ height: width
49+ source: Qt.resolvedUrl(layout.metaModel.image)
50+ sourceSize.height: width
51+ sourceSize.width: width
52+ SlotsLayout.position: SlotsLayout.First
53+ }
54+
55+ title.text: layout.metaModel.name
56+ // #FIXME: Change this 2 to prevent title eliding when UITK is updated to rev > 1800
57+ title.maximumLineCount: 1
58+ title.color: player.playlist.currentIndex === index ? podbird.appTheme.focusText
59+ : podbird.appTheme.baseText
60+
61+ subtitle.text: layout.metaModel.artist
62+ subtitle.color: podbird.appTheme.baseSubText
63+ }
64+
65+ leadingActions: ListItemActions {
66+ actions: [
67+ Action {
68+ iconName: "delete"
69+ onTriggered: {
70+ player.playlist.removeItem(index)
71+ var source = model.source
72+ source = source.toString()
73+ Podcasts.removeItemFromQueue(source)
74+ }
75+ }
76+ ]
77+ }
78+
79+ onClicked: {
80+ if (player.playlist.currentIndex === index) {
81+ player.toggle()
82+ } else {
83+ player.playlist.currentIndex = index
84+ }
85+ }
86+ }
87+ }
88+}
po/com.mikeasoft.podbird.pot+79−49
@@ -8,7 +8,7 @@ msgid ""
88msgstr ""
99"Project-Id-Version: \n"
1010"Report-Msgid-Bugs-To: \n"
11−"POT-Creation-Date: 2016-03-19 07:47+0530\n"
11+"POT-Creation-Date: 2016-03-19 19:13+0530\n"
1212"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
1313"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
1414"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -34,52 +34,42 @@ msgstr ""
3434msgid "Skip"
3535msgstr ""
3636
37−#. 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)
38−#: ../app/podbird.qml:175
39−#, qt-format
40−msgid "Podcasts listened to today: <b>%1</b>"
41−msgstr ""
42−
43−#: ../app/podbird.qml:176
44−msgid "No podcasts listened to today"
45−msgstr ""
46−
47−#: ../app/podcasts.js:182
37+#: ../app/podcasts.js:282
4838#, no-c-format, qt-format
4939msgid "%1 hr %2 min"
5040msgstr ""
5141
52−#: ../app/podcasts.js:191
42+#: ../app/podcasts.js:291
5343#, no-c-format, qt-format
5444msgid "%1 hr"
5545msgstr ""
5646
57−#: ../app/podcasts.js:199
47+#: ../app/podcasts.js:299
5848#, no-c-format, qt-format
5949msgid "%1 min"
6050msgstr ""
6151
6252#. TRANSLATORS: About as in information about the app
63−#: ../app/settings/About.qml:28 ../app/settings/About.qml:47
53+#: ../app/settings/About.qml:28 ../app/settings/About.qml:46
6454#: ../app/ui/SettingsPage.qml:222
6555msgid "About"
6656msgstr ""
6757
68−#: ../app/settings/About.qml:47
58+#: ../app/settings/About.qml:46
6959msgid "Credits"
7060msgstr ""
7161
7262#. TRANSLATORS: Podbird version number e.g Version 0.7
73−#: ../app/settings/About.qml:93
63+#: ../app/settings/About.qml:92
7464#, qt-format
7565msgid "Version %1"
7666msgstr ""
7767
78−#: ../app/settings/About.qml:114
68+#: ../app/settings/About.qml:113
7969msgid "Released under the terms of the GNU GPL v3"
8070msgstr ""
8171
82−#: ../app/settings/About.qml:124
72+#: ../app/settings/About.qml:123
8373#, qt-format
8474msgid "Source code available on %1"
8575msgstr ""
@@ -168,7 +158,7 @@ msgstr ""
168158msgid "Search Episode"
169159msgstr ""
170160
171−#: ../app/ui/EpisodesPage.qml:82 ../app/ui/EpisodesTab.qml:72
161+#: ../app/ui/EpisodesPage.qml:82
172162msgid "Mark all listened"
173163msgstr ""
174164
@@ -177,7 +167,7 @@ msgstr ""
177167msgid "Unsubscribe"
178168msgstr ""
179169
180−#: ../app/ui/EpisodesPage.qml:136 ../app/ui/EpisodesTab.qml:173
170+#: ../app/ui/EpisodesPage.qml:136 ../app/ui/EpisodesTab.qml:120
181171msgid "Search episode"
182172msgstr ""
183173
@@ -195,11 +185,11 @@ msgstr ""
195185msgid "Cancel"
196186msgstr ""
197187
198−#: ../app/ui/EpisodesPage.qml:223 ../app/ui/EpisodesTab.qml:295
188+#: ../app/ui/EpisodesPage.qml:223 ../app/ui/EpisodesTab.qml:405
199189msgid "Episode Description"
200190msgstr ""
201191
202−#: ../app/ui/EpisodesPage.qml:234 ../app/ui/EpisodesTab.qml:306
192+#: ../app/ui/EpisodesPage.qml:234 ../app/ui/EpisodesTab.qml:416
203193#: ../app/ui/SearchPage.qml:170
204194msgid "Close"
205195msgstr ""
@@ -220,82 +210,122 @@ msgstr ""
220210msgid "Listened"
221211msgstr ""
222212
223−#: ../app/ui/EpisodesPage.qml:413 ../app/ui/EpisodesTab.qml:135
213+#: ../app/ui/EpisodesPage.qml:413
224214msgid "Downloaded"
225215msgstr ""
226216
227−#: ../app/ui/EpisodesTab.qml:87
228−msgid "Download all"
217+#: ../app/ui/EpisodesTab.qml:82
218+msgid "Recent"
229219msgstr ""
230220
231−#: ../app/ui/EpisodesTab.qml:104
232−msgid "Delete all"
221+#: ../app/ui/EpisodesTab.qml:82
222+msgid "Downloads"
233223msgstr ""
234224
235−#: ../app/ui/EpisodesTab.qml:135
236−msgid "Recent"
225+#: ../app/ui/EpisodesTab.qml:82
226+msgid "Favourites"
237227msgstr ""
238228
239−#: ../app/ui/EpisodesTab.qml:135
240−msgid "Favourites"
229+#: ../app/ui/EpisodesTab.qml:141
230+msgid "Back"
231+msgstr ""
232+
233+#: ../app/ui/EpisodesTab.qml:153
234+msgid "Mark Listened"
235+msgstr ""
236+
237+#: ../app/ui/EpisodesTab.qml:171
238+msgid "Download episode(s)"
239+msgstr ""
240+
241+#: ../app/ui/EpisodesTab.qml:199
242+msgid "Delete episode(s)"
241243msgstr ""
242244
243−#: ../app/ui/EpisodesTab.qml:198
245+#: ../app/ui/EpisodesTab.qml:222
246+msgid "Favourite episode(s)"
247+msgstr ""
248+
249+#: ../app/ui/EpisodesTab.qml:245
250+msgid "Unfavourite episode(s)"
251+msgstr ""
252+
253+#: ../app/ui/EpisodesTab.qml:268
254+msgid "Add to queue"
255+msgstr ""
256+
257+#: ../app/ui/EpisodesTab.qml:308
244258msgid "No New Episodes"
245259msgstr ""
246260
247−#: ../app/ui/EpisodesTab.qml:200
261+#: ../app/ui/EpisodesTab.qml:310
248262msgid "No Downloaded Episodes"
249263msgstr ""
250264
251−#: ../app/ui/EpisodesTab.qml:202
265+#: ../app/ui/EpisodesTab.qml:312
252266msgid "No Favourited Episodes"
253267msgstr ""
254268
255−#: ../app/ui/EpisodesTab.qml:204
269+#: ../app/ui/EpisodesTab.qml:314
256270msgid "No Episodes Found"
257271msgstr ""
258272
259−#: ../app/ui/EpisodesTab.qml:210
273+#: ../app/ui/EpisodesTab.qml:320
260274msgid "No more episodes to listen to!"
261275msgstr ""
262276
263−#: ../app/ui/EpisodesTab.qml:212
277+#: ../app/ui/EpisodesTab.qml:322
264278msgid "No episodes have been downloaded for offline listening"
265279msgstr ""
266280
267−#: ../app/ui/EpisodesTab.qml:214
281+#: ../app/ui/EpisodesTab.qml:324
268282msgid "No episodes have been favourited."
269283msgstr ""
270284
271−#: ../app/ui/EpisodesTab.qml:216
285+#: ../app/ui/EpisodesTab.qml:326
272286msgid "No Episodes found matching the search term."
273287msgstr ""
274288
275−#: ../app/ui/EpisodesTab.qml:347
289+#: ../app/ui/EpisodesTab.qml:455
290+msgid "Downloads in progress"
291+msgstr ""
292+
293+#: ../app/ui/EpisodesTab.qml:495
294+msgid "Downloaded episodes"
295+msgstr ""
296+
297+#: ../app/ui/EpisodesTab.qml:505
276298msgid "Today"
277299msgstr ""
278300
279−#: ../app/ui/EpisodesTab.qml:351
301+#: ../app/ui/EpisodesTab.qml:509
280302msgid "Yesterday"
281303msgstr ""
282304
283−#: ../app/ui/EpisodesTab.qml:355
305+#: ../app/ui/EpisodesTab.qml:513
284306msgid "Older"
285307msgstr ""
286308
287−#: ../app/ui/NowPlayingPage.qml:29
309+#: ../app/ui/NowPlayingPage.qml:34
288310msgid "Now Playing"
289311msgstr ""
290312
313+#: ../app/ui/NowPlayingPage.qml:61
314+msgid "Full view"
315+msgstr ""
316+
317+#: ../app/ui/NowPlayingPage.qml:61
318+msgid "Queue"
319+msgstr ""
320+
291321#. TRANSLATORS: The string shown in the UI is -15s to denote the number of seconds that the podcast playback will skip backward.
292−#: ../app/ui/NowPlayingPage.qml:200
322+#: ../app/ui/NowPlayingPage.qml:261
293323#, no-c-format, qt-format
294324msgid "-%1s"
295325msgstr ""
296326
297327#. TRANSLATORS: The string shown in the UI is +15s to denote the number of seconds that the podcast playback will skip forward.
298−#: ../app/ui/NowPlayingPage.qml:261
328+#: ../app/ui/NowPlayingPage.qml:322
299329#, no-c-format, qt-format
300330msgid "+%1s"
301331msgstr ""
@@ -331,7 +361,7 @@ msgstr ""
331361msgid "No podcasts found matching the search term."
332362msgstr ""
333363
334−#: ../app/ui/PodcastsTab.qml:174 ../app/ui/PodcastsTab.qml:221
364+#: ../app/ui/PodcastsTab.qml:222
335365#, qt-format
336366msgid "%1 unheard episode"
337367msgid_plural "%1 unheard episodes"
@@ -529,10 +559,10 @@ msgstr ""
529559msgid "Finish"
530560msgstr ""
531561
532−#: /home/krnekhelesh/Development/devel-branch-sync-1-build/po/Podbird.desktop.in.h:1
562+#: /home/krnekhelesh/Development/devel-branch-sync-2-build/po/Podbird.desktop.in.h:1
533563msgid "The chirpiest podcast manager for Ubuntu"
534564msgstr ""
535565
536−#: /home/krnekhelesh/Development/devel-branch-sync-1-build/po/Podbird.desktop.in.h:2
566+#: /home/krnekhelesh/Development/devel-branch-sync-2-build/po/Podbird.desktop.in.h:2
537567msgid "podcast;audio;itunes;broadcast;digital;stream;podcatcher;video;vodcast;"
538568msgstr ""