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
=== modified file 'app/components/Card.qml'
--- app/components/Card.qml 2016-02-25 11:09:44 +0000
+++ app/components/Card.qml 2016-03-28 22:39:33 +0000
@@ -1,12 +1,13 @@
1/*1/*
2 * Copyright (C) 2014-20162 * Copyright 2016 Podbird Team
3 * Andrew Hayzen <ahayzen@gmail.com>3 *
4 *4 * This file is part of Podbird.
5 * This program is free software; you can redistribute it and/or modify5 *
6 * Podbird is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by7 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; version 3.8 * the Free Software Foundation; version 3.
8 *9 *
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,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.13 * GNU General Public License for more details.
@@ -18,122 +19,66 @@
18import QtQuick 2.419import QtQuick 2.4
19import Ubuntu.Components 1.320import Ubuntu.Components 1.3
2021
21Item {22AbstractButton {
22 id: card23 id: card
2324
24 /* Required by ColumnFlow */25 height: parent.parent.cellHeight
25 property int index26 width: parent.parent.cellWidth
26 property var model
2727
28 property alias coverArt: imgFrame.source28 property alias coverArt: imgFrame.source
29 property alias primaryText: primaryLabel.text29 property alias primaryText: primaryLabel.text
30 property alias secondaryText: secondaryLabel.text30 property string secondaryText: ""
31 property alias secondaryTextVisible: secondaryLabel.visible31
3232 Image {
33 signal clicked(var mouse)33 id: imgFrame
34 signal pressAndHold(var mouse)34 width: parent.width/1.2
3535 height: width
36 height: cardColumn.childrenRect.height + 2 * bg.anchors.margins36 anchors.top: parent.top
3737 anchors.horizontalCenter: parent.horizontalCenter
38 /* Background for card */38 sourceSize.height: width
39 Rectangle {39 sourceSize.width: width
40 id: bg40
41 anchors.fill: parent41 Loader {
42 anchors.margins: units.gu(1)42 id: hintLoader
43 color: podbird.appTheme.hightlightListView43 anchors.verticalCenter: parent.top
44 }44 anchors.right: parent.right
4545 anchors.rightMargin: units.gu(-0.5)
46 /* Column containing image and labels */46 sourceComponent: secondaryText !== "" ? hintComponent : undefined
47 Column {47 }
48 id: cardColumn48
4949 Component {
50 anchors.fill: bg50 id: hintComponent
51 spacing: units.gu(0.5)51 Rectangle {
5252 color: podbird.appTheme.focusText
53 Image {53 width: secondaryLabel.implicitWidth + units.gu(1)
54 id: imgFrame54 height: secondaryLabel.implicitHeight + units.gu(1)
55 width: parent.width55 radius: units.gu(0.5)
56 height: width56 visible: secondaryLabel.text !== ""
57 sourceSize.height: width57 Label {
58 sourceSize.width: width58 id: secondaryLabel
59 }59 anchors.centerIn: parent
6060 text: secondaryText
61 Item {61 visible: text !== ""
62 height: units.gu(1)62 textSize: Label.Small
63 width: units.gu(1)63 color: "White"
64 }64 }
6565 }
66 Label {66 }
67 id: primaryLabel67 }
68 anchors {68
69 left: parent.left69 Label {
70 right: parent.right70 id: primaryLabel
71 margins: units.gu(1)71 anchors {
72 }72 top: imgFrame.bottom
73 color: podbird.appTheme.baseText73 left: imgFrame.left
74 elide: Text.ElideRight74 right: imgFrame.right
75 textSize: Label.Small75 margins: units.gu(1)
76 opacity: 1.076 }
77 wrapMode: Text.WordWrap77 color: podbird.appTheme.baseText
78 horizontalAlignment: Text.AlignHCenter78 elide: Text.ElideRight
79 }79 textSize: Label.Small
8080 wrapMode: Text.WordWrap
81 Label {81 maximumLineCount: 2
82 id: secondaryLabel82 horizontalAlignment: Text.AlignHCenter
83 anchors {
84 left: parent.left
85 leftMargin: units.gu(1)
86 right: parent.right
87 rightMargin: units.gu(1)
88 }
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)
100 }
101 }
102
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 {}
112 }
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 {}
138 }83 }
139}84}
14085
=== modified file 'app/components/CardView.qml'
--- app/components/CardView.qml 2016-02-25 11:09:44 +0000
+++ app/components/CardView.qml 2016-03-28 22:39:33 +0000
@@ -18,33 +18,28 @@
18import QtQuick 2.418import QtQuick 2.4
19import Ubuntu.Components 1.319import Ubuntu.Components 1.3
2020
21Flickable {21GridView {
22 id: cardViewFlickable22 id: gridView
23
23 anchors {24 anchors {
24 fill: parent25 fill: parent
25 margins: units.gu(1)26 margins: units.gu(1)
26 }27 }
2728
28 // dont use flow.contentHeight as it is inaccurate due to height of labels29 cellHeight: cellSize + heightOffset
29 // changing as they load30 cellWidth: cellSize + widthOffset
30 contentHeight: flow.contentHeight + flow.anchors.margins * 2 + units.gu(8)31
31 contentWidth: width32 header: Item {
3233 width: parent.width
33 property alias count: flow.count34 height: units.gu(2)
34 property alias delegate: flow.delegate
35 property var getter
36 property alias model: flow.model
37 property real itemWidth: units.gu(15)
38
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
46 }35 }
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
48 Component.onCompleted: {43 Component.onCompleted: {
49 // FIXME: workaround for qtubuntu not returning values depending on the grid unit definition44 // FIXME: workaround for qtubuntu not returning values depending on the grid unit definition
50 // for Flickable.maximumFlickVelocity and Flickable.flickDeceleration45 // for Flickable.maximumFlickVelocity and Flickable.flickDeceleration
5146
=== removed file 'app/components/ColumnFlow.qml'
--- app/components/ColumnFlow.qml 2016-02-25 11:09:44 +0000
+++ app/components/ColumnFlow.qml 1970-01-01 00:00:00 +0000
@@ -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
18import QtQuick 2.4
19
20Item {
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}
4950
=== added file 'app/components/CustomSectionHeader.qml'
--- app/components/CustomSectionHeader.qml 1970-01-01 00:00:00 +0000
+++ app/components/CustomSectionHeader.qml 2016-03-28 22:39:33 +0000
@@ -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
19import QtQuick 2.4
20import Ubuntu.Components 1.3
21
22Item {
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}
048
=== removed file 'app/components/TabsList.qml'
--- app/components/TabsList.qml 2016-03-28 22:39:32 +0000
+++ app/components/TabsList.qml 1970-01-01 00:00:00 +0000
@@ -1,52 +0,0 @@
1/*
2 * Copyright 2015-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
19import QtQuick 2.4
20import Ubuntu.Components 1.3
21
22ActionList {
23 id: tabsList
24
25 property int currentTab: tabs.selectedTabIndex
26
27 children: [
28 Action {
29 text: i18n.tr("Episodes")
30 visible: currentTab !== 0
31 onTriggered: {
32 tabs.selectedTabIndex = 0
33 }
34 },
35
36 Action {
37 text: i18n.tr("Podcasts")
38 visible: currentTab !== 1
39 onTriggered: {
40 tabs.selectedTabIndex = 1
41 }
42 },
43
44 Action {
45 text: i18n.tr("Settings")
46 visible: currentTab !== 2
47 onTriggered: {
48 tabs.selectedTabIndex = 2
49 }
50 }
51 ]
52}
530
=== modified file 'app/podbird.qml'
--- app/podbird.qml 2016-03-28 22:39:32 +0000
+++ app/podbird.qml 2016-03-28 22:39:33 +0000
@@ -18,13 +18,12 @@
1818
19import QtQuick 2.419import QtQuick 2.4
20import Podbird 1.020import Podbird 1.0
21import UserMetrics 0.121import QtMultimedia 5.6
22import QtMultimedia 5.4
23import Ubuntu.Connectivity 1.022import Ubuntu.Connectivity 1.0
24import Qt.labs.settings 1.023import Qt.labs.settings 1.0
25import Ubuntu.Components 1.324import Ubuntu.Components 1.3
26import QtQuick.LocalStorage 2.025import QtQuick.LocalStorage 2.0
27import Ubuntu.DownloadManager 0.126import Ubuntu.DownloadManager 1.2
28import "ui"27import "ui"
29import "themes" as Themes28import "themes" as Themes
30import "podcasts.js" as Podcasts29import "podcasts.js" as Podcasts
@@ -45,11 +44,11 @@
4544
46 Component.onDestruction: {45 Component.onDestruction: {
47 console.log("[LOG]: Download cancelled");46 console.log("[LOG]: Download cancelled");
48 downloader.cancel();
49 var db = Podcasts.init()47 var db = Podcasts.init()
50 db.transaction(function (tx) {48 db.transaction(function (tx) {
51 tx.executeSql('UPDATE Episode SET queued=0 WHERE queued=1');49 tx.executeSql('UPDATE Episode SET queued=0 WHERE queued=1');
52 })50 })
51 Podcasts.clearQueue()
53 }52 }
5453
55 // RefreshModel function to call refreshModel() function of the tab currently54 // RefreshModel function to call refreshModel() function of the tab currently
@@ -138,84 +137,156 @@
138 }137 }
139 }138 }
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 {
142 id: downloader160 id: downloader
143 property var queue: []161
144 property string downloadingGuid162 property string downloadingGuid: downloads.length > 0 ? downloads[0].guid : "NULL"
145163 property int progress: downloads.length > 0 ? downloads[0].progress : 0
146 onFinished: {164
165 cleanDownloads: true
166 onDownloadFinished: {
147 var db = Podcasts.init();167 var db = Podcasts.init();
148 var finalLocation = fileManager.saveDownload(path);168 var finalLocation = fileManager.saveDownload(path);
149 db.transaction(function (tx) {169 db.transaction(function (tx) {
150 tx.executeSql("UPDATE Episode SET downloadedfile=?, queued=0 WHERE guid=?", [finalLocation, downloadingGuid]);170 tx.executeSql("UPDATE Episode SET downloadedfile=?, queued=0 WHERE guid=?", [finalLocation, download.guid]);
151 queue.shift();
152 if (queue.length > 0) {
153 downloadingGuid = queue[0][0];
154 download(queue[0][1]);
155 } else {
156 downloadingGuid = "";
157 }
158 });171 });
159 }172 }
160173
161 function addDownload(guid, url) {174 onErrorFound: {
162 queue.push([guid, url]);175 console.log("[ERROR]: " + download.errorMessage)
163 if (queue.length == 1) {176 }
164 downloadingGuid = guid;177 }
165 download(url);178
166 }179 MediaPlayer {
167 }180 id: player
168 }181
169182 // Wrapper function around decodeURIComponent() to prevent exceptions
170 // UserMetrics to show Podbird stats on welcome screen183 // from bubbling up to the app.
171 Metric {184 function decodeFileURI(filename)
172 id: podcastsMetric185 {
173 name: "podcast-metrics"186 var newFilename = "";
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)187 try {
175 format: i18n.tr("Podcasts listened to today: <b>%1</b>")188 newFilename = decodeURIComponent(filename);
176 emptyFormat: i18n.tr("No podcasts listened to today")189 } catch (e) {
177 domain: "com.mikeasoft.podbird"190 newFilename = filename;
178 }191 console.log("Unicode decoding error:", filename, e.message)
179192 }
180 // Load the media player only when the user starts to play some media. This193
181 // should improve app-startup slightly.194 return newFilename;
182 Loader {195 }
183 id: playerLoader196
184 sourceComponent: currentUrl != "" ? playerComponent : undefined197 function metaForSource(source) {
185 }198 var blankMeta = {
186199 name: "",
187 Component {200 artist: "",
188 id: playerComponent201 image: "",
189 MediaPlayer {202 guid: "",
190 id: player203 }
191204
192 property bool podcastCounted: false205 source = source.toString()
193206
194 source: currentUrl207 return Podcasts.lookup(decodeFileURI(source)) || blankMeta;
195208 }
196 onSourceChanged: {209
197 podcastCounted = false210 function toggle() {
198 }211 if (playbackState === MediaPlayer.PlayingState) {
199212 pause()
200 onPositionChanged: {213 } else {
201 if (currentGuid == "" || duration <= 0) {214 play()
202 return;215 }
203 }216 }
204217
205 if (position > 10000 && !podcastCounted) {218 function playEpisode(guid, image, name, artist, url) {
206 podcastCounted = true219 // Clear current queue
207 podcastsMetric.increment()220 player.playlist.clear()
208 console.log("[LOG]: Podcast User metric incremented")221 Podcasts.clearQueue()
209 }222
210223 // Add episode to queue
211 var db = Podcasts.init();224 Podcasts.addItemToQueue(guid, image, name, artist, url)
212 db.transaction(function (tx) {225 player.playlist.addItem(url)
213 tx.executeSql("UPDATE Episode SET position=? WHERE guid=?", [position >= duration ? 120 : position, currentGuid]);226
214 if (position >= duration - 120) {227 // Play episode
215 tx.executeSql("UPDATE Episode SET listened = 1 WHERE guid=?", [currentGuid]);228 player.play()
216 }229 }
217 });230
218 }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
244 }
245 }
246
247 property bool endOfMedia: false
248 property double progress: 0
249
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()
283 }
284
285 pause()
286 }
287
288 // Always reset endOfMedia
289 endOfMedia = false
219 }290 }
220 }291 }
221292
@@ -290,13 +361,13 @@
290 states: [361 states: [
291 State {362 State {
292 name: "shown"363 name: "shown"
293 when: currentUrl != "" && !mainStack.currentPage.isNowPlayingPage364 when: player.playlist.itemCount !== 0 && !mainStack.currentPage.isNowPlayingPage
294 PropertyChanges { target: playerControlLoader; anchors.bottomMargin: 0 }365 PropertyChanges { target: playerControlLoader; anchors.bottomMargin: 0 }
295 },366 },
296367
297 State {368 State {
298 name: "hidden"369 name: "hidden"
299 when: currentUrl == "" || mainStack.currentPage.isNowPlayingPage || !playerControl.visible370 when: player.playlist.itemCount === 0 || mainStack.currentPage.isNowPlayingPage || !playerControl.visible
300 PropertyChanges { target: playerControlLoader; anchors.bottomMargin: -units.gu(7) }371 PropertyChanges { target: playerControlLoader; anchors.bottomMargin: -units.gu(7) }
301 }372 }
302 ]373 ]
303374
=== modified file 'app/podcasts.js'
--- app/podcasts.js 2016-03-28 22:39:32 +0000
+++ app/podcasts.js 2016-03-28 22:39:33 +0000
@@ -22,6 +22,7 @@
22 db.transaction(function(tx) {22 db.transaction(function(tx) {
23 tx.executeSql('CREATE TABLE IF NOT EXISTS Podcast(artist TEXT, name TEXT, description TEXT, feed TEXT, image TEXT, lastupdate TIMESTAMP)');23 tx.executeSql('CREATE TABLE IF NOT EXISTS Podcast(artist TEXT, name TEXT, description TEXT, feed TEXT, image TEXT, lastupdate TIMESTAMP)');
24 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))');24 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)');
25 });26 });
2627
27 try {28 try {
@@ -55,6 +56,105 @@
55 return db;56 return db;
56}57}
5758
59// Function to add item to queue
60function 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
75function 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
92function clearQueue() {
93 var db = init();
94 db.transaction(function(tx) {
95 tx.executeSql("DELETE FROM Queue");
96 });
97}
98
99function 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
126function 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
142function 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
58function subscribe(artist, name, feed, img) {158function subscribe(artist, name, feed, img) {
59 var db = init();159 var db = init();
60 db.transaction(function(tx) {160 db.transaction(function(tx) {
@@ -259,8 +359,8 @@
259 var rs2 = tx.executeSql("SELECT rowid, * FROM Episode WHERE podcast=? ORDER BY published DESC", [rs.rows.item(i).rowid]);359 var rs2 = tx.executeSql("SELECT rowid, * FROM Episode WHERE podcast=? ORDER BY published DESC", [rs.rows.item(i).rowid]);
260 var loopCount = maxEpisodeDownload > rs2.rows.length ? rs2.rows.length : maxEpisodeDownload360 var loopCount = maxEpisodeDownload > rs2.rows.length ? rs2.rows.length : maxEpisodeDownload
261 for (var j=0; j < loopCount; j++) {361 for (var j=0; j < loopCount; j++) {
262 if (!rs2.rows.item(j).downloadedfile && !rs2.rows.item(j).listened) {362 if (!rs2.rows.item(j).downloadedfile && !rs2.rows.item(j).listened && rs2.rows.item(j).audiourl) {
263 downloader.addDownload(rs2.rows.item(j).guid, 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)
264 tx.executeSql("UPDATE Episode SET queued=1 WHERE guid = ?", [rs2.rows.item(j).guid]);364 tx.executeSql("UPDATE Episode SET queued=1 WHERE guid = ?", [rs2.rows.item(j).guid]);
265 }365 }
266 }366 }
267367
=== modified file 'app/settings/About.qml'
--- app/settings/About.qml 2016-02-25 11:09:44 +0000
+++ app/settings/About.qml 2016-03-28 22:39:33 +0000
@@ -36,7 +36,6 @@
3636
37 anchors {37 anchors {
38 left: parent.left38 left: parent.left
39 leftMargin: units.gu(2)
40 bottom: parent.bottom39 bottom: parent.bottom
41 }40 }
4241
4342
=== modified file 'app/ui/EpisodesPage.qml'
--- app/ui/EpisodesPage.qml 2016-03-28 22:39:32 +0000
+++ app/ui/EpisodesPage.qml 2016-03-28 22:39:33 +0000
@@ -17,10 +17,10 @@
17 */17 */
1818
19import QtQuick 2.419import QtQuick 2.4
20import QtMultimedia 5.420import QtMultimedia 5.6
21import Ubuntu.Components 1.321import Ubuntu.Components 1.3
22import QtQuick.LocalStorage 2.022import QtQuick.LocalStorage 2.0
23import Ubuntu.DownloadManager 0.123import Ubuntu.DownloadManager 1.2
24import Ubuntu.Components.Popups 1.324import Ubuntu.Components.Popups 1.3
25import "../podcasts.js" as Podcasts25import "../podcasts.js" as Podcasts
26import "../components"26import "../components"
@@ -442,8 +442,8 @@
442 id: listItemLayout442 id: listItemLayout
443443
444 title.text: model.name !== undefined ? model.name.trim() : "Undefined"444 title.text: model.name !== undefined ? model.name.trim() : "Undefined"
445 title.color: currentGuid === model.guid || downloader.downloadingGuid === model.guid ? podbird.appTheme.focusText445 title.color: downloader.downloadingGuid === model.guid ? podbird.appTheme.focusText
446 : podbird.appTheme.baseText446 : podbird.appTheme.baseText
447 // #FIXME: Change this 2 to prevent title eliding when UITK is updated to rev > 1800447 // #FIXME: Change this 2 to prevent title eliding when UITK is updated to rev > 1800
448 title.maximumLineCount: 1448 title.maximumLineCount: 1
449449
@@ -505,12 +505,24 @@
505 tx.executeSql("UPDATE Episode SET queued=1 WHERE guid = ?", [model.guid]);505 tx.executeSql("UPDATE Episode SET queued=1 WHERE guid = ?", [model.guid]);
506 });506 });
507 episodeModel.setProperty(model.index, "queued", 1)507 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 }
509 }513 }
510 }514 }
511 },515 },
512516
513 Action {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
525 Action {
514 iconName: model.favourited ? "unlike" : "like"526 iconName: model.favourited ? "unlike" : "like"
515 onTriggered: {527 onTriggered: {
516 var db = Podcasts.init();528 var db = Podcasts.init();
@@ -536,20 +548,10 @@
536548
537 onClicked: {549 onClicked: {
538 Haptics.play()550 Haptics.play()
539 var db = Podcasts.init();551 if (currentGuid !== model.guid) {
540 db.transaction(function (tx) {552 currentUrl = model.downloadedfile ? "file://" + model.downloadedfile : model.audiourl;
541 if (currentGuid !== model.guid) {553 player.playEpisode(model.guid, model.image, model.name, model.artist, currentUrl)
542 currentGuid = "";554 }
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 });
553 }555 }
554 }556 }
555557
556558
=== modified file 'app/ui/EpisodesTab.qml'
--- app/ui/EpisodesTab.qml 2016-03-28 22:39:32 +0000
+++ app/ui/EpisodesTab.qml 2016-03-28 22:39:33 +0000
@@ -17,10 +17,10 @@
17 */17 */
1818
19import QtQuick 2.419import QtQuick 2.4
20import QtMultimedia 5.420import QtMultimedia 5.6
21import Ubuntu.Components 1.321import Ubuntu.Components 1.3
22import QtQuick.LocalStorage 2.022import QtQuick.LocalStorage 2.0
23import Ubuntu.DownloadManager 0.123import Ubuntu.DownloadManager 1.2
24import Ubuntu.Components.Popups 1.324import Ubuntu.Components.Popups 1.3
25import "../podcasts.js" as Podcasts25import "../podcasts.js" as Podcasts
26import "../components"26import "../components"
@@ -64,58 +64,6 @@
64 episodesPage.header = searchHeader64 episodesPage.header = searchHeader
65 searchField.item.forceActiveFocus()65 searchField.item.forceActiveFocus()
66 }66 }
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 }
119 }67 }
120 ]68 ]
12169
@@ -124,7 +72,6 @@
12472
125 anchors {73 anchors {
126 left: parent.left74 left: parent.left
127 leftMargin: units.gu(2)
128 bottom: parent.bottom75 bottom: parent.bottom
129 }76 }
13077
@@ -132,7 +79,7 @@
132 selectedSectionColor: podbird.appTheme.focusText79 selectedSectionColor: podbird.appTheme.focusText
133 }80 }
13481
135 model: [i18n.tr("Recent"), i18n.tr("Downloaded"), i18n.tr("Favourites")]82 model: [i18n.tr("Recent"), i18n.tr("Downloads"), i18n.tr("Favourites")]
136 onSelectedIndexChanged: {83 onSelectedIndexChanged: {
137 refreshModel();84 refreshModel();
138 }85 }
@@ -174,6 +121,169 @@
174 }121 }
175 }122 }
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
177 Loader {287 Loader {
178 id: emptyState288 id: emptyState
179289
@@ -185,7 +295,7 @@
185 verticalCenterOffset: Qt.inputMethod.visible ? units.gu(4) : 0295 verticalCenterOffset: Qt.inputMethod.visible ? units.gu(4) : 0
186 }296 }
187297
188 sourceComponent: episodesModel.count === 0 || sortedEpisodeModel.count === 0 ? emptyStateComponent : undefined298 sourceComponent: (episodesModel.count === 0 || sortedEpisodeModel.count === 0) && downloader.downloads.length === 0 ? emptyStateComponent : undefined
189 }299 }
190300
191 Component {301 Component {
@@ -315,6 +425,9 @@
315 ListView {425 ListView {
316 id: episodeList426 id: episodeList
317427
428 signal clearSelection()
429 signal closeSelection()
430
318 Component.onCompleted: {431 Component.onCompleted: {
319 // FIXME: workaround for qtubuntu not returning values depending on the grid unit definition432 // FIXME: workaround for qtubuntu not returning values depending on the grid unit definition
320 // for Flickable.maximumFlickVelocity and Flickable.flickDeceleration433 // for Flickable.maximumFlickVelocity and Flickable.flickDeceleration
@@ -332,35 +445,77 @@
332445
333 clip: true446 clip: true
334 model: sortedEpisodeModel447 model: sortedEpisodeModel
448
449 header: Column {
450 width: episodeList.width
451 visible: height !== 0
452 height: downloader.downloads.length > 0 && episodesPageHeaderSections.selectedIndex === 1 ? childrenRect.height : 0
453
454 CustomSectionHeader {
455 title: i18n.tr("Downloads in progress")
456 }
457
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 }
473
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 }
482
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 }
490 }
491 }
492 }
493
494 CustomSectionHeader {
495 title: i18n.tr("Downloaded episodes")
496 visible: sortedEpisodeModel.count !== 0 || episodesModel.count !== 0
497 }
498 }
499
335 section.property: "diff"500 section.property: "diff"
336 section.labelPositioning: ViewSection.InlineLabels501 section.labelPositioning: ViewSection.InlineLabels
337502 section.delegate: CustomSectionHeader {
338 section.delegate: ListItem {503 title: {
339 height: headerText.title.text !== "" ? headerText.height + divider.height : units.gu(0)504 if (section === "Today") {
340 divider.anchors.leftMargin: units.gu(2)505 return i18n.tr("Today")
341 divider.anchors.rightMargin: units.gu(2)506 }
342507
343 ListItemLayout {508 else if (section === "Yesterday") {
344 id: headerText509 return i18n.tr("Yesterday")
345 title.text: {510 }
346 if (section === "Today") {511
347 return i18n.tr("Today")512 else if (section === "Older") {
348 }513 return i18n.tr("Older")
349514 }
350 else if (section === "Yesterday") {515
351 return i18n.tr("Yesterday")516 else {
352 }517 return ""
353518 }
354 else if (section === "Older") {
355 return i18n.tr("Older")
356 }
357
358 else {
359 return ""
360 }
361 }
362 title.color: podbird.appTheme.baseText
363 title.font.weight: Font.DemiBold
364 }519 }
365 }520 }
366521
@@ -380,8 +535,8 @@
380 id: listItemLayout535 id: listItemLayout
381536
382 title.text: model.name !== undefined ? model.name.trim() : "Undefined"537 title.text: model.name !== undefined ? model.name.trim() : "Undefined"
383 title.color: currentGuid === model.guid || downloader.downloadingGuid === model.guid ? podbird.appTheme.focusText538 title.color: downloader.downloadingGuid === model.guid ? podbird.appTheme.focusText
384 : podbird.appTheme.baseText539 : podbird.appTheme.baseText
385 // #FIXME: Change this 2 to prevent title eliding when UITK is updated to rev > 1800540 // #FIXME: Change this 2 to prevent title eliding when UITK is updated to rev > 1800
386 title.maximumLineCount: 1541 title.maximumLineCount: 1
387542
@@ -439,7 +594,11 @@
439 tx.executeSql("UPDATE Episode SET queued=1 WHERE guid = ?", [model.guid]);594 tx.executeSql("UPDATE Episode SET queued=1 WHERE guid = ?", [model.guid]);
440 });595 });
441 episodesModel.setProperty(model.index, "queued", 1)596 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 }
443 }602 }
444 }603 }
445 },604 },
@@ -465,6 +624,14 @@
465 },624 },
466625
467 Action {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
634 Action {
468 iconName: model.favourited ? "unlike" : "like"635 iconName: model.favourited ? "unlike" : "like"
469 onTriggered: {636 onTriggered: {
470 var db = Podcasts.init();637 var db = Podcasts.init();
@@ -496,21 +663,29 @@
496663
497 onClicked: {664 onClicked: {
498 Haptics.play()665 Haptics.play()
499 var db = Podcasts.init();666 if (selectMode) {
500 db.transaction(function (tx) {667 selected = !selected
668 } else {
501 if (currentGuid !== model.guid) {669 if (currentGuid !== model.guid) {
502 currentGuid = "";670 currentUrl = model.downloadedfile ? "file://" + model.downloadedfile : model.audiourl;
503 currentUrl = model.downloadedfile ? model.downloadedfile : model.audiourl;671 player.playEpisode(model.guid, model.image, model.name, model.artist, currentUrl)
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;
511 }672 }
512 });673 }
513 }674 }
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
514 }689 }
515690
516 Scrollbar {691 Scrollbar {
517692
=== modified file 'app/ui/NowPlayingPage.qml'
--- app/ui/NowPlayingPage.qml 2016-02-25 11:09:44 +0000
+++ app/ui/NowPlayingPage.qml 2016-03-28 22:39:33 +0000
@@ -17,8 +17,9 @@
17 */17 */
1818
19import QtQuick 2.419import QtQuick 2.4
20import QtMultimedia 5.420import QtMultimedia 5.6
21import Ubuntu.Components 1.321import Ubuntu.Components 1.3
22import QtQuick.LocalStorage 2.0
22import "../podcasts.js" as Podcasts23import "../podcasts.js" as Podcasts
23import "../components"24import "../components"
2425
@@ -26,245 +27,349 @@
26 id: nowPlayingPage27 id: nowPlayingPage
2728
28 visible: false29 visible: false
29 title: i18n.tr("Now Playing")
3030
31 property bool isNowPlayingPage: true31 property bool isNowPlayingPage: true
32 property bool isLandscapeMode: width > height32
3333 header: PageHeader {
34 // Landscape rule34 title: i18n.tr("Now Playing")
35 states: [35
36 State {36 StyleHints {
37 name: "landscape"37 backgroundColor: podbird.appTheme.background
38 when: isLandscapeMode38 }
3939
40 PropertyChanges {40 trailingActionBar.actions: Action {
41 target: blurredBackground41 iconName: "delete"
42 width: parent.width/2.242 visible: nowPlayingPageSections.selectedIndex === 1
43 height: parent.height43 onTriggered: {
44 }44 Podcasts.clearQueue()
4545 player.playlist.clear()
46 AnchorChanges {46 mainStack.pop()
47 target: blurredBackground47 }
48 }
49
50 extension: Sections {
51 id: nowPlayingPageSections
52
53 anchors {
54 left: parent.left
55 bottom: parent.bottom
56 }
57
58 StyleHints {
59 selectedSectionColor: podbird.appTheme.focusText
60 }
61 model: [i18n.tr("Full view"), i18n.tr("Queue")]
62 }
63 }
64
65 VisualItemModel {
66 id: tabs
67
68 Item {
69 id: nowPlayingItem
70
71 width: tabView.width
72 height: tabView.height
73
74 property bool isLandscapeMode: nowPlayingPage.width > nowPlayingPage.height
75
76 // Landscape rule
77 states: [
78 State {
79 name: "landscape"
80 when: nowPlayingItem.isLandscapeMode
81
82 PropertyChanges {
83 target: blurredBackground
84 width: nowPlayingPage.width/2.2
85 height: nowPlayingPage.height
86 }
87
88 AnchorChanges {
89 target: blurredBackground
90 anchors {
91 top: nowPlayingItem.top
92 left: parent.left
93 right: undefined
94 }
95 }
96
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 }
106 }
107 ]
108
109 BlurredBackground {
110 id: blurredBackground
111
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
118
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 }
129
130 Item {
131 id: dataContainer
132
48 anchors {133 anchors {
49 top: parent.top134 top: blurredBackground.bottom
50 left: parent.left135 left: parent.left
51 right: undefined
52 }
53 }
54
55 AnchorChanges {
56 target: dataContainer
57 anchors {
58 top: parent.top
59 left: blurredBackground.right
60 right: parent.right136 right: parent.right
61 bottom: parent.bottom137 bottom: parent.bottom
138 margins: units.gu(2)
139 bottomMargin: nowPlayingItem.isLandscapeMode ? units.gu(4) : units.gu(2)
140 }
141
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 }
154
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)
175 }
176
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 }
187 }
188
189 function formatValue(v) { return Podcasts.formatTime(v/1000); }
190 StyleHints { foregroundColor: podbird.appTheme.focusText }
191 }
192
193 Connections {
194 target: player
195 onPositionChanged: scrubber.value = player.position
196 }
197
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)
205 }
206
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)
214 }
215
216 Row {
217 id: controls
218
219 anchors.bottom: parent.bottom
220 anchors.horizontalCenter: parent.horizontalCenter
221 spacing: units.gu(1)
222
223 AbstractButton {
224 id: mediaBackwardButton
225 width: units.gu(6)
226 height: width
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 }
240 }
241
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 }
347 }
62 }348 }
63 }349 }
64 }350 }
65 ]351
66352 Queue {
67 BlurredBackground {353 width: tabView.width
68 id: blurredBackground354 height: tabView.height
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
85 }355 }
86 }356 }
87357
88 Item {358 ListView {
89 id: dataContainer359 id: tabView
360 model: tabs
361 interactive: false
90362
91 anchors {363 anchors {
92 top: blurredBackground.bottom364 top: nowPlayingPage.header.bottom
93 left: parent.left365 left: parent.left
94 right: parent.right366 right: parent.right
95 bottom: parent.bottom367 bottom: parent.bottom
96 margins: units.gu(2)368 }
97 bottomMargin: isLandscapeMode ? units.gu(4) : units.gu(2)369
98 }370 orientation: Qt.Horizontal
99371 snapMode: ListView.SnapOneItem
100 Label {372 currentIndex: nowPlayingPageSections.selectedIndex
101 id: title373 highlightMoveDuration: UbuntuAnimation.SlowDuration
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 }
112
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 }
124
125 Slider {
126 id: scrubber
127
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 }
134
135 live: true
136 minimumValue: 0
137 maximumValue: playerLoader.item.duration
138 value: playerLoader.item.position
139 height: units.gu(2)
140
141 onValueChanged: {
142 if (pressed) {
143 playerLoader.item.seek(value);
144 }
145 }
146
147 function formatValue(v) { return Podcasts.formatTime(v/1000); }
148 StyleHints { foregroundColor: podbird.appTheme.focusText }
149 }
150
151 Connections {
152 target: playerLoader.item
153 onPositionChanged: scrubber.value = playerLoader.item.position
154 }
155
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 }
164
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 }
173
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 }
191 }
192
193 Row {
194 spacing: units.gu(1)
195 anchors.centerIn: parent
196
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
204 }
205
206 Icon {
207 id: skipBackwardIcon
208 width: units.gu(3)
209 height: width
210 name: "media-seek-backward"
211 color: podbird.appTheme.baseIcon
212 }
213 }
214 }
215
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()
222
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"
231 }
232 }
233
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 }
244 }
245
246 Row {
247 spacing: units.gu(1)
248 anchors.centerIn: parent
249
250 Icon {
251 id: skipForwardIcon
252 width: units.gu(3)
253 height: width
254 name: "media-seek-forward"
255 color: podbird.appTheme.baseIcon
256 }
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 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
265 }
266 }
267 }
268 }
269 }374 }
270}375}
271376
=== modified file 'app/ui/PlayerControls.qml'
--- app/ui/PlayerControls.qml 2016-02-25 11:09:44 +0000
+++ app/ui/PlayerControls.qml 2016-03-28 22:39:33 +0000
@@ -17,7 +17,7 @@
17 */17 */
1818
19import QtQuick 2.419import QtQuick 2.4
20import QtMultimedia 5.420import QtMultimedia 5.6
21import Ubuntu.Components 1.321import Ubuntu.Components 1.3
2222
23Rectangle {23Rectangle {
@@ -49,7 +49,7 @@
49 anchors.top: cover.bottom49 anchors.top: cover.bottom
50 color: podbird.appTheme.focusText50 color: podbird.appTheme.focusText
51 height: units.gu(0.25)51 height: units.gu(0.25)
52 width: playerLoader.item.duration > 0 ? (playerLoader.item.position / playerLoader.item.duration) * parent.width : 052 width: player.duration > 0 ? (player.position / player.duration) * parent.width : 0
53 }53 }
5454
55 Column {55 Column {
@@ -97,16 +97,16 @@
97 visible: playButton.pressed97 visible: playButton.pressed
98 }98 }
9999
100 onClicked: playerLoader.item.playbackState === MediaPlayer.PlayingState ? playerLoader.item.pause()100 onClicked: player.playbackState === MediaPlayer.PlayingState ? player.pause()
101 : playerLoader.item.play()101 : player.play()
102102
103 Icon {103 Icon {
104 color: "white"104 color: "white"
105 width: units.gu(3)105 width: units.gu(3)
106 height: width106 height: width
107 anchors.centerIn: playButtonBackground107 anchors.centerIn: playButtonBackground
108 name: playerLoader.item.playbackState === MediaPlayer.PlayingState ? "media-playback-pause"108 name: player.playbackState === MediaPlayer.PlayingState ? "media-playback-pause"
109 : "media-playback-start"109 : "media-playback-start"
110 opacity: playButton.pressed ? 0.4 : 1.0110 opacity: playButton.pressed ? 0.4 : 1.0
111 }111 }
112 }112 }
113113
=== modified file 'app/ui/PodcastsTab.qml'
--- app/ui/PodcastsTab.qml 2016-03-28 22:39:32 +0000
+++ app/ui/PodcastsTab.qml 2016-03-28 22:39:33 +0000
@@ -17,10 +17,10 @@
17 */17 */
1818
19import QtQuick 2.419import QtQuick 2.4
20import QtMultimedia 5.420import QtMultimedia 5.6
21import QtQuick.LocalStorage 2.021import QtQuick.LocalStorage 2.0
22import Ubuntu.Components 1.322import Ubuntu.Components 1.3
23import Ubuntu.DownloadManager 0.123import Ubuntu.DownloadManager 1.2
24import Ubuntu.Components.Popups 1.024import Ubuntu.Components.Popups 1.0
25import "../podcasts.js" as Podcasts25import "../podcasts.js" as Podcasts
26import "../components"26import "../components"
@@ -166,12 +166,13 @@
166 CardView {166 CardView {
167 id: cardView167 id: cardView
168 clip: true168 clip: true
169 heightOffset: units.gu(4)
169 model: sortedPodcastModel170 model: sortedPodcastModel
170 delegate: Card {171 delegate: Card {
171 id: albumCard172 id: albumCard
172 coverArt: model.image !== undefined ? model.image : Qt.resolvedUrl("../graphics/podbird.png")173 coverArt: model.image !== undefined ? model.image : Qt.resolvedUrl("../graphics/podbird.png")
173 primaryText: model.name !== undefined ? model.name.trim() : "Undefined"174 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
175 : ""176 : ""
176 onClicked: {177 onClicked: {
177 if(podcastPage.header === searchHeader) {178 if(podcastPage.header === searchHeader) {
178179
=== added file 'app/ui/Queue.qml'
--- app/ui/Queue.qml 1970-01-01 00:00:00 +0000
+++ app/ui/Queue.qml 2016-03-28 22:39:33 +0000
@@ -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
19import QtQuick 2.4
20import Ubuntu.Components 1.3
21import "../podcasts.js" as Podcasts
22import "../components"
23
24Item {
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}
089
=== modified file 'po/com.mikeasoft.podbird.pot'
--- po/com.mikeasoft.podbird.pot 2016-03-28 22:39:32 +0000
+++ po/com.mikeasoft.podbird.pot 2016-03-28 22:39:33 +0000
@@ -8,7 +8,7 @@
8msgstr ""8msgstr ""
9"Project-Id-Version: \n"9"Project-Id-Version: \n"
10"Report-Msgid-Bugs-To: \n"10"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"
12"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"12"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"13"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14"Language-Team: LANGUAGE <LL@li.org>\n"14"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -34,52 +34,42 @@
34msgid "Skip"34msgid "Skip"
35msgstr ""35msgstr ""
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)37#: ../app/podcasts.js:282
38#: ../app/podbird.qml:175
39#, qt-format
40msgid "Podcasts listened to today: <b>%1</b>"
41msgstr ""
42
43#: ../app/podbird.qml:176
44msgid "No podcasts listened to today"
45msgstr ""
46
47#: ../app/podcasts.js:182
48#, no-c-format, qt-format38#, no-c-format, qt-format
49msgid "%1 hr %2 min"39msgid "%1 hr %2 min"
50msgstr ""40msgstr ""
5141
52#: ../app/podcasts.js:19142#: ../app/podcasts.js:291
53#, no-c-format, qt-format43#, no-c-format, qt-format
54msgid "%1 hr"44msgid "%1 hr"
55msgstr ""45msgstr ""
5646
57#: ../app/podcasts.js:19947#: ../app/podcasts.js:299
58#, no-c-format, qt-format48#, no-c-format, qt-format
59msgid "%1 min"49msgid "%1 min"
60msgstr ""50msgstr ""
6151
62#. TRANSLATORS: About as in information about the app52#. TRANSLATORS: About as in information about the app
63#: ../app/settings/About.qml:28 ../app/settings/About.qml:4753#: ../app/settings/About.qml:28 ../app/settings/About.qml:46
64#: ../app/ui/SettingsPage.qml:22254#: ../app/ui/SettingsPage.qml:222
65msgid "About"55msgid "About"
66msgstr ""56msgstr ""
6757
68#: ../app/settings/About.qml:4758#: ../app/settings/About.qml:46
69msgid "Credits"59msgid "Credits"
70msgstr ""60msgstr ""
7161
72#. TRANSLATORS: Podbird version number e.g Version 0.762#. TRANSLATORS: Podbird version number e.g Version 0.7
73#: ../app/settings/About.qml:9363#: ../app/settings/About.qml:92
74#, qt-format64#, qt-format
75msgid "Version %1"65msgid "Version %1"
76msgstr ""66msgstr ""
7767
78#: ../app/settings/About.qml:11468#: ../app/settings/About.qml:113
79msgid "Released under the terms of the GNU GPL v3"69msgid "Released under the terms of the GNU GPL v3"
80msgstr ""70msgstr ""
8171
82#: ../app/settings/About.qml:12472#: ../app/settings/About.qml:123
83#, qt-format73#, qt-format
84msgid "Source code available on %1"74msgid "Source code available on %1"
85msgstr ""75msgstr ""
@@ -168,7 +158,7 @@
168msgid "Search Episode"158msgid "Search Episode"
169msgstr ""159msgstr ""
170160
171#: ../app/ui/EpisodesPage.qml:82 ../app/ui/EpisodesTab.qml:72161#: ../app/ui/EpisodesPage.qml:82
172msgid "Mark all listened"162msgid "Mark all listened"
173msgstr ""163msgstr ""
174164
@@ -177,7 +167,7 @@
177msgid "Unsubscribe"167msgid "Unsubscribe"
178msgstr ""168msgstr ""
179169
180#: ../app/ui/EpisodesPage.qml:136 ../app/ui/EpisodesTab.qml:173170#: ../app/ui/EpisodesPage.qml:136 ../app/ui/EpisodesTab.qml:120
181msgid "Search episode"171msgid "Search episode"
182msgstr ""172msgstr ""
183173
@@ -195,11 +185,11 @@
195msgid "Cancel"185msgid "Cancel"
196msgstr ""186msgstr ""
197187
198#: ../app/ui/EpisodesPage.qml:223 ../app/ui/EpisodesTab.qml:295188#: ../app/ui/EpisodesPage.qml:223 ../app/ui/EpisodesTab.qml:405
199msgid "Episode Description"189msgid "Episode Description"
200msgstr ""190msgstr ""
201191
202#: ../app/ui/EpisodesPage.qml:234 ../app/ui/EpisodesTab.qml:306192#: ../app/ui/EpisodesPage.qml:234 ../app/ui/EpisodesTab.qml:416
203#: ../app/ui/SearchPage.qml:170193#: ../app/ui/SearchPage.qml:170
204msgid "Close"194msgid "Close"
205msgstr ""195msgstr ""
@@ -220,82 +210,122 @@
220msgid "Listened"210msgid "Listened"
221msgstr ""211msgstr ""
222212
223#: ../app/ui/EpisodesPage.qml:413 ../app/ui/EpisodesTab.qml:135213#: ../app/ui/EpisodesPage.qml:413
224msgid "Downloaded"214msgid "Downloaded"
225msgstr ""215msgstr ""
226216
227#: ../app/ui/EpisodesTab.qml:87217#: ../app/ui/EpisodesTab.qml:82
228msgid "Download all"
229msgstr ""
230
231#: ../app/ui/EpisodesTab.qml:104
232msgid "Delete all"
233msgstr ""
234
235#: ../app/ui/EpisodesTab.qml:135
236msgid "Recent"218msgid "Recent"
237msgstr ""219msgstr ""
238220
239#: ../app/ui/EpisodesTab.qml:135221#: ../app/ui/EpisodesTab.qml:82
222msgid "Downloads"
223msgstr ""
224
225#: ../app/ui/EpisodesTab.qml:82
240msgid "Favourites"226msgid "Favourites"
241msgstr ""227msgstr ""
242228
243#: ../app/ui/EpisodesTab.qml:198229#: ../app/ui/EpisodesTab.qml:141
230msgid "Back"
231msgstr ""
232
233#: ../app/ui/EpisodesTab.qml:153
234msgid "Mark Listened"
235msgstr ""
236
237#: ../app/ui/EpisodesTab.qml:171
238msgid "Download episode(s)"
239msgstr ""
240
241#: ../app/ui/EpisodesTab.qml:199
242msgid "Delete episode(s)"
243msgstr ""
244
245#: ../app/ui/EpisodesTab.qml:222
246msgid "Favourite episode(s)"
247msgstr ""
248
249#: ../app/ui/EpisodesTab.qml:245
250msgid "Unfavourite episode(s)"
251msgstr ""
252
253#: ../app/ui/EpisodesTab.qml:268
254msgid "Add to queue"
255msgstr ""
256
257#: ../app/ui/EpisodesTab.qml:308
244msgid "No New Episodes"258msgid "No New Episodes"
245msgstr ""259msgstr ""
246260
247#: ../app/ui/EpisodesTab.qml:200261#: ../app/ui/EpisodesTab.qml:310
248msgid "No Downloaded Episodes"262msgid "No Downloaded Episodes"
249msgstr ""263msgstr ""
250264
251#: ../app/ui/EpisodesTab.qml:202265#: ../app/ui/EpisodesTab.qml:312
252msgid "No Favourited Episodes"266msgid "No Favourited Episodes"
253msgstr ""267msgstr ""
254268
255#: ../app/ui/EpisodesTab.qml:204269#: ../app/ui/EpisodesTab.qml:314
256msgid "No Episodes Found"270msgid "No Episodes Found"
257msgstr ""271msgstr ""
258272
259#: ../app/ui/EpisodesTab.qml:210273#: ../app/ui/EpisodesTab.qml:320
260msgid "No more episodes to listen to!"274msgid "No more episodes to listen to!"
261msgstr ""275msgstr ""
262276
263#: ../app/ui/EpisodesTab.qml:212277#: ../app/ui/EpisodesTab.qml:322
264msgid "No episodes have been downloaded for offline listening"278msgid "No episodes have been downloaded for offline listening"
265msgstr ""279msgstr ""
266280
267#: ../app/ui/EpisodesTab.qml:214281#: ../app/ui/EpisodesTab.qml:324
268msgid "No episodes have been favourited."282msgid "No episodes have been favourited."
269msgstr ""283msgstr ""
270284
271#: ../app/ui/EpisodesTab.qml:216285#: ../app/ui/EpisodesTab.qml:326
272msgid "No Episodes found matching the search term."286msgid "No Episodes found matching the search term."
273msgstr ""287msgstr ""
274288
275#: ../app/ui/EpisodesTab.qml:347289#: ../app/ui/EpisodesTab.qml:455
290msgid "Downloads in progress"
291msgstr ""
292
293#: ../app/ui/EpisodesTab.qml:495
294msgid "Downloaded episodes"
295msgstr ""
296
297#: ../app/ui/EpisodesTab.qml:505
276msgid "Today"298msgid "Today"
277msgstr ""299msgstr ""
278300
279#: ../app/ui/EpisodesTab.qml:351301#: ../app/ui/EpisodesTab.qml:509
280msgid "Yesterday"302msgid "Yesterday"
281msgstr ""303msgstr ""
282304
283#: ../app/ui/EpisodesTab.qml:355305#: ../app/ui/EpisodesTab.qml:513
284msgid "Older"306msgid "Older"
285msgstr ""307msgstr ""
286308
287#: ../app/ui/NowPlayingPage.qml:29309#: ../app/ui/NowPlayingPage.qml:34
288msgid "Now Playing"310msgid "Now Playing"
289msgstr ""311msgstr ""
290312
313#: ../app/ui/NowPlayingPage.qml:61
314msgid "Full view"
315msgstr ""
316
317#: ../app/ui/NowPlayingPage.qml:61
318msgid "Queue"
319msgstr ""
320
291#. TRANSLATORS: The string shown in the UI is -15s to denote the number of seconds that the podcast playback will skip backward.321#. 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:200322#: ../app/ui/NowPlayingPage.qml:261
293#, no-c-format, qt-format323#, no-c-format, qt-format
294msgid "-%1s"324msgid "-%1s"
295msgstr ""325msgstr ""
296326
297#. TRANSLATORS: The string shown in the UI is +15s to denote the number of seconds that the podcast playback will skip forward.327#. 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:261328#: ../app/ui/NowPlayingPage.qml:322
299#, no-c-format, qt-format329#, no-c-format, qt-format
300msgid "+%1s"330msgid "+%1s"
301msgstr ""331msgstr ""
@@ -331,7 +361,7 @@
331msgid "No podcasts found matching the search term."361msgid "No podcasts found matching the search term."
332msgstr ""362msgstr ""
333363
334#: ../app/ui/PodcastsTab.qml:174 ../app/ui/PodcastsTab.qml:221364#: ../app/ui/PodcastsTab.qml:222
335#, qt-format365#, qt-format
336msgid "%1 unheard episode"366msgid "%1 unheard episode"
337msgid_plural "%1 unheard episodes"367msgid_plural "%1 unheard episodes"
@@ -529,10 +559,10 @@
529msgid "Finish"559msgid "Finish"
530msgstr ""560msgstr ""
531561
532#: /home/krnekhelesh/Development/devel-branch-sync-1-build/po/Podbird.desktop.in.h:1562#: /home/krnekhelesh/Development/devel-branch-sync-2-build/po/Podbird.desktop.in.h:1
533msgid "The chirpiest podcast manager for Ubuntu"563msgid "The chirpiest podcast manager for Ubuntu"
534msgstr ""564msgstr ""
535565
536#: /home/krnekhelesh/Development/devel-branch-sync-1-build/po/Podbird.desktop.in.h:2566#: /home/krnekhelesh/Development/devel-branch-sync-2-build/po/Podbird.desktop.in.h:2
537msgid "podcast;audio;itunes;broadcast;digital;stream;podcatcher;video;vodcast;"567msgid "podcast;audio;itunes;broadcast;digital;stream;podcatcher;video;vodcast;"
538msgstr ""568msgstr ""

Subscribers

People subscribed via source and target branches