Merge lp:~nik90/podbird/improve-podcast-page into lp:podbird

Proposed by Nekhelesh Ramananthan
Status: Merged
Approved by: Michael Sheldon
Approved revision: 20
Merged at revision: 10
Proposed branch: lp:~nik90/podbird/improve-podcast-page
Merge into: lp:podbird
Diff against target: 1238 lines (+681/-333)
6 files modified
app/podbird.qml (+45/-9)
app/podcasts.js (+8/-0)
app/ui/EpisodesPage.qml (+444/-0)
app/ui/PodcastsTab.qml (+79/-262)
app/ui/SearchTab.qml (+54/-38)
po/com.mikeasoft.podbird.pot (+51/-24)
To merge this branch: bzr merge lp:~nik90/podbird/improve-podcast-page
Reviewer Review Type Date Requested Status
Michael Sheldon Approve
Review via email: mp+247523@code.launchpad.net

Commit message

- Refined the designs of the Podcast, search and episodes page.
- Fixed episode time format to be more clear
- Episode title now wrap into max of 2 lines instead of eliding immediately
- Swipe delete podcasts
- Moved episode list of a podcast into its own page
- Added Pagestack to allow for separation of episode list into its own page for better code separation.

Description of the change

This MP implements the following,
- Refined the designs of the Podcast, search and episodes page.
- Fixed episode time format to be more clear
- Episode title now wrap into max of 2 lines instead of eliding immediately
- Swipe delete podcasts
- Moved episode list of a podcast into its own page
- Added Pagestack to allow for separation of episode list into its own page for better code separation.

Screenshots of the refined design can be seen at http://imgur.com/a/Pf1ij#0

NOTE: There are 2 things that require attention,

1. I have separated PodcastsTab.qml and EpisodesPage.qml to the best of my understanding. I would recommend you go through PodcastsTab.qml to check if anything else can be moved.

2. After removing the use of UbuntuShape, the image size shown does not seem to respect the height and width specified in the code. On the phone it looks good as shown in the screenshot link above. However on the emulator, the cover image looks bigger than expected. Not sure why this is the case. ---- FIXED

To post a comment you must log in.
Revision history for this message
Nekhelesh Ramananthan (nik90) wrote :

NOTE: DO NOT MERGE! I am still tweaking it until I think it looks good. Still not satisfied with it yet :)

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

Okay, I've set to work in progress, just set back to needs review when you're ready :)

Revision history for this message
Nekhelesh Ramananthan (nik90) wrote :

Okay I am happy with how it has turned out. Let me know if you want me to tweak something to your liking.

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

I like the new style, a couple of issue found while testing:

With that reorganisation you'll need to move the SingleDownload into main or similar, otherwise it'll get destroyed when the user leaves that page.

It looks like PNGs aren't getting scaled correctly (subscribe to http://s.libre.fm/podcast.rss for an example), probably best to set the sourceSize for images (more memory efficient too)

The author field in the search tab doesn't elide and overlaps the edge of the screen for long author names (e.g. try searching for "Science", and looking at the "Hidden Universe HD" entry)

Once those are fixed it should be good to go :).

review: Needs Fixing
14. By Nekhelesh Ramananthan

Fixed author field not eliding in the search page

15. By Nekhelesh Ramananthan

Fixed scaling of PNG Images by setting sourceSize explicitly

16. By Nekhelesh Ramananthan

Hide duration icon if duration is not available

17. By Nekhelesh Ramananthan

Fixed PNG Image scaling in the search page as well

18. By Nekhelesh Ramananthan

Moved SingleDownloader to podbird.qml to preserve its status regardless of which page the user navigates to.

Revision history for this message
Nekhelesh Ramananthan (nik90) wrote :

> I like the new style, a couple of issue found while testing:
>

Awesome. Glad to hear. I thought perhaps I changed it too much without asking you.

> With that reorganisation you'll need to move the SingleDownload into main or
> similar, otherwise it'll get destroyed when the user leaves that page.
>

Ah yes, I moved it to podbird.qml alongside ImageDownloader. Fixed in rev 18

> It looks like PNGs aren't getting scaled correctly (subscribe to
> http://s.libre.fm/podcast.rss for an example), probably best to set the
> sourceSize for images (more memory efficient too)
>

By setting the sourceSize explicitly it now works correctly both on the phone and on the emulator for different podcasts I tried. Rev 15, 17

> The author field in the search tab doesn't elide and overlaps the edge of the
> screen for long author names (e.g. try searching for "Science", and looking at
> the "Hidden Universe HD" entry)
>

Fixed in rev 14

> Once those are fixed it should be good to go :).

I also fixed an issue where the duration icon was shown despite the duration being not available.

19. By Nekhelesh Ramananthan

Bah, missed one last episode artist eliding that I just fixed in this commit

20. By Nekhelesh Ramananthan

Added confirm delete dialog in epsiode page

Revision history for this message
Nekhelesh Ramananthan (nik90) wrote :

Chloe reported in G+ that it is too easy to delete a podcast from the episode list page. So I added a dialog to confirm deletion. http://imgur.com/Fn0isnk

Revision history for this message
Nekhelesh Ramananthan (nik90) wrote :

Sry wrong link. Here is the correct one http://imgur.com/0aTU0dq

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

