From 77b24d58cb3352b7285b32c320671e1205e28daf Mon Sep 17 00:00:00 2001 From: Thomas Perl Date: Wed, 17 Dec 2014 22:40:06 +0100 Subject: [PATCH] Very basic playlist support --- common/GPodderEpisodeListModel.qml | 7 ++++ common/GPodderPlayback.qml | 48 +++++++++++++++++++++++ common/util.js | 10 +++++ touch/EpisodeItem.qml | 9 +++++ touch/EpisodesPage.qml | 14 +++++++ touch/PlayerPage.qml | 61 ++++++++++++++++++++++++++++++ 6 files changed, 149 insertions(+) diff --git a/common/GPodderEpisodeListModel.qml b/common/GPodderEpisodeListModel.qml index 6528b66..a51b540 100644 --- a/common/GPodderEpisodeListModel.qml +++ b/common/GPodderEpisodeListModel.qml @@ -62,6 +62,13 @@ ListModel { }); } + function forEachEpisode(callback) { + // Go from bottom up (= chronological order) + for (var i=count-1; i>=0; i--) { + callback(get(i)); + } + } + function setQueryIndex(index) { currentFilterIndex = index; py.call('main.set_config_value', ['ui.qml.episode_list.filter_eql', filters[currentFilterIndex].query]); diff --git a/common/GPodderPlayback.qml b/common/GPodderPlayback.qml index 9629da0..32f81c1 100644 --- a/common/GPodderPlayback.qml +++ b/common/GPodderPlayback.qml @@ -31,6 +31,7 @@ MediaPlayer { signal playerCreated() property var queue: ([]) + signal queueUpdated() property bool isPlaying: playbackState == MediaPlayer.PlayingState property bool inhibitPositionEvents: false @@ -58,6 +59,24 @@ MediaPlayer { } } + function enqueueEpisode(episode_id, callback) { + py.call('main.show_episode', [episode_id], function (episode) { + if (episode_id != player.episode && !queue.some(function (queued) { + return queued.episode_id === episode_id; + })) { + queue.push({ + episode_id: episode_id, + title: episode.title, + }); + queueUpdated(); + } + + if (callback !== undefined) { + callback(); + } + }); + } + function playbackEpisode(episode_id) { if (episode == episode_id) { // If the episode is already loaded, just start playing @@ -157,6 +176,35 @@ MediaPlayer { } } + property var nextInQueueTimer: Timer { + interval: 500 + + repeat: false + + onTriggered: { + if (queue.length > 0) { + playbackEpisode(queue.shift().episode_id); + player.queueUpdated(); + } + } + } + + function jumpToQueueIndex(index) { + playbackEpisode(removeQueueIndex(index).episode_id); + } + + function removeQueueIndex(index) { + var result = queue.splice(index, 1)[0]; + player.queueUpdated(); + return result; + } + + onStatusChanged: { + if (status === MediaPlayer.EndOfMedia) { + nextInQueueTimer.start(); + } + } + property var savePlaybackPositionTimer: Timer { // Save position every minute during playback interval: 60 * 1000 diff --git a/common/util.js b/common/util.js index a70208a..3cb2d20 100644 --- a/common/util.js +++ b/common/util.js @@ -75,3 +75,13 @@ function format(s, d) { return (k in d) ? d[k] : m; }); } + +function atMostOnce(callback) { + var called = false; + return function () { + if (!called) { + called = true; + callback(); + } + }; +} diff --git a/touch/EpisodeItem.qml b/touch/EpisodeItem.qml index dfde864..60671ab 100644 --- a/touch/EpisodeItem.qml +++ b/touch/EpisodeItem.qml @@ -57,6 +57,15 @@ Item { player.playbackEpisode(id); } } + + onPressAndHold: { + player.enqueueEpisode(id, function () { + if (!player.isPlaying) { + player.jumpToQueueIndex(0); + } + pgst.loadPage('PlayerPage.qml'); + }); + } } IconMenuItem { diff --git a/touch/EpisodesPage.qml b/touch/EpisodesPage.qml index 8215431..dc046c2 100644 --- a/touch/EpisodesPage.qml +++ b/touch/EpisodesPage.qml @@ -47,6 +47,20 @@ SlidePage { py.call('main.mark_episodes_as_old', [page.podcast_id]); }, }, + { + label: 'Enqueue episodes in player', + callback: function () { + var startPlayback = Util.atMostOnce(function () { + if (!player.isPlaying) { + player.jumpToQueueIndex(0); + } + }); + + episodeList.model.forEachEpisode(function (episode) { + player.enqueueEpisode(episode.id, startPlayback); + }); + }, + }, { label: 'Podcast details', callback: function () { diff --git a/touch/PlayerPage.qml b/touch/PlayerPage.qml index f076cfb..e19e7cc 100644 --- a/touch/PlayerPage.qml +++ b/touch/PlayerPage.qml @@ -190,6 +190,67 @@ SlidePage { } } } + + SectionHeader { + text: 'Play queue' + visible: playQueueRepeater.count > 0 + width: parent.width + } + + Repeater { + id: playQueueRepeater + model: player.queue + + property var queueConnections: Connections { + target: player + + onQueueUpdated: { + playQueueRepeater.model = player.queue; + } + } + + ButtonArea { + height: Constants.layout.item.height * pgst.scalef + width: parent.width + transparent: true + + PLabel { + anchors { + left: parent.left + right: parent.right + margins: Constants.layout.padding * pgst.scalef + verticalCenter: parent.verticalCenter + } + + text: modelData.title + elide: Text.ElideRight + } + + onClicked: { + player.jumpToQueueIndex(index); + } + + onPressAndHold: { + pgst.showSelection([ + { + label: 'Shownotes', + callback: function () { + pgst.loadPage('EpisodeDetail.qml', { + episode_id: modelData.episode_id, + title: modelData.title + }); + }, + }, + { + label: 'Remove from queue', + callback: function () { + player.removeQueueIndex(index); + }, + }, + ]); + } + } + } } }