git.iko.soy/maciek134/podphoenix
on main help sign in
Merged devel branch rev 128 (queue support)
commit0922b257fdced343c0d835610a9e371974c6a268 authorNekhelesh Ramananthan <krnekhelesh@gmail.com> dateSat 19 Mar 2016, 14:27 (11 years ago) parent703146d
8 files changed+639−277
app/podbird.qml+108−41 app/podcasts.js+99 app/ui/EpisodesPage.qml+15−17 app/ui/EpisodesTab.qml+15−17 app/ui/NowPlayingPage.qml+301−195 app/ui/PlayerControls.qml+6−6 app/ui/PodcastsTab.qml+1−1 app/ui/Queue.qml+94
app/podbird.qml+108−41
@@ -19,7 +19,7 @@
1919import QtQuick 2.4
2020import Podbird 1.0
2121import UserMetrics 0.1
22−import QtMultimedia 5.4
22+import QtMultimedia 5.6
2323import Ubuntu.Connectivity 1.0
2424import Qt.labs.settings 1.0
2525import Ubuntu.Components 1.3
@@ -50,6 +50,7 @@ MainView {
5050 db.transaction(function (tx) {
5151 tx.executeSql('UPDATE Episode SET queued=0 WHERE queued=1');
5252 })
53+ Podcasts.clearQueue()
5354 }
5455
5556 // RefreshModel function to call refreshModel() function of the tab currently
@@ -167,55 +168,121 @@ MainView {
167168 }
168169 }
169170
170− // UserMetrics to show Podbird stats on welcome screen
171− Metric {
172− id: podcastsMetric
173− name: "podcast-metrics"
174− // TRANSLATORS: this refers to a number of songs greater than one. The actual number will be prepended to the string automatically (plural forms are not yet fully supported in usermetrics, the library that displays that string)
175− format: i18n.tr("Podcasts listened to today: <b>%1</b>")
176− emptyFormat: i18n.tr("No podcasts listened to today")
177− domain: "com.mikeasoft.podbird"
178− }
171+ MediaPlayer {
172+ id: player
173+
174+ // Wrapper function around decodeURIComponent() to prevent exceptions
175+ // from bubbling up to the app.
176+ function decodeFileURI(filename)
177+ {
178+ var newFilename = "";
179+ try {
180+ newFilename = decodeURIComponent(filename);
181+ } catch (e) {
182+ newFilename = filename;
183+ console.log("Unicode decoding error:", filename, e.message)
184+ }
179185
180− // Load the media player only when the user starts to play some media. This
181− // should improve app-startup slightly.
182− Loader {
183− id: playerLoader
184− sourceComponent: currentUrl != "" ? playerComponent : undefined
185− }
186+ return newFilename;
187+ }
186188
187− Component {
188− id: playerComponent
189− MediaPlayer {
190− id: player
189+ function metaForSource(source) {
190+ var blankMeta = {
191+ name: "",
192+ artist: "",
193+ image: "",
194+ guid: "",
195+ }
191196
192− property bool podcastCounted: false
197+ source = source.toString()
193198
194− source: currentUrl
199+ if (source.indexOf("file://") === 0) {
200+ source = source.substring(7);
201+ }
195202
196− onSourceChanged: {
197− podcastCounted = false
203+ return Podcasts.lookup(decodeFileURI(source)) || blankMeta;
204+ }
205+
206+ function toggle() {
207+ if (playbackState === MediaPlayer.PlayingState) {
208+ pause()
209+ } else {
210+ play()
198211 }
212+ }
199213
200− onPositionChanged: {
201− if (currentGuid == "" || duration <= 0) {
202− return;
203− }
214+ function playEpisode(guid, image, name, artist, url) {
215+ // Clear current queue
216+ player.playlist.clear()
217+ Podcasts.clearQueue()
218+
219+ // Add episode to queue
220+ player.playlist.addItem(Qt.resolvedUrl(url))
221+ Podcasts.addItemToQueue(guid, image, name, artist, url)
222+
223+ // Play episode
224+ player.play()
225+ }
204226
205− if (position > 10000 && !podcastCounted) {
206− podcastCounted = true
207− podcastsMetric.increment()
208− console.log("[LOG]: Podcast User metric incremented")
227+ function addEpisodeToQueue(guid, image, name, artist, url) {
228+ player.playlist.addItem(Qt.resolvedUrl(url))
229+ Podcasts.addItemToQueue(guid, image, name, artist, url)
230+
231+ // If added episode is the first one in the queue, then set the current metadata
232+ // so that the bottom player controls will be shown, allowing the user to play
233+ // the episode if he chooses to.
234+ if (player.playlist.itemCount === 0) {
235+ currentGuid = guid
236+ currentName = name
237+ currentArtist = artist
238+ currentImage = image
239+ currentUrl = url
240+ }
241+ }
242+
243+ property bool endOfMedia: false
244+ property double progress: 0
245+
246+ playlist: Playlist {
247+ playbackMode: Playlist.Sequential
248+
249+ readonly property bool canGoPrevious: currentIndex !== 0
250+ readonly property bool canGoNext: currentIndex !== itemCount - 1
251+
252+ onCurrentItemSourceChanged: {
253+ var meta = player.metaForSource(currentItemSource)
254+ currentGuid = "";
255+ currentName = meta.name
256+ currentArtist = meta.artist
257+ currentImage = meta.image
258+ currentGuid = meta.guid
259+ }
260+ }
261+
262+ onStatusChanged: {
263+ if (status === MediaPlayer.EndOfMedia) {
264+ console.log("[LOG]: End of Media. Stopping.")
265+ endOfMedia = true
266+ stop()
267+ }
268+ }
269+
270+ onStopped: {
271+ if (playlist.itemCount > 0) {
272+ if (endOfMedia) {
273+ // We just ended media, so jump to start of playlist
274+ playlist.currentIndex = 0;
275+
276+ // Play then pause otherwise when we come from EndOfMedia
277+ // it calls next() until EndOfMedia again.
278+ play()
209279 }
210280
211− var db = Podcasts.init();
212− db.transaction(function (tx) {
213− tx.executeSql("UPDATE Episode SET position=? WHERE guid=?", [position >= duration ? 120 : position, currentGuid]);
214− if (position >= duration - 120) {
215− tx.executeSql("UPDATE Episode SET listened = 1 WHERE guid=?", [currentGuid]);
216− }
217− });
281+ pause()
218282 }
283+
284+ // Always reset endOfMedia
285+ endOfMedia = false
219286 }
220287 }
221288
@@ -290,13 +357,13 @@ MainView {
290357 states: [
291358 State {
292359 name: "shown"
293− when: currentUrl != "" && !mainStack.currentPage.isNowPlayingPage
360+ when: player.playlist.itemCount !== 0 && !mainStack.currentPage.isNowPlayingPage
294361 PropertyChanges { target: playerControlLoader; anchors.bottomMargin: 0 }
295362 },
296363
297364 State {
298365 name: "hidden"
299− when: currentUrl == "" || mainStack.currentPage.isNowPlayingPage || !playerControl.visible
366+ when: player.playlist.itemCount === 0 || mainStack.currentPage.isNowPlayingPage || !playerControl.visible
300367 PropertyChanges { target: playerControlLoader; anchors.bottomMargin: -units.gu(7) }
301368 }
302369 ]
app/podcasts.js+99
@@ -22,6 +22,7 @@ function init() {
2222 db.transaction(function(tx) {
2323 tx.executeSql('CREATE TABLE IF NOT EXISTS Podcast(artist TEXT, name TEXT, description TEXT, feed TEXT, image TEXT, lastupdate TIMESTAMP)');
2424 tx.executeSql('CREATE TABLE IF NOT EXISTS Episode(guid TEXT, podcast INTEGER, name TEXT, subtitle TEXT, description TEXT, duration INTEGER, audiourl TEXT, downloadedfile TEXT, published TIMESTAMP, queued BOOLEAN, listened BOOLEAN, favourited BOOLEAN, position INTEGER, FOREIGN KEY(podcast) REFERENCES Podcast(rowid))');
25+ tx.executeSql('CREATE TABLE IF NOT EXISTS Queue(ind INTEGER NOT NULL, guid TEXT, image TEXT, name TEXT, artist TEXT, url TEXT)');
2526 });
2627
2728 /*
@@ -39,6 +40,104 @@ function init() {
3940 return db;
4041}
4142
43+// Function to add item to queue
44+function addItemToQueue(guid, image, name, artist, url) {
45+ var db = init()
46+
47+ db.transaction(function(tx) {
48+ var ind = getNextIndex(tx);
49+ var rs = tx.executeSql("INSERT OR REPLACE INTO Queue (ind, guid, image, name, artist, url) VALUES (?, ?, ?, ?, ?, ?)", [ind, guid, image, name, artist, url]);
50+ if (rs.rowsAffected > 0) {
51+ console.log("[LOG]: QUEUE add OK")
52+ } else {
53+ console.log("[LOG]: QUEUE add FAIL")
54+ }
55+ });
56+}
57+
58+function removeItemFromQueue(source) {
59+ var db = init()
60+
61+ db.transaction(function(tx) {
62+ // Remove selected source from the queue
63+ tx.executeSql("DELETE FROM Queue WHERE url = ?", source)
64+
65+ // Rebuild queue in order
66+ var rs = tx.executeSql("SELECT ind FROM Queue ORDER BY ind ASC")
67+
68+ for (var i=0; i<rs.rows.length; i++) {
69+ tx.executeSql("UPDATE Queue SET ind = ? WHERE ind = ?", [i, rs.rows.item(i).ind])
70+ }
71+ })
72+}
73+
74+// Function to clear the queue
75+function clearQueue() {
76+ var db = init();
77+ db.transaction(function(tx) {
78+ tx.executeSql("DELETE FROM Queue");
79+ });
80+}
81+
82+function lookup(source) {
83+ var db = init();
84+ var meta = {
85+ name: "",
86+ artist: "",
87+ image: "",
88+ guid: "",
89+ }
90+
91+ db.transaction(function(tx) {
92+ var rs = tx.executeSql("SELECT * FROM Queue ORDER BY ind ASC");
93+ for(var i = 0; i < rs.rows.length; i++) {
94+ var episode = rs.rows.item(i);
95+ if (source === episode.url) {
96+ meta.name = episode.name
97+ meta.artist = episode.artist
98+ meta.image = episode.image
99+ meta.guid = episode.guid
100+ break
101+ }
102+ }
103+ });
104+
105+ return meta
106+}
107+
108+// Function to get the next index for the queue
109+function getNextIndex(tx) {
110+ var ind;
111+
112+ if (tx === undefined) {
113+ var db = init();
114+ db.transaction(function(tx) {
115+ ind = getNextIndex(tx);
116+ });
117+ } else {
118+ var rs = tx.executeSql('SELECT MAX(ind) FROM Queue')
119+ ind = isQueueEmpty(tx) ? 0 : rs.rows.item(0)["MAX(ind)"] + 1
120+ }
121+
122+ return ind;
123+}
124+
125+function isQueueEmpty(tx) {
126+ var empty = false;
127+
128+ if (tx === undefined) {
129+ var db = init();
130+ db.transaction( function(tx) {
131+ empty = isQueueEmpty(tx)
132+ });
133+ } else {
134+ var rs = tx.executeSql("SELECT count(*) as value FROM Queue")
135+ empty = rs.rows.item(0).value === 0
136+ }
137+
138+ return empty
139+}
140+
42141function subscribe(artist, name, feed, img) {
43142 var db = init();
44143 db.transaction(function(tx) {
app/ui/EpisodesPage.qml+15−17
@@ -17,7 +17,7 @@
1717 */
1818
1919import QtQuick 2.4
20−import QtMultimedia 5.4
20+import QtMultimedia 5.6
2121import Ubuntu.Components 1.3
2222import QtQuick.LocalStorage 2.0
2323import Ubuntu.DownloadManager 0.1
@@ -442,8 +442,8 @@ Page {
442442 id: listItemLayout
443443
444444 title.text: model.name !== undefined ? model.name.trim() : "Undefined"
445− title.color: currentGuid === model.guid || downloader.downloadingGuid === model.guid ? podbird.appTheme.focusText
446− : podbird.appTheme.baseText
445+ title.color: downloader.downloadingGuid === model.guid ? podbird.appTheme.focusText
446+ : podbird.appTheme.baseText
447447 // #FIXME: Change this 2 to prevent title eliding when UITK is updated to rev > 1800
448448 title.maximumLineCount: 1
449449
@@ -510,6 +510,14 @@ Page {
510510 }
511511 },
512512
513+ Action {
514+ iconName: "add-to-playlist"
515+ onTriggered: {
516+ var url = model.downloadedfile ? model.downloadedfile : model.audiourl
517+ player.addEpisodeToQueue(model.guid, model.image, model.name, model.artist, url)
518+ }
519+ },
520+
513521 Action {
514522 iconName: model.favourited ? "unlike" : "like"
515523 onTriggered: {
@@ -536,20 +544,10 @@ Page {
536544
537545 onClicked: {
538546 Haptics.play()
539− var db = Podcasts.init();
540− db.transaction(function (tx) {
541− if (currentGuid !== model.guid) {
542− currentGuid = "";
543− currentUrl = model.downloadedfile ? model.downloadedfile : model.audiourl;
544− var rs = tx.executeSql("SELECT position FROM Episode WHERE guid=?", [model.guid]);
545− playerLoader.item.play();
546− playerLoader.item.seek(rs.rows.item(0).position);
547− currentName = model.name;
548− currentArtist = model.artist;
549− currentImage = model.image;
550− currentGuid = model.guid;
551− }
552− });
547+ if (currentGuid !== model.guid) {
548+ currentUrl = model.downloadedfile ? model.downloadedfile : model.audiourl;
549+ player.playEpisode(model.guid, model.image, model.name, model.artist, currentUrl)
550+ }
553551 }
554552 }
555553
app/ui/EpisodesTab.qml+15−17
@@ -17,7 +17,7 @@
1717 */
1818
1919import QtQuick 2.4
20−import QtMultimedia 5.4
20+import QtMultimedia 5.6
2121import Ubuntu.Components 1.3
2222import QtQuick.LocalStorage 2.0
2323import Ubuntu.DownloadManager 0.1
@@ -380,8 +380,8 @@ Tab {
380380 id: listItemLayout
381381
382382 title.text: model.name !== undefined ? model.name.trim() : "Undefined"
383− title.color: currentGuid === model.guid || downloader.downloadingGuid === model.guid ? podbird.appTheme.focusText
384− : podbird.appTheme.baseText
383+ title.color: downloader.downloadingGuid === model.guid ? podbird.appTheme.focusText
384+ : podbird.appTheme.baseText
385385 // #FIXME: Change this 2 to prevent title eliding when UITK is updated to rev > 1800
386386 title.maximumLineCount: 1
387387
@@ -464,6 +464,14 @@ Tab {
464464 }
465465 },
466466
467+ Action {
468+ iconName: "add-to-playlist"
469+ onTriggered: {
470+ var url = model.downloadedfile ? model.downloadedfile : model.audiourl
471+ player.addEpisodeToQueue(model.guid, model.image, model.name, model.artist, url)
472+ }
473+ },
474+
467475 Action {
468476 iconName: model.favourited ? "unlike" : "like"
469477 onTriggered: {
@@ -496,20 +504,10 @@ Tab {
496504
497505 onClicked: {
498506 Haptics.play()
499− var db = Podcasts.init();
500− db.transaction(function (tx) {
501− if (currentGuid !== model.guid) {
502− currentGuid = "";
503− currentUrl = model.downloadedfile ? model.downloadedfile : model.audiourl;
504− var rs = tx.executeSql("SELECT position FROM Episode WHERE guid=?", [model.guid]);
505− playerLoader.item.play();
506− playerLoader.item.seek(rs.rows.item(0).position);
507− currentName = model.name;
508− currentArtist = model.artist;
509− currentImage = model.image;
510− currentGuid = model.guid;
511− }
512− });
507+ if (currentGuid !== model.guid) {
508+ currentUrl = model.downloadedfile ? model.downloadedfile : model.audiourl;
509+ player.playEpisode(model.guid, model.image, model.name, model.artist, currentUrl)
510+ }
513511 }
514512 }
515513
app/ui/NowPlayingPage.qml+301−195
@@ -17,8 +17,9 @@
1717 */
1818
1919import QtQuick 2.4
20−import QtMultimedia 5.4
20+import QtMultimedia 5.6
2121import Ubuntu.Components 1.3
22+import QtQuick.LocalStorage 2.0
2223import "../podcasts.js" as Podcasts
2324import "../components"
2425
@@ -26,245 +27,350 @@ Page {
2627 id: nowPlayingPage
2728
2829 visible: false
29− title: i18n.tr("Now Playing")
3030
3131 property bool isNowPlayingPage: true
32− property bool isLandscapeMode: width > height
33−
34− // Landscape rule
35− states: [
36− State {
37− name: "landscape"
38− when: isLandscapeMode
39−
40− PropertyChanges {
41− target: blurredBackground
42− width: parent.width/2.2
43− height: parent.height
32+
33+ header: PageHeader {
34+ title: i18n.tr("Now Playing")
35+
36+ StyleHints {
37+ backgroundColor: podbird.appTheme.background
38+ }
39+
40+ trailingActionBar.actions: Action {
41+ iconName: "delete"
42+ visible: nowPlayingPageSections.selectedIndex === 1
43+ onTriggered: {
44+ Podcasts.clearQueue()
45+ player.playlist.clear()
46+ mainStack.pop()
4447 }
48+ }
4549
46− AnchorChanges {
47− target: blurredBackground
48− anchors {
49− top: parent.top
50− left: parent.left
51− right: undefined
52− }
50+ extension: Sections {
51+ id: nowPlayingPageSections
52+
53+ anchors {
54+ left: parent.left
55+ leftMargin: units.gu(2)
56+ bottom: parent.bottom
5357 }
5458
55− AnchorChanges {
56− target: dataContainer
57− anchors {
58− top: parent.top
59− left: blurredBackground.right
60− right: parent.right
61− bottom: parent.bottom
62− }
59+ StyleHints {
60+ selectedSectionColor: podbird.appTheme.focusText
6361 }
64− }
65− ]
66−
67− BlurredBackground {
68− id: blurredBackground
69−
70− anchors.left: parent.left
71− anchors.top: parent.top
72− anchors.right: parent.right
73− height: title.lineCount === 1 ? parent.height/2 + units.gu(3)
74− : parent.height/2
75− art: currentImage
76−
77− Image {
78− width: Math.min(parent.width/2, parent.height)
79− height: width
80− sourceSize.height: width
81− sourceSize.width: width
82− source: currentImage
83− asynchronous: true
84− anchors.centerIn: parent
62+ model: [i18n.tr("Full view"), i18n.tr("Queue")]
8563 }
8664 }
8765
88− Item {
89− id: dataContainer
66+ VisualItemModel {
67+ id: tabs
9068
91− anchors {
92− top: blurredBackground.bottom
93− left: parent.left
94− right: parent.right
95− bottom: parent.bottom
96− margins: units.gu(2)
97− bottomMargin: isLandscapeMode ? units.gu(4) : units.gu(2)
98− }
69+ Item {
70+ id: nowPlayingItem
9971
100− Label {
101− id: title
102− anchors.left: parent.left
103− anchors.right: parent.right
104− anchors.top: parent.top
105− text: currentName
106− elide: Text.ElideRight
107− textSize: Label.Large
108− maximumLineCount: 2
109− wrapMode: Text.WordWrap
110− color: podbird.appTheme.baseText
111− }
72+ width: tabView.width
73+ height: tabView.height
11274
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− }
75+ property bool isLandscapeMode: nowPlayingPage.width > nowPlayingPage.height
12476
125− Slider {
126− id: scrubber
77+ // Landscape rule
78+ states: [
79+ State {
80+ name: "landscape"
81+ when: nowPlayingItem.isLandscapeMode
12782
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− }
83+ PropertyChanges {
84+ target: blurredBackground
85+ width: nowPlayingPage.width/2.2
86+ height: nowPlayingPage.height
87+ }
13488
135− live: true
136− minimumValue: 0
137− maximumValue: playerLoader.item.duration
138− value: playerLoader.item.position
139− height: units.gu(2)
89+ AnchorChanges {
90+ target: blurredBackground
91+ anchors {
92+ top: nowPlayingItem.top
93+ left: parent.left
94+ right: undefined
95+ }
96+ }
14097
141− onValueChanged: {
142− if (pressed) {
143− playerLoader.item.seek(value);
98+ AnchorChanges {
99+ target: dataContainer
100+ anchors {
101+ top: nowPlayingItem.top
102+ left: blurredBackground.right
103+ right: parent.right
104+ bottom: parent.bottom
105+ }
106+ }
144107 }
145− }
108+ ]
146109
147− function formatValue(v) { return Podcasts.formatTime(v/1000); }
148− StyleHints { foregroundColor: podbird.appTheme.focusText }
149− }
110+ BlurredBackground {
111+ id: blurredBackground
150112
151− Connections {
152− target: playerLoader.item
153− onPositionChanged: scrubber.value = playerLoader.item.position
154− }
113+ anchors.left: parent.left
114+ anchors.top: nowPlayingItem.top
115+ anchors.right: parent.right
116+ height: title.lineCount === 1 ? nowPlayingPage.height/2.3 + units.gu(3)
117+ : nowPlayingPage.height/2.3
118+ art: currentImage
155119
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− }
120+ Image {
121+ width: Math.min(nowPlayingPage.width/2, nowPlayingPage.height/2)
122+ height: width
123+ sourceSize.height: width
124+ sourceSize.width: width
125+ source: currentImage
126+ asynchronous: true
127+ anchors.centerIn: parent
128+ }
129+ }
164130
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− }
131+ Item {
132+ id: dataContainer
173133
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− }
134+ anchors {
135+ top: blurredBackground.bottom
136+ left: parent.left
137+ right: parent.right
138+ bottom: parent.bottom
139+ margins: units.gu(2)
140+ bottomMargin: nowPlayingItem.isLandscapeMode ? units.gu(4) : units.gu(2)
191141 }
192142
193− Row {
194− spacing: units.gu(1)
195− anchors.centerIn: parent
143+ Label {
144+ id: title
145+ anchors.left: parent.left
146+ anchors.right: parent.right
147+ anchors.top: parent.top
148+ text: currentName
149+ elide: Text.ElideRight
150+ textSize: Label.Large
151+ maximumLineCount: 2
152+ wrapMode: Text.WordWrap
153+ color: podbird.appTheme.baseText
154+ }
196155
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
156+ Label {
157+ id: artist
158+ anchors.left: title.left
159+ anchors.right: title.right
160+ anchors.top: title.bottom
161+ anchors.topMargin: units.gu(1)
162+ text: currentArtist
163+ elide: Text.ElideRight
164+ textSize: Label.Small
165+ color: podbird.appTheme.baseSubText
166+ }
167+
168+ Slider {
169+ id: scrubber
170+
171+ anchors {
172+ left: parent.left
173+ right: parent.right
174+ bottom: controls.top
175+ bottomMargin: nowPlayingItem.isLandscapeMode && title.lineCount < 2 ? units.gu(4) : units.gu(2)
204176 }
205177
206− Icon {
207− id: skipBackwardIcon
208− width: units.gu(3)
209− height: width
210− name: "media-seek-backward"
211− color: podbird.appTheme.baseIcon
178+ live: true
179+ minimumValue: 0
180+ maximumValue: player.duration
181+ value: player.position
182+ height: units.gu(2)
183+
184+ onValueChanged: {
185+ if (pressed) {
186+ player.seek(value);
187+ }
212188 }
189+
190+ function formatValue(v) { return Podcasts.formatTime(v/1000); }
191+ StyleHints { foregroundColor: podbird.appTheme.focusText }
213192 }
214− }
215193
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()
194+ Connections {
195+ target: player
196+ onPositionChanged: scrubber.value = player.position
197+ }
222198
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"
199+ Label {
200+ id: startTime
201+ textSize: Label.Small
202+ anchors.left: scrubber.left
203+ anchors.top: scrubber.bottom
204+ color: podbird.appTheme.baseText
205+ text: Podcasts.formatTime(player.position / 1000)
231206 }
232− }
233207
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− }
208+ Label {
209+ id: endTime
210+ textSize: Label.Small
211+ anchors.right: scrubber.right
212+ anchors.top: scrubber.bottom
213+ color: podbird.appTheme.baseText
214+ text: Podcasts.formatTime(player.duration / 1000)
244215 }
245216
246217 Row {
218+ id: controls
219+
220+ anchors.bottom: parent.bottom
221+ anchors.horizontalCenter: parent.horizontalCenter
247222 spacing: units.gu(1)
248− anchors.centerIn: parent
249223
250− Icon {
251− id: skipForwardIcon
252− width: units.gu(3)
224+ AbstractButton {
225+ id: mediaBackwardButton
226+ width: units.gu(6)
253227 height: width
254− name: "media-seek-forward"
255− color: podbird.appTheme.baseIcon
228+ anchors.verticalCenter: parent.verticalCenter
229+ enabled: player.playlist.canGoPrevious
230+ opacity: enabled ? 1.0 : 0.4
231+ onClicked: player.playlist.previous()
232+
233+ Icon {
234+ id: mediaBackwardIcon
235+ width: units.gu(3)
236+ height: width
237+ anchors.centerIn: parent
238+ color: podbird.appTheme.baseIcon
239+ name: "media-skip-backward"
240+ }
256241 }
257242
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
243+ AbstractButton {
244+ id: skipBackwardButton
245+ width: units.gu(6)
246+ height: width
247+ anchors.verticalCenter: parent.verticalCenter
248+ opacity: player.position === 0 ? 0.4 : 1.0
249+ onClicked: {
250+ if (player.position > 0) {
251+ player.seek(player.position - podbird.settings.skipBack * 1000);
252+ }
253+ }
254+
255+ Row {
256+ spacing: units.gu(1)
257+ anchors.centerIn: parent
258+
259+ Label {
260+ // TRANSLATORS: The string shown in the UI is -15s to denote the number of seconds that the podcast playback will skip backward.
261+ // xgettext: no-c-format
262+ text: i18n.tr("-%1s").arg(podbird.settings.skipBack)
263+ textSize: Label.XxSmall
264+ color: podbird.appTheme.baseText
265+ anchors.verticalCenter: skipBackwardIcon.verticalCenter
266+ }
267+
268+ Icon {
269+ id: skipBackwardIcon
270+ width: units.gu(3)
271+ height: width
272+ name: "media-seek-backward"
273+ color: podbird.appTheme.baseIcon
274+ }
275+ }
276+ }
277+
278+ AbstractButton {
279+ id: playButton
280+ width: units.gu(10)
281+ height: width
282+ opacity: playButton.pressed ? 0.4 : 1.0
283+ onClicked: player.playbackState === MediaPlayer.PlayingState ? player.pause() : player.play()
284+
285+ Icon {
286+ id: playIcon
287+ width: units.gu(6)
288+ height: width
289+ anchors.centerIn: parent
290+ color: podbird.appTheme.baseIcon
291+ name: player.playbackState === MediaPlayer.PlayingState ? "media-playback-pause"
292+ : "media-playback-start"
293+ }
294+ }
295+
296+ AbstractButton {
297+ id: skipForwardButton
298+ width: units.gu(6)
299+ height: width
300+ anchors.verticalCenter: parent.verticalCenter
301+ opacity: player.position === 0 ? 0.4 : 1.0
302+ onClicked: {
303+ if (player.position > 0) {
304+ player.seek(player.position + podbird.settings.skipForward * 1000);
305+ }
306+ }
307+
308+ Row {
309+ spacing: units.gu(1)
310+ anchors.centerIn: parent
311+
312+ Icon {
313+ id: skipForwardIcon
314+ width: units.gu(3)
315+ height: width
316+ name: "media-seek-forward"
317+ color: podbird.appTheme.baseIcon
318+ }
319+
320+ Label {
321+ // TRANSLATORS: The string shown in the UI is +15s to denote the number of seconds that the podcast playback will skip forward.
322+ // xgettext: no-c-format
323+ text: i18n.tr("+%1s").arg(podbird.settings.skipForward)
324+ textSize: Label.XxSmall
325+ color: podbird.appTheme.baseText
326+ anchors.verticalCenter: skipForwardIcon.verticalCenter
327+ }
328+ }
329+ }
330+
331+ AbstractButton {
332+ id: mediaForwardButton
333+ width: units.gu(6)
334+ height: width
335+ anchors.verticalCenter: parent.verticalCenter
336+ enabled: player.playlist.canGoNext
337+ opacity: enabled ? 1.0 : 0.4
338+ onClicked: player.playlist.next()
339+
340+ Icon {
341+ id: mediaForwardIcon
342+ width: units.gu(3)
343+ height: width
344+ anchors.centerIn: parent
345+ color: podbird.appTheme.baseIcon
346+ name: "media-skip-forward"
347+ }
265348 }
266349 }
267350 }
268351 }
352+
353+ Queue {
354+ width: tabView.width
355+ height: tabView.height
356+ }
357+ }
358+
359+ ListView {
360+ id: tabView
361+ model: tabs
362+ interactive: false
363+
364+ anchors {
365+ top: nowPlayingPage.header.bottom
366+ left: parent.left
367+ right: parent.right
368+ bottom: parent.bottom
369+ }
370+
371+ orientation: Qt.Horizontal
372+ snapMode: ListView.SnapOneItem
373+ currentIndex: nowPlayingPageSections.selectedIndex
374+ highlightMoveDuration: UbuntuAnimation.SlowDuration
269375 }
270376}
app/ui/PlayerControls.qml+6−6
@@ -17,7 +17,7 @@
1717 */
1818
1919import QtQuick 2.4
20−import QtMultimedia 5.4
20+import QtMultimedia 5.6
2121import Ubuntu.Components 1.3
2222
2323Rectangle {
@@ -49,7 +49,7 @@ Rectangle {
4949 anchors.top: cover.bottom
5050 color: podbird.appTheme.focusText
5151 height: units.gu(0.25)
52− width: playerLoader.item.duration > 0 ? (playerLoader.item.position / playerLoader.item.duration) * parent.width : 0
52+ width: player.duration > 0 ? (player.position / player.duration) * parent.width : 0
5353 }
5454
5555 Column {
@@ -97,16 +97,16 @@ Rectangle {
9797 visible: playButton.pressed
9898 }
9999
100− onClicked: playerLoader.item.playbackState === MediaPlayer.PlayingState ? playerLoader.item.pause()
101− : playerLoader.item.play()
100+ onClicked: player.playbackState === MediaPlayer.PlayingState ? player.pause()
101+ : player.play()
102102
103103 Icon {
104104 color: "white"
105105 width: units.gu(3)
106106 height: width
107107 anchors.centerIn: playButtonBackground
108− name: playerLoader.item.playbackState === MediaPlayer.PlayingState ? "media-playback-pause"
109− : "media-playback-start"
108+ name: player.playbackState === MediaPlayer.PlayingState ? "media-playback-pause"
109+ : "media-playback-start"
110110 opacity: playButton.pressed ? 0.4 : 1.0
111111 }
112112 }
app/ui/PodcastsTab.qml+1−1
@@ -17,7 +17,7 @@
1717 */
1818
1919import QtQuick 2.4
20−import QtMultimedia 5.4
20+import QtMultimedia 5.6
2121import QtQuick.LocalStorage 2.0
2222import Ubuntu.Components 1.3
2323import Ubuntu.DownloadManager 0.1
app/ui/Queue.qmlnew file+94
@@ -0,0 +1,94 @@
1+/*
2+ * Copyright 2016 Podbird Team
3+ *
4+ * This file is part of Podbird.
5+ *
6+ * Podbird is free software; you can redistribute it and/or modify
7+ * it under the terms of the GNU General Public License as published by
8+ * the Free Software Foundation; version 3.
9+ *
10+ * Podbird is distributed in the hope that it will be useful,
11+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
12+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+ * GNU General Public License for more details.
14+ *
15+ * You should have received a copy of the GNU General Public License
16+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
17+ */
18+
19+import QtQuick 2.4
20+import Ubuntu.Components 1.3
21+import "../podcasts.js" as Podcasts
22+import "../components"
23+
24+Item {
25+ id: queuePage
26+
27+ ListView {
28+ id: queueList
29+
30+ anchors.fill: parent
31+ model: player.playlist
32+
33+ delegate: ListItem {
34+ id: listItem
35+
36+ height: layout.height
37+ divider.visible: false
38+
39+ ListItemLayout {
40+ id: layout
41+
42+ // Grab the metaData for the current index using its unique source url
43+ property var metaModel: player.metaForSource(model.source)
44+
45+ Image {
46+ id: imgFrame
47+ width: units.gu(6)
48+ height: width
49+ source: Qt.resolvedUrl(layout.metaModel.image)
50+ sourceSize.height: width
51+ sourceSize.width: width
52+ SlotsLayout.position: SlotsLayout.First
53+ }
54+
55+ title.text: layout.metaModel.name
56+ // #FIXME: Change this 2 to prevent title eliding when UITK is updated to rev > 1800
57+ title.maximumLineCount: 1
58+ title.color: player.playlist.currentIndex === index ? podbird.appTheme.focusText
59+ : podbird.appTheme.baseText
60+
61+ subtitle.text: layout.metaModel.artist
62+ subtitle.color: podbird.appTheme.baseSubText
63+ }
64+
65+ leadingActions: ListItemActions {
66+ actions: [
67+ Action {
68+ iconName: "delete"
69+ onTriggered: {
70+ player.playlist.removeItem(index)
71+ var source = model.source
72+
73+ source = source.toString()
74+
75+ if (source.indexOf("file://") === 0) {
76+ source = source.substring(7);
77+ }
78+
79+ Podcasts.removeItemFromQueue(source)
80+ }
81+ }
82+ ]
83+ }
84+
85+ onClicked: {
86+ if (player.playlist.currentIndex === index) {
87+ player.toggle()
88+ } else {
89+ player.playlist.currentIndex = index
90+ }
91+ }
92+ }
93+ }
94+}