Looks good, thanks!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'app/podbird.qml'
2--- app/podbird.qml 2015-01-10 10:05:28 +0000
3+++ app/podbird.qml 2015-01-25 18:02:01 +0000
4@@ -37,6 +37,37 @@
5 }
6 }
7
8+ SingleDownload {
9+ id: downloader
10+ property var queue: []
11+ property string downloadingGuid
12+
13+ onFinished: {
14+ var db = Podcasts.init();
15+ var finalLocation = fileManager.saveDownload(path);
16+ db.transaction(function (tx) {
17+ tx.executeSql("UPDATE Episode SET downloadedfile=? WHERE guid=?", [finalLocation, downloadingGuid]);
18+ queue.shift();
19+ if (queue.length > 0) {
20+ downloadingGuid = queue[0][0];
21+ download(queue[0][1]);
22+ } else {
23+ downloadingGuid = "";
24+ }
25+
26+ loadEpisodes(episodeModel.pid, episodeModel.artist, episodeModel.image);
27+ });
28+ }
29+
30+ function addDownload(guid, url) {
31+ queue.push([guid, url]);
32+ if (queue.length == 1) {
33+ downloadingGuid = guid;
34+ download(url);
35+ }
36+ }
37+ }
38+
39 MediaPlayer {
40 id: player
41 onPositionChanged: {
42@@ -54,15 +85,20 @@
43 }
44 }
45
46- Tabs {
47- id: tabs
48-
49- PodcastsTab {
50- objectName: "podcastsTab"
51- }
52-
53- SearchTab {
54- objectName: "searchTab"
55+ PageStack {
56+ id: mainStack
57+ Component.onCompleted: push(tabs)
58+ Tabs {
59+ id: tabs
60+
61+ PodcastsTab {
62+ id: podcastTab
63+ objectName: "podcastsTab"
64+ }
65+
66+ SearchTab {
67+ objectName: "searchTab"
68+ }
69 }
70 }
71
72
73=== modified file 'app/podcasts.js'
74--- app/podcasts.js 2015-01-04 10:20:41 +0000
75+++ app/podcasts.js 2015-01-25 18:02:01 +0000
76@@ -35,6 +35,14 @@
77 });
78 }
79
80+function getTimeDiff(time) {
81+ var hours, minutes;
82+ time = Math.floor(time / 60)
83+ minutes = time % 60
84+ hours = Math.floor(time / 60)
85+ return [hours, minutes]
86+}
87+
88 function formatTime(seconds) {
89 var rem = seconds % 3600;
90 return Math.floor(seconds / 3600) + ":" + zeroFill(Math.floor(rem / 60), 2);
91
92=== added file 'app/ui/EpisodesPage.qml'
93--- app/ui/EpisodesPage.qml 1970-01-01 00:00:00 +0000
94+++ app/ui/EpisodesPage.qml 2015-01-25 18:02:01 +0000
95@@ -0,0 +1,444 @@
96+import QtQuick 2.0
97+import QtMultimedia 5.0
98+import Ubuntu.Components 1.1
99+import QtQuick.Layouts 1.1
100+import QtQuick.LocalStorage 2.0
101+import Ubuntu.DownloadManager 0.1
102+import Ubuntu.Components.Popups 1.0
103+import Ubuntu.Components.ListItems 1.0 as ListItem
104+import "../podcasts.js" as Podcasts
105+
106+Page {
107+ id: episodesPage
108+
109+ visible: false
110+ title: episodeName
111+
112+ property string episodeName
113+ property string episodeId
114+ property string episodeArtist
115+ property string episodeImage
116+
117+ property bool episodesUpdating: false;
118+
119+ Component.onCompleted: {
120+ loadEpisodes(episodeId, episodeArtist, episodeImage)
121+ }
122+
123+ head.contents: Label {
124+ text: title
125+ anchors.fill: parent
126+ anchors.margins: units.gu(0.5)
127+ verticalAlignment: Text.AlignVCenter
128+
129+ fontSize: "x-large"
130+ fontSizeMode: Text.Fit
131+
132+ maximumLineCount: 3
133+ minimumPointSize: 8
134+ elide: Text.Right
135+ wrapMode: Text.WordWrap
136+ }
137+
138+ head.actions: [
139+ Action {
140+ text: i18n.tr("Unsubscribe")
141+ iconName: "delete"
142+ onTriggered: {
143+ PopupUtils.open(confirmDeleteDialog);
144+ }
145+ }
146+ ]
147+
148+ Component {
149+ id: confirmDeleteDialog
150+ Dialog {
151+ id: dialogInternal
152+ title: i18n.tr("Unsubscribe Confirmation")
153+ text: i18n.tr("Are you sure you want to unsubscribe from <b>%1</b>?").arg(episodesPage.episodeName)
154+ Button {
155+ text: i18n.tr("Yes")
156+ color: UbuntuColors.orange
157+ onClicked: {
158+ var db = Podcasts.init();
159+ db.transaction(function (tx) {
160+ var rs = tx.executeSql("SELECT downloadedfile FROM Episode WHERE downloadedfile NOT NULL AND podcast=?", [episodeModel.pid]);
161+ for(var i = 0; i < rs.rows.length; i++) {
162+ fileManager.deleteFile(rs.rows.item(i).downloadedfile);
163+ }
164+ tx.executeSql("DELETE FROM Episode WHERE podcast=?", [episodeModel.pid]);
165+ tx.executeSql("DELETE FROM Podcast WHERE rowid=?", [episodeModel.pid]);
166+ mainStack.pop()
167+ PopupUtils.close(dialogInternal)
168+ });
169+ }
170+ }
171+ Button {
172+ text: i18n.tr("No")
173+ color: UbuntuColors.green
174+ onClicked: {
175+ PopupUtils.close(dialogInternal)
176+ }
177+ }
178+ }
179+ }
180+
181+ ListModel {
182+ id: episodeModel
183+ property string pid;
184+ property string artist;
185+ property string image;
186+ }
187+
188+ ListView {
189+ id: episodeList
190+
191+ clip: true
192+ anchors.fill: parent
193+ model: episodeModel
194+
195+ footer: Item {
196+ width: parent.width
197+ height: units.gu(8)
198+ }
199+
200+ delegate: ListItem.Empty {
201+ id: listItem
202+
203+ property bool expanded: false
204+
205+ width: parent.width
206+ height: mainColumn.height
207+
208+ onClicked: listItem.expanded = !listItem.expanded
209+
210+ Column {
211+ id: mainColumn
212+
213+ anchors {
214+ top: parent.top
215+ left: parent.left
216+ right: parent.right
217+ margins: units.gu(2)
218+ topMargin: units.gu(1)
219+ }
220+
221+ spacing: units.gu(1)
222+
223+ RowLayout {
224+ id: titleRow
225+
226+ width: parent.width
227+ spacing: units.gu(2)
228+
229+ Image {
230+ id: imgFrame
231+ width: units.gu(6)
232+ height: width
233+ sourceSize.height: width
234+ sourceSize.width: width
235+ source: model.image
236+ }
237+
238+ Column {
239+ id: detailColumn
240+
241+ anchors.verticalCenter: imgFrame.verticalCenter
242+ Layout.fillWidth: true
243+
244+ Label {
245+ textFormat: Text.PlainText
246+ text: model.name.trim()
247+ width: parent.width
248+ elide: Text.ElideRight
249+ }
250+
251+ Label {
252+ id: episodeArtist
253+ width: parent.width
254+ text: model.artist
255+ fontSize: "small"
256+ elide: Text.ElideRight
257+ }
258+ }
259+ }
260+
261+ Label {
262+ id: desc
263+ text: model.description
264+ textFormat: Text.RichText
265+ clip: true
266+ height: listItem.expanded ? contentHeight : units.gu(4)
267+ wrapMode: Text.WordWrap
268+ width: parent.width
269+ elide: Text.ElideRight
270+ fontSize: "small"
271+ color: "#999999"
272+ Behavior on height {
273+ UbuntuNumberAnimation {
274+ duration: UbuntuAnimation.SlowDuration
275+ }
276+ }
277+
278+ }
279+
280+ Item {
281+ id: statusBox
282+
283+ width: parent.width
284+ height: units.gu(6)
285+
286+ function formatTime(seconds) {
287+ var time = Podcasts.getTimeDiff(seconds)
288+ var hour = time[0]
289+ var minute = time[1]
290+ if(hour > 0 && minute > 0) {
291+ return (i18n.tr("%1h %2m"))
292+ .arg(hour)
293+ .arg(minute)
294+ }
295+
296+ else if(hour > 0 && minute === 0) {
297+ return (i18n.tr("%1h"))
298+ .arg(hour)
299+ }
300+
301+ else if(hour === 0 && minute > 0) {
302+ return (i18n.tr("%1m"))
303+ .arg(minute)
304+ }
305+
306+ else {
307+ return Podcasts.formatTime(model.duration)
308+ }
309+ }
310+
311+ Rectangle {
312+ id: listened
313+ border.color: UbuntuColors.lightGrey
314+ height: units.gu(2.5)
315+ width: height
316+ radius: width / 2
317+ anchors.right: durationIcon.left
318+ anchors.rightMargin: units.gu(2)
319+ visible: model.listened
320+ Icon {
321+ id: tick
322+ name: "tick"
323+ anchors.centerIn: parent
324+ anchors.verticalCenterOffset: units.gu(0.1)
325+ height: units.gu(1.4)
326+ width: height
327+ }
328+ }
329+
330+ Icon {
331+ id: durationIcon
332+ width: units.gu(2.5)
333+ height: width
334+ name: "alarm-clock"
335+ visible: duration.text !== ""
336+ anchors.right: duration.left
337+ anchors.rightMargin: units.gu(0.5)
338+ }
339+
340+ Label {
341+ id: duration
342+ anchors.right: parent.right
343+ anchors.verticalCenter: durationIcon.verticalCenter
344+ fontSize: "small"
345+ text: !isNaN(model.duration) && model.duration !== 0 ? statusBox.formatTime(model.duration) : ""
346+ }
347+
348+ Row {
349+ id: actionRow
350+
351+ spacing: units.gu(2)
352+ anchors.left: parent.left
353+
354+ Icon {
355+ id: playButton
356+ name: player.playbackState === MediaPlayer.PlayingState && currentGuid === model.guid ? "media-playback-pause"
357+ : "media-playback-start"
358+ width: units.gu(2.5)
359+ height: width
360+ MouseArea {
361+ anchors.fill: parent
362+
363+ onClicked: {
364+ var db = Podcasts.init();
365+ db.transaction(function (tx) {
366+ if (currentGuid === model.guid) {
367+ if (player.playbackState === MediaPlayer.PlayingState) {
368+ player.pause()
369+ } else {
370+ player.play()
371+ }
372+ } else {
373+ currentGuid = "";
374+ player.source = model.downloadedfile ? model.downloadedfile : model.audiourl;
375+ var rs = tx.executeSql("SELECT position FROM Episode WHERE guid=?", [model.guid]);
376+ player.play();
377+ player.seek(rs.rows.item(0).position);
378+ currentName = model.name;
379+ currentArtist = model.artist;
380+ currentImage = model.image;
381+ currentGuid = model.guid;
382+ }
383+ });
384+ }
385+ }
386+ }
387+
388+ Item {
389+ id: downloadButton
390+
391+ width: units.gu(2.5)
392+ height: width
393+
394+ ActivityIndicator {
395+ anchors.centerIn: parent
396+ visible: downloader.downloadingGuid === model.guid
397+ running: visible
398+ }
399+
400+ Icon {
401+ anchors.fill: parent
402+ property bool queued: false;
403+ name: model.downloadedfile ? "delete" : (queued && downloader.downloadingGuid !== model.guid ? "history" : "save")
404+ width: units.gu(4)
405+ height: width
406+ opacity: downloader.downloadingGuid === model.guid ? 0.4 : 1.0
407+
408+ MouseArea {
409+ anchors.fill: parent
410+ enabled: downloader.downloadingGuid !== model.guid
411+
412+ onClicked: {
413+ if (model.downloadedfile) {
414+ fileManager.deleteFile(model.downloadedfile);
415+ var db = Podcasts.init();
416+ db.transaction(function (tx) {
417+ tx.executeSql("UPDATE Episode SET downloadedfile = NULL WHERE guid = ?", [model.guid]);
418+ });
419+ loadEpisodes(episodeModel.pid, episodeModel.artist, episodeModel.image);
420+ } else {
421+ parent.queued = true;
422+ downloader.addDownload(model.guid, model.audiourl);
423+ }
424+ }
425+ }
426+ }
427+ }
428+ }
429+ }
430+ }
431+ }
432+
433+ PullToRefresh {
434+ refreshing: episodesUpdating
435+ onRefresh: updateEpisodes();
436+ }
437+ }
438+
439+ function refreshModel() {
440+ var db = Podcasts.init();
441+ loadEpisodes(episodeModel.pid, episodeModel.artist, episodeModel.image);
442+ episodesUpdating = false;
443+ }
444+
445+ function loadEpisodes(pid, artist, img) {
446+ var db = Podcasts.init();
447+ db.transaction(function (tx) {
448+ episodeModel.clear();
449+ var rs = tx.executeSql("SELECT rowid, * FROM Episode WHERE podcast=? ORDER BY published DESC", [pid]);
450+ for(var i = 0; i < rs.rows.length; i++) {
451+ var episode = rs.rows.item(i);
452+ episodeModel.pid = pid;
453+ episodeModel.artist = artist;
454+ episodeModel.image = img;
455+ episodeModel.append({"guid" : episode.guid, "listened" : episode.listened, "name" : episode.name, "description" : episode.description, "duration" : episode.duration, "position" : episode.position, "downloadedfile" : episode.downloadedfile, "image" : img, "artist" : artist, "audiourl" : episode.audiourl});
456+ }
457+ });
458+ }
459+
460+ function updateEpisodes() {
461+ var db = Podcasts.init();
462+ episodesUpdating = true;
463+ db.transaction(function(tx) {
464+ var rs = tx.executeSql("SELECT rowid, feed FROM Podcast");
465+ tx.executeSql("UPDATE Podcast SET lastupdate=CURRENT_TIMESTAMP");
466+ var xhr = [];
467+ for(var i = 0; i < rs.rows.length; i++) {
468+ (function (i) {
469+ xhr[i] = new XMLHttpRequest;
470+ var url = rs.rows.item(i).feed;
471+ var pid = rs.rows.item(i).rowid;
472+ xhr[i].open("GET", url);
473+ xhr[i].onreadystatechange = function() {
474+ if (xhr[i].readyState === XMLHttpRequest.DONE) {
475+ var e = xhr[i].responseXML.documentElement;
476+ for(var h = 0; h < e.childNodes.length; h++) {
477+ if(e.childNodes[h].nodeName === "channel") {
478+ var c = e.childNodes[h];
479+ for(var j = 0; j < c.childNodes.length; j++) {
480+ if(c.childNodes[j].nodeName === "item") {
481+ var t = c.childNodes[j];
482+ var track = {}
483+ for(var k = 0; k < t.childNodes.length; k++) {
484+ try {
485+ var nodeName = t.childNodes[k].nodeName.toLowerCase();
486+ if (nodeName === "title") track['name'] = t.childNodes[k].childNodes[0].nodeValue;
487+ else if (nodeName === "description") track['description'] = t.childNodes[k].childNodes[0].nodeValue;
488+ else if (nodeName === "guid") track['guid'] = t.childNodes[k].childNodes[0].nodeValue;
489+ else if (nodeName === "pubdate") track['published'] = new Date(t.childNodes[k].childNodes[0].nodeValue).getTime();
490+ else if (nodeName === "duration") {
491+ var dur = t.childNodes[k].childNodes[0].nodeValue.split(":");
492+ if (dur.length === 1) {
493+ track['duration'] = parseInt(dur[0]);
494+ } else if (dur.length === 2) {
495+ track['duration'] = parseInt(dur[0]) * 60 + parseInt(dur[1]);
496+ } else if (dur.length === 3) {
497+ track['duration'] = parseInt(dur[0]) * 3600 + parseInt(dur[1]) * 60 + parseInt(dur[2]);
498+ }
499+ } else if (nodeName === "enclosure") {
500+ var el = t.childNodes[k];
501+ for (var l = 0; l < el.attributes.length; l++) {
502+ if(el.attributes[l].nodeName === "url") track['audiourl'] = el.attributes[l].nodeValue;
503+ }
504+ }
505+ } catch(err) {
506+ console.debug(err.message);
507+ }
508+ }
509+ if (!track.hasOwnProperty("guid")) {
510+ track['guid'] = track.audiourl;
511+ }
512+
513+ db.transaction(function(tx2) {
514+ var ers = tx2.executeSql("SELECT rowid FROM Episode WHERE guid=?", [track.guid]);
515+ if (ers.rows.length === 0) {
516+ tx2.executeSql("INSERT INTO Episode(podcast, name, description, audiourl, guid, listened, duration, published) VALUES(?, ?, ? , ?, ?, ?, ?, ?)", [pid,
517+ track.name,
518+ track.description,
519+ track.audiourl,
520+ track.guid,
521+ false,
522+ track.duration,
523+ track.published]);
524+ }
525+ });
526+ }
527+ }
528+ }
529+ }
530+ }
531+ refreshModel();
532+ }
533+ xhr[i].send();
534+
535+ })(i);
536+ }
537+ });
538+ }
539+}
540
541=== modified file 'app/ui/PodcastsTab.qml'
542--- app/ui/PodcastsTab.qml 2015-01-24 22:45:46 +0000
543+++ app/ui/PodcastsTab.qml 2015-01-25 18:02:01 +0000
544@@ -18,15 +18,19 @@
545
546 import QtQuick 2.0
547 import QtMultimedia 5.0
548+import QtQuick.Layouts 1.1
549 import QtQuick.LocalStorage 2.0
550 import Ubuntu.Components 1.1
551 import Ubuntu.DownloadManager 0.1
552+import Ubuntu.Components.ListItems 1.0 as ListItem
553 import Ubuntu.Components.Popups 1.0
554 import "../podcasts.js" as Podcasts
555
556 Tab {
557 id: tab
558+
559 title: i18n.tr("Podcasts")
560+
561 property bool episodesUpdating: false;
562 property bool addPodcast: false;
563
564@@ -35,41 +39,10 @@
565 Action {
566 text: i18n.tr("Add Podcast")
567 iconName: "add"
568- visible: view.model === podcastModel && !addPodcast
569+ visible: !addPodcast
570 onTriggered: {
571 addPodcast = true;
572 }
573- },
574-
575- Action {
576- text: i18n.tr("Up")
577- iconName: "up"
578- visible: view.model === episodeModel
579- onTriggered: {
580- page.title = i18n.tr("Podcasts");
581- view.model = podcastModel;
582- refreshModel();
583- }
584- },
585-
586- Action {
587- text: i18n.tr("Unsubscribe")
588- iconName: "delete"
589- visible: view.model === episodeModel
590- onTriggered: {
591- var db = Podcasts.init();
592- db.transaction(function (tx) {
593- var rs = tx.executeSql("SELECT downloadedfile FROM Episode WHERE downloadedfile NOT NULL AND podcast=?", [episodeModel.pid]);
594- for(var i = 0; i < rs.rows.length; i++) {
595- fileManager.deleteFile(rs.rows.item(i).downloadedfile);
596- }
597- tx.executeSql("DELETE FROM Episode WHERE podcast=?", [episodeModel.pid]);
598- tx.executeSql("DELETE FROM Podcast WHERE rowid=?", [episodeModel.pid]);
599- page.title = i18n.tr("Podcasts");
600- view.model = podcastModel;
601- refreshModel();
602- });
603- }
604 }
605 ]
606
607@@ -79,37 +52,6 @@
608 }
609 }
610
611- SingleDownload {
612- id: downloader
613- property var queue: []
614- property string downloadingGuid
615-
616- onFinished: {
617- var db = Podcasts.init();
618- var finalLocation = fileManager.saveDownload(path);
619- db.transaction(function (tx) {
620- tx.executeSql("UPDATE Episode SET downloadedfile=? WHERE guid=?", [finalLocation, downloadingGuid]);
621- queue.shift();
622- if (queue.length > 0) {
623- downloadingGuid = queue[0][0];
624- download(queue[0][1]);
625- } else {
626- downloadingGuid = "";
627- }
628-
629- loadEpisodes(episodeModel.pid, episodeModel.artist, episodeModel.image);
630- });
631- }
632-
633- function addDownload(guid, url) {
634- queue.push([guid, url]);
635- if (queue.length == 1) {
636- downloadingGuid = guid;
637- download(url);
638- }
639- }
640- }
641-
642 Component {
643 id: subscribeFailedDialog
644 Dialog {
645@@ -144,196 +86,90 @@
646
647 ListView {
648 id: view
649+
650+ clip: true
651+ model: podcastModel
652 anchors.fill: parent
653- anchors.margins: units.gu(2)
654- anchors.bottomMargin: 0
655- model: podcastModel
656- clip: true
657- spacing: units.gu(1)
658+
659 footer: Item {
660 width: parent.width
661 height: units.gu(8)
662 }
663
664- delegate: Rectangle {
665+ delegate: ListItem.Empty {
666 id: listItem
667- height: Math.max(imgFrame.height, detailCol.height)
668- width: parent.width
669- color: Theme.palette.normal.background
670- property bool expanded: false;
671-
672- MouseArea {
673- anchors.fill: parent
674- onClicked: {
675- if (view.model === podcastModel) {
676- page.title = model.name
677- loadEpisodes(model.id, model.artist, model.image)
678- view.model = episodeModel
679- } else {
680- listItem.expanded = !listItem.expanded
681+
682+ property bool expanded: false
683+
684+ height: units.gu(8)
685+ removable: true
686+ confirmRemoval: true
687+ onItemRemoved: {
688+ var db = Podcasts.init();
689+ db.transaction(function (tx) {
690+ var rs = tx.executeSql("SELECT downloadedfile FROM Episode WHERE downloadedfile NOT NULL AND podcast=?", [model.id]);
691+ for(var i = 0; i < rs.rows.length; i++) {
692+ fileManager.deleteFile(rs.rows.item(i).downloadedfile);
693 }
694- }
695- }
696-
697- UbuntuShape {
698- id: imgFrame
699- width: units.gu(9.1)
700- height: width
701+ tx.executeSql("DELETE FROM Episode WHERE podcast=?", [model.id]);
702+ tx.executeSql("DELETE FROM Podcast WHERE rowid=?", [model.id]);
703+ refreshModel()
704+ });
705+ }
706+
707+ onClicked: {
708+ mainStack.push(Qt.resolvedUrl("EpisodesPage.qml"), {"episodeName": model.name, "episodeId": model.id, "episodeArtist": model.artist, "episodeImage": model.image})
709+ }
710+
711+ Column {
712+ id: mainColumn
713
714 anchors.left: parent.left
715- image: Image {
716- source: model.image
717- }
718- }
719-
720- Column {
721- id: detailCol
722- anchors.left: imgFrame.right
723- anchors.leftMargin: units.gu(2)
724 anchors.right: parent.right
725- anchors.rightMargin: units.gu(2)
726- spacing: units.gu(0.5)
727-
728- Row {
729- width: parent.width
730- spacing: units.gu(1)
731-
732- Label {
733- textFormat: Text.PlainText
734- text: model.name.trim()
735- width: parent.width - episodeCount.width - units.gu(1)
736- elide: Text.ElideRight
737- }
738-
739- Label {
740- id: episodeCount
741- width: units.gu(4)
742- visible: view.model === episodeModel || model.episodeCount > 0
743- text: view.model === episodeModel ? (!isNaN(model.duration) && model.duration !== 0 ? Podcasts.formatTime(model.duration) : "") : model.episodeCount
744- horizontalAlignment: Text.AlignRight
745- fontSize: "small"
746- }
747- }
748-
749- Row {
750- width: parent.width
751- spacing: units.gu(1)
752-
753- Label {
754- id: desc
755- text: view.model === episodeModel ? model.description : model.artist
756- textFormat: Text.RichText
757- clip: true
758- height: listItem.expanded ? contentHeight : units.gu(2)
759- wrapMode: Text.WordWrap
760- width: parent.width - listened.width - units.gu(1)
761- elide: Text.ElideRight
762- fontSize: "small"
763-
764- Behavior on height {
765- UbuntuNumberAnimation {
766- duration: UbuntuAnimation.SlowDuration
767- }
768- }
769-
770- }
771-
772- Rectangle {
773- id: listened
774- border.color: UbuntuColors.lightGrey
775- height: units.gu(2)
776- width: height
777- radius: width / 2
778- visible: view.model === episodeModel && model.listened
779- Icon {
780- id: tick
781- name: "tick"
782- anchors.centerIn: parent
783- anchors.verticalCenterOffset: units.gu(0.1)
784- height: units.gu(1.4)
785- width: height
786- }
787- }
788- }
789-
790- Row {
791+ anchors.margins: units.gu(2)
792+ anchors.verticalCenter: parent.verticalCenter
793+ spacing: units.gu(1)
794+
795+ RowLayout {
796+ id: titleRow
797+
798 width: parent.width
799 spacing: units.gu(2)
800- Icon {
801- name: player.playbackState === MediaPlayer.PlayingState && currentGuid === model.guid ? "media-playback-pause"
802- : "media-playback-start"
803- visible: view.model === episodeModel
804- width: units.gu(4)
805+
806+ Image {
807+ id: imgFrame
808+ width: units.gu(5)
809 height: width
810- MouseArea {
811- anchors.fill: parent
812-
813- onClicked: {
814- var db = Podcasts.init();
815- db.transaction(function (tx) {
816- if (currentGuid === model.guid) {
817- if (player.playbackState === MediaPlayer.PlayingState) {
818- player.pause()
819- } else {
820- player.play()
821- }
822- } else {
823- currentGuid = "";
824- player.source = model.downloadedfile ? model.downloadedfile : model.audiourl;
825- var rs = tx.executeSql("SELECT position FROM Episode WHERE guid=?", [model.guid]);
826- player.play();
827- player.seek(rs.rows.item(0).position);
828- currentName = model.name;
829- currentArtist = model.artist;
830- currentImage = model.image;
831- currentGuid = model.guid;
832- }
833- });
834- }
835- }
836+ sourceSize.height: width
837+ sourceSize.width: width
838+ source: model.image
839 }
840
841- Item {
842- width: units.gu(4)
843- height: width
844-
845- ActivityIndicator {
846- anchors.centerIn: parent
847- visible: downloader.downloadingGuid === model.guid
848- running: visible
849+ Column {
850+ id: detailColumn
851+
852+ anchors.verticalCenter: imgFrame.verticalCenter
853+ Layout.fillWidth: true
854+
855+ Label {
856+ id: podcastTitle
857+ textFormat: Text.PlainText
858+ text: model.name.trim()
859+ width: parent.width
860+ fontSize: "small"
861+ elide: Text.ElideRight
862 }
863
864- Icon {
865- anchors.fill: parent
866- property bool queued: false;
867- name: model.downloadedfile ? "delete" : (queued && downloader.downloadingGuid !== model.guid ? "history" : "save")
868- width: units.gu(4)
869- height: width
870- visible: view.model === episodeModel
871- opacity: downloader.downloadingGuid === model.guid ? 0.4 : 1.0
872-
873- MouseArea {
874- anchors.fill: parent
875- enabled: downloader.downloadingGuid !== model.guid
876-
877- onClicked: {
878- if (model.downloadedfile) {
879- fileManager.deleteFile(model.downloadedfile);
880- var db = Podcasts.init();
881- db.transaction(function (tx) {
882- tx.executeSql("UPDATE Episode SET downloadedfile = NULL WHERE guid = ?", [model.guid]);
883- });
884- loadEpisodes(episodeModel.pid, episodeModel.artist, episodeModel.image);
885- } else {
886- parent.queued = true;
887- downloader.addDownload(model.guid, model.audiourl);
888- }
889- }
890- }
891+ Label {
892+ id: episodeCount
893+ width: parent.width
894+ color: "#999999"
895+ visible: model.episodeCount > 0
896+ text: model.episodeCount + " Episodes"
897+ fontSize: "x-small"
898 }
899 }
900 }
901-
902 }
903 }
904
905@@ -427,41 +263,22 @@
906 function refreshModel() {
907 var db = Podcasts.init();
908
909- if (view.model === podcastModel) {
910- db.transaction(function (tx) {
911- podcastModel.clear();
912- var rs = tx.executeSql("SELECT rowid, * FROM Podcast ORDER BY name ASC");
913- for(var i = 0; i < rs.rows.length; i++) {
914- var podcast = rs.rows.item(i);
915- var rs2 = tx.executeSql("SELECT Count(*) AS epcount FROM Episode WHERE podcast=? AND NOT listened", [rs.rows.item(i).rowid]);
916- podcastModel.append({"id" : podcast.rowid, "name" : podcast.name, "artist" : podcast.artist, "image" : podcast.image, "episodeCount" : rs2.rows.item(0).epcount});
917- if (podcast.lastupdate === null && !episodesUpdating) {
918- updateEpisodes();
919- }
920+ db.transaction(function (tx) {
921+ podcastModel.clear();
922+ var rs = tx.executeSql("SELECT rowid, * FROM Podcast ORDER BY name ASC");
923+ for(var i = 0; i < rs.rows.length; i++) {
924+ var podcast = rs.rows.item(i);
925+ var rs2 = tx.executeSql("SELECT Count(*) AS epcount FROM Episode WHERE podcast=? AND NOT listened", [rs.rows.item(i).rowid]);
926+ podcastModel.append({"id" : podcast.rowid, "name" : podcast.name, "artist" : podcast.artist, "image" : podcast.image, "episodeCount" : rs2.rows.item(0).epcount});
927+ if (podcast.lastupdate === null && !episodesUpdating) {
928+ updateEpisodes();
929 }
930- });
931- } else {
932- loadEpisodes(episodeModel.pid, episodeModel.artist, episodeModel.image);
933- }
934+ }
935+ });
936
937 episodesUpdating = false;
938 }
939
940- function loadEpisodes(pid, artist, img) {
941- var db = Podcasts.init();
942- db.transaction(function (tx) {
943- episodeModel.clear();
944- var rs = tx.executeSql("SELECT rowid, * FROM Episode WHERE podcast=? ORDER BY published DESC", [pid]);
945- for(var i = 0; i < rs.rows.length; i++) {
946- var episode = rs.rows.item(i);
947- episodeModel.pid = pid;
948- episodeModel.artist = artist;
949- episodeModel.image = img;
950- episodeModel.append({"guid" : episode.guid, "listened" : episode.listened, "name" : episode.name, "description" : episode.description, "duration" : episode.duration, "position" : episode.position, "downloadedfile" : episode.downloadedfile, "image" : img, "artist" : artist, "audiourl" : episode.audiourl});
951- }
952- });
953- }
954-
955 function subscribeFromFeed(feed) {
956 var xhr = new XMLHttpRequest;
957 if (feed.indexOf("://") === -1) {
958
959=== modified file 'app/ui/SearchTab.qml'
960--- app/ui/SearchTab.qml 2015-01-10 10:10:43 +0000
961+++ app/ui/SearchTab.qml 2015-01-25 18:02:01 +0000
962@@ -17,8 +17,10 @@
963 */
964
965 import QtQuick 2.0
966+import QtQuick.Layouts 1.1
967 import Ubuntu.Components 1.1
968 import QtQuick.LocalStorage 2.0
969+import Ubuntu.Components.ListItems 1.0 as ListItem
970 import "../podcasts.js" as Podcasts
971
972 Tab {
973@@ -30,12 +32,13 @@
974 Column {
975 spacing: units.gu(2)
976 anchors.fill: parent
977- anchors.margins: units.gu(2)
978- anchors.bottomMargin: 0
979+ anchors.topMargin: units.gu(2)
980
981 TextField {
982 id: searchField
983- width: parent.width
984+ anchors.left: parent.left
985+ anchors.right: parent.right
986+ anchors.margins: units.gu(2)
987 placeholderText: i18n.tr("Search...")
988 inputMethodHints: Qt.ImhNoPredictiveText;
989 onTextChanged: {
990@@ -48,50 +51,62 @@
991 }
992
993 ListView {
994+ clip: true
995 width: parent.width
996 model: searchResults
997 height: parent.height - searchField.height - units.gu(2)
998- clip: true
999- spacing: units.gu(1)
1000+
1001 footer: Item {
1002 width: parent.width
1003 height: units.gu(7)
1004 }
1005
1006- delegate: Item {
1007-
1008- height: imgFrame.height
1009- width: parent.width
1010-
1011- UbuntuShape {
1012- id: imgFrame
1013- width: units.gu(9.1)
1014- height: width
1015+ delegate: ListItem.Empty {
1016+
1017+ height: units.gu(8)
1018+
1019+ RowLayout {
1020+ id: titleRow
1021
1022 anchors.left: parent.left
1023- image: Image {
1024+ anchors.right: parent.right
1025+ anchors.margins: units.gu(2)
1026+ anchors.verticalCenter: parent.verticalCenter
1027+
1028+ spacing: units.gu(2)
1029+
1030+ Image {
1031+ id: imgFrame
1032+ width: units.gu(5)
1033+ height: width
1034+ sourceSize.height: width
1035+ sourceSize.width: width
1036 source: model.image
1037 }
1038- }
1039-
1040- Column {
1041- anchors.left: imgFrame.right
1042- anchors.leftMargin: units.gu(2)
1043- anchors.right: parent.right
1044- anchors.rightMargin: units.gu(2)
1045- spacing: units.gu(0.5)
1046-
1047- Label {
1048- text: model.name
1049- elide: Text.ElideRight
1050- width: parent.width
1051- }
1052-
1053- Label {
1054- text: model.artist
1055- width: parent.width
1056- elide: Text.ElideRight
1057- fontSize: "small"
1058+
1059+ Column {
1060+ id: detailColumn
1061+
1062+ anchors.verticalCenter: imgFrame.verticalCenter
1063+ Layout.fillWidth: true
1064+
1065+ Label {
1066+ id: podcastTitle
1067+ textFormat: Text.PlainText
1068+ text: model.name
1069+ width: parent.width
1070+ fontSize: "small"
1071+ elide: Text.ElideRight
1072+ }
1073+
1074+ Label {
1075+ id: episodeCount
1076+ width: parent.width
1077+ color: "#999999"
1078+ text: model.artist
1079+ fontSize: "x-small"
1080+ elide: Text.ElideRight
1081+ }
1082 }
1083
1084 Button {
1085@@ -111,6 +126,7 @@
1086 }
1087 }
1088
1089+
1090 ListModel {
1091 id: searchResults
1092 }
1093@@ -124,9 +140,9 @@
1094 var json = JSON.parse(xhr.responseText);
1095 for(var i in json.results) {
1096 searchResults.append({"name" : json.results[i].trackName,
1097- "artist" : json.results[i].artistName,
1098- "feed" : json.results[i].feedUrl,
1099- "image" : json.results[i].artworkUrl100});
1100+ "artist" : json.results[i].artistName,
1101+ "feed" : json.results[i].feedUrl,
1102+ "image" : json.results[i].artworkUrl100});
1103 }
1104 }
1105 }
1106
1107=== modified file 'po/com.mikeasoft.podbird.pot'
1108--- po/com.mikeasoft.podbird.pot 2015-01-24 22:45:46 +0000
1109+++ po/com.mikeasoft.podbird.pot 2015-01-25 18:02:01 +0000
1110@@ -8,7 +8,7 @@
1111 msgstr ""
1112 "Project-Id-Version: \n"
1113 "Report-Msgid-Bugs-To: \n"
1114-"POT-Creation-Date: 2015-01-24 22:45+0000\n"
1115+"POT-Creation-Date: 2015-01-25 19:00+0100\n"
1116 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
1117 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
1118 "Language-Team: LANGUAGE <LL@li.org>\n"
1119@@ -17,69 +17,96 @@
1120 "Content-Type: text/plain; charset=CHARSET\n"
1121 "Content-Transfer-Encoding: 8bit\n"
1122
1123-#: ../app/ui/PodcastsTab.qml:29 ../app/ui/PodcastsTab.qml:49
1124-#: ../app/ui/PodcastsTab.qml:68
1125+#: ../app/ui/EpisodesPage.qml:45
1126+msgid "Unsubscribe"
1127+msgstr ""
1128+
1129+#: ../app/ui/EpisodesPage.qml:57
1130+msgid "Unsubscribe Confirmation"
1131+msgstr ""
1132+
1133+#: ../app/ui/EpisodesPage.qml:58
1134+#, qt-format
1135+msgid "Are you sure you want to unsubscribe from <b>%1</b>?"
1136+msgstr ""
1137+
1138+#: ../app/ui/EpisodesPage.qml:60
1139+msgid "Yes"
1140+msgstr ""
1141+
1142+#: ../app/ui/EpisodesPage.qml:77
1143+msgid "No"
1144+msgstr ""
1145+
1146+#: ../app/ui/EpisodesPage.qml:196
1147+#, qt-format
1148+msgid "%1h %2m"
1149+msgstr ""
1150+
1151+#: ../app/ui/EpisodesPage.qml:202
1152+#, qt-format
1153+msgid "%1h"
1154+msgstr ""
1155+
1156+#: ../app/ui/EpisodesPage.qml:207
1157+#, c-format, qt-format
1158+msgid "%1m"
1159+msgstr ""
1160+
1161+#: ../app/ui/PodcastsTab.qml:32
1162 msgid "Podcasts"
1163 msgstr ""
1164
1165-#: ../app/ui/PodcastsTab.qml:36
1166+#: ../app/ui/PodcastsTab.qml:40
1167 msgid "Add Podcast"
1168 msgstr ""
1169
1170-#: ../app/ui/PodcastsTab.qml:45
1171-msgid "Up"
1172-msgstr ""
1173-
1174-#: ../app/ui/PodcastsTab.qml:56
1175-msgid "Unsubscribe"
1176-msgstr ""
1177-
1178-#: ../app/ui/PodcastsTab.qml:117
1179+#: ../app/ui/PodcastsTab.qml:59
1180 msgid "Unable to subscribe"
1181 msgstr ""
1182
1183-#: ../app/ui/PodcastsTab.qml:118
1184+#: ../app/ui/PodcastsTab.qml:60
1185 msgid "Please check the URL and try again"
1186 msgstr ""
1187
1188-#: ../app/ui/PodcastsTab.qml:120
1189+#: ../app/ui/PodcastsTab.qml:62
1190 msgid "Close"
1191 msgstr ""
1192
1193-#: ../app/ui/PodcastsTab.qml:130
1194+#: ../app/ui/PodcastsTab.qml:72
1195 msgid "No Podcast Subscriptions"
1196 msgstr ""
1197
1198-#: ../app/ui/PodcastsTab.qml:131
1199+#: ../app/ui/PodcastsTab.qml:73
1200 msgid ""
1201 "You haven't subscribed to any podcasts yet, visit the 'Search' page to add "
1202 "some."
1203 msgstr ""
1204
1205-#: ../app/ui/PodcastsTab.qml:386
1206+#: ../app/ui/PodcastsTab.qml:222
1207 msgid "Feed URL..."
1208 msgstr ""
1209
1210-#: ../app/ui/PodcastsTab.qml:400
1211+#: ../app/ui/PodcastsTab.qml:236
1212 msgid "Cancel"
1213 msgstr ""
1214
1215-#: ../app/ui/PodcastsTab.qml:410
1216+#: ../app/ui/PodcastsTab.qml:246
1217 msgid "Add"
1218 msgstr ""
1219
1220-#: ../app/ui/SearchTab.qml:25
1221+#: ../app/ui/SearchTab.qml:27
1222 msgid "Search"
1223 msgstr ""
1224
1225-#: ../app/ui/SearchTab.qml:39
1226+#: ../app/ui/SearchTab.qml:42
1227 msgid "Search..."
1228 msgstr ""
1229
1230-#: ../app/ui/SearchTab.qml:99
1231+#: ../app/ui/SearchTab.qml:114
1232 msgid "Subscribe"
1233 msgstr ""
1234
1235-#: /home/mike/src/build-podbird-Desktop-Default/po/Podbird.desktop.in.h:1
1236+#: /home/krnekhelesh/Documents/Ubuntu-Projects/MP-Reviews/builddir/build-new-listitem-actions-UbuntuSDK_for_armhf_GCC_ubuntu_sdk_14_10_utopic-Default/po/Podbird.desktop.in.h:1
1237 msgid "Podbird"
1238 msgstr ""

Subscribers

People subscribed via source and target branches