Various bugfixes, support for filtering episode lists

This commit is contained in:
Thomas Perl 2014-03-15 15:44:00 +01:00
parent 431ca519d3
commit 0da88e3577
17 changed files with 421 additions and 45 deletions

View file

@ -26,24 +26,66 @@ import 'constants.js' as Constants
ListModel {
id: episodeListModel
property var podcast_id
property var podcast_id: -1
function loadEpisodes(podcast_id) {
episodeListModel.podcast_id = podcast_id;
reload();
property var queries: ({
All: '',
Fresh: 'new or downloading',
Downloaded: 'downloaded or downloading',
HideDeleted: 'not deleted',
Deleted: 'deleted',
})
property var filters: ([
{ label: 'All', query: episodeListModel.queries.All },
{ label: 'Fresh', query: episodeListModel.queries.Fresh },
{ label: 'Downloaded', query: episodeListModel.queries.Downloaded },
{ label: 'Hide deleted', query: episodeListModel.queries.HideDeleted },
{ label: 'Deleted', query: episodeListModel.queries.Deleted },
])
property bool ready: false
property int currentFilterIndex: -1
property string currentCustomQuery: queries.All
function setQuery(query) {
for (var i=0; i<filters.length; i++) {
if (filters[i].query === query) {
currentFilterIndex = i;
return;
}
}
currentFilterIndex = -1;
currentCustomQuery = query;
}
function loadFreshEpisodes(callback) {
function loadAllEpisodes(callback) {
episodeListModel.podcast_id = -1;
py.call('main.get_fresh_episodes', [], function (episodes) {
Util.updateModelFrom(episodeListModel, episodes);
callback();
});
reload(callback);
}
function reload() {
py.call('main.load_episodes', [podcast_id], function (episodes) {
function loadEpisodes(podcast_id, callback) {
episodeListModel.podcast_id = podcast_id;
reload(callback);
}
function reload(callback) {
var query;
if (currentFilterIndex !== -1) {
query = filters[currentFilterIndex].query;
} else {
query = currentCustomQuery;
}
ready = false;
py.call('main.load_episodes', [podcast_id, query], function (episodes) {
Util.updateModelFrom(episodeListModel, episodes);
episodeListModel.ready = true;
if (callback !== undefined) {
callback();
}
});
}

View file

@ -32,6 +32,7 @@ var colors = {
destructive: '#cf424f', /* destructive actions */
page: '#dddddd',
dialogBackground: '#aa000000',
text: '#333333', /* text color */
highlight: '#433b67',
secondaryHighlight: '#605885',

39
main.py
View file

@ -31,6 +31,7 @@ import gpodder
from gpodder.api import core
from gpodder.api import util
from gpodder.api import query
import logging
import functools
@ -55,6 +56,8 @@ def run_in_background_thread(f):
class gPotherSide:
ALL_PODCASTS = -1
def __init__(self):
self.core = None
self._checking_for_new_episodes = False
@ -149,9 +152,26 @@ class gPotherSide:
'hasShownotes': episode.description != '',
}
def load_episodes(self, id):
podcast = self._get_podcast_by_id(id)
return [self.convert_episode(episode) for episode in podcast.episodes]
def load_episodes(self, id=ALL_PODCASTS, eql=None):
if id is not None and id != self.ALL_PODCASTS:
podcasts = [self._get_podcast_by_id(id)]
else:
podcasts = self.core.model.get_podcasts()
if eql:
filter_func = query.EQL(eql).filter
else:
filter_func = lambda episodes: episodes
result = []
for podcast in podcasts:
result.extend(filter_func(podcast.episodes))
if id == self.ALL_PODCASTS:
result.sort(key=lambda e: e.published, reverse=True)
return [self.convert_episode(episode) for episode in result]
def get_fresh_episodes_summary(self, count):
summary = []
@ -167,19 +187,9 @@ class gPotherSide:
summary.sort(key=lambda e: e['newEpisodes'], reverse=True)
return summary[:int(count)]
def get_fresh_episodes(self):
fresh_episodes = []
for podcast in self.core.model.get_podcasts():
for episode in podcast.episodes:
if episode.is_fresh():
fresh_episodes.append(episode)
fresh_episodes.sort(key=lambda e: e.published, reverse=True)
return [self.convert_episode(e) for e in fresh_episodes]
@run_in_background_thread
def subscribe(self, url):
url = util.normalize_feed_url(url)
url = self.core.model.normalize_feed_url(url)
# TODO: Check if subscription already exists
self.core.model.load_podcast(url, create=True)
self.core.save()
@ -324,7 +334,6 @@ subscribe = gpotherside.subscribe
unsubscribe = gpotherside.unsubscribe
check_for_episodes = gpotherside.check_for_episodes
get_stats = gpotherside.get_stats
get_fresh_episodes = gpotherside.get_fresh_episodes
get_fresh_episodes_summary = gpotherside.get_fresh_episodes_summary
download_episode = gpotherside.download_episode
delete_episode = gpotherside.delete_episode

View file

@ -13,7 +13,7 @@ dist/$(PROJECT)-$(VERSION).tar.gz:
git archive --format=tar --prefix=$(PROJECT)-$(VERSION)/ $(VERSION) | gzip >$@
clean:
find . -name '__pycache__' -exec rm {} +
find . -name '__pycache__' -exec rm -rf {} +
distclean: clean
rm -rf dist

59
touch/Dialog.qml Normal file
View file

@ -0,0 +1,59 @@
/**
*
* gPodder QML UI Reference Implementation
* Copyright (c) 2014, Thomas Perl <m@thp.io>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*
*/
import QtQuick 2.0
import 'common/constants.js' as Constants
Rectangle {
id: page
color: Constants.colors.dialogBackground
default property alias children: contents.children
property bool isDialog: true
property int contentHeight: -1
function closePage() {
stacking.startFadeOut();
}
onXChanged: pgst.update(page, x)
width: parent.width
height: parent.height
DialogStacking { id: stacking }
MouseArea {
anchors.fill: parent
onClicked: page.closePage();
}
Rectangle {
id: contents
property int border: parent.width * 0.1
width: parent.width - 2 * border
property int maxHeight: parent.height - 4 * border
height: ((page.contentHeight > 0 && page.contentHeight < maxHeight) ? page.contentHeight : maxHeight) * parent.opacity
anchors.centerIn: parent
color: Constants.colors.page
clip: true
}
}

70
touch/DialogStacking.qml Normal file
View file

@ -0,0 +1,70 @@
/**
*
* gPodder QML UI Reference Implementation
* Copyright (c) 2013, Thomas Perl <m@thp.io>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*
*/
import QtQuick 2.0
Item {
id: stacking
property variant page: parent
PropertyAnimation {
id: fadeIn
target: stacking.page
property: 'opacity'
to: 1
duration: 500
easing.type: Easing.OutCubic
onStopped: {
pgst.loadPageInProgress = false;
}
}
PropertyAnimation {
id: fadeOut
target: stacking.page
property: 'opacity'
to: 0
duration: 500
easing.type: Easing.OutCubic
}
function startFadeOut() {
fadeOut.start();
page.destroy(500);
}
function fadeInAgain() {
fadeIn.start();
}
function stopAllAnimations() {
fadeIn.stop();
}
Component.onCompleted: {
if (pgst.loadPageInProgress) {
page.x = 0;
page.opacity = 0;
fadeIn.start();
}
}
}

View file

@ -0,0 +1,46 @@
/**
*
* gPodder QML UI Reference Implementation
* Copyright (c) 2014, Thomas Perl <m@thp.io>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*
*/
import QtQuick 2.0
Item {
id: episodeQueryControl
property var model
property string title
function showSelectionDialog() {
pgst.loadPage('SelectionDialog.qml', {
title: episodeQueryControl.title,
callback: function (index, result) {
episodeQueryControl.model.currentFilterIndex = index;
episodeQueryControl.model.reload();
},
items: function () {
var labels = [];
for (var i in episodeQueryControl.model.filters) {
labels.push(episodeQueryControl.model.filters[i].label);
}
return labels;
}(),
selectedIndex: episodeQueryControl.model.currentFilterIndex,
});
}
}

View file

@ -22,28 +22,42 @@ import QtQuick 2.0
import 'common'
import 'common/util.js' as Util
import 'common/constants.js' as Constants
import 'icons/icons.js' as Icons
SlidePage {
id: freshEpisodes
property bool ready: false
id: allEpisodesPage
EpisodeQueryControl {
id: queryControl
model: episodesListModel
title: 'Select filter'
}
Component.onCompleted: {
freshEpisodesListModel.loadFreshEpisodes(function () {
freshEpisodes.ready = true;
});
episodesListModel.setQuery(episodesListModel.queries.Fresh);
episodesListModel.reload();
}
PBusyIndicator {
visible: !freshEpisodes.ready
visible: !episodesListModel.ready
anchors.centerIn: parent
}
PListView {
id: episodeList
property int selectedIndex: -1
title: 'Fresh episodes'
title: 'Episodes'
headerHasIcon: true
headerIconText: 'Filter'
onHeaderIconClicked: queryControl.showSelectionDialog();
model: GPodderEpisodeListModel { id: freshEpisodesListModel }
PPlaceholder {
text: 'No episodes found'
visible: episodeList.count === 0 && episodesListModel.ready
}
model: GPodderEpisodeListModel { id: episodesListModel }
section.property: 'published'
section.delegate: SectionHeader { text: section }
@ -51,4 +65,3 @@ SlidePage {
delegate: EpisodeItem { }
}
}

View file

@ -36,7 +36,17 @@ SlidePage {
width: parent.width
height: parent.height
Component.onCompleted: episodeListModel.loadEpisodes(podcast_id);
Component.onCompleted: {
episodeListModel.podcast_id = podcast_id;
episodeListModel.setQuery(episodeListModel.queries.All);
episodeListModel.reload();
}
EpisodeQueryControl {
id: queryControl
model: episodeListModel
title: 'Select filter'
}
PullMenu {
PullMenuItem {
@ -81,6 +91,10 @@ SlidePage {
title: episodesPage.title
model: GPodderEpisodeListModel { id: episodeListModel }
headerHasIcon: true
headerIconText: 'Filter'
onHeaderIconClicked: queryControl.showSelectionDialog();
PPlaceholder {
text: 'No episodes'
visible: episodeList.count === 0

View file

@ -42,7 +42,11 @@ Item {
}
}
children[index-1].opacity = x / width;
if (page.isDialog) {
children[index-1].opacity = 1;
} else {
children[index-1].opacity = x / width;
}
//children[index-1].pushPhase = x / width;
}

View file

@ -27,9 +27,19 @@ ListView {
property string title
property real pushPhase: 0
property bool headerHasIcon: false
property string headerIconText
signal headerIconClicked()
boundsBehavior: Flickable.StopAtBounds
header: SlidePageHeader { title: pListView.title }
header: SlidePageHeader {
title: pListView.title
hasIcon: pListView.headerHasIcon
iconText: pListView.headerIconText
onIconClicked: pListView.headerIconClicked()
}
PScrollDecorator { flickable: pListView }
}

View file

@ -26,5 +26,5 @@ import 'common/constants.js' as Constants
PLabel {
anchors.centerIn: parent
font.pixelSize: 40 * pgst.scalef
color: Constants.colors.text
color: Constants.colors.placeholder
}

84
touch/SelectionDialog.qml Normal file
View file

@ -0,0 +1,84 @@
/**
*
* gPodder QML UI Reference Implementation
* Copyright (c) 2014, Thomas Perl <m@thp.io>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*
*/
import QtQuick 2.0
import 'common/constants.js' as Constants
import 'icons/icons.js' as Icons
Dialog {
id: selectionDialog
property string title: 'Dialog'
property var callback: undefined
property var items: ([])
property var selectedIndex: -1
contentHeight: selectionDialogFlickable.contentHeight
Flickable {
id: selectionDialogFlickable
anchors.fill: parent
contentHeight: contentColumn.height
Column {
id: contentColumn
width: parent.width
SlidePageHeader {
id: header
color: Constants.colors.highlight
title: selectionDialog.title
}
Repeater {
model: selectionDialog.items
delegate: ButtonArea {
width: parent.width
height: 60 * pgst.scalef
transparent: (index != selectionDialog.selectedIndex)
PLabel {
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
margins: 20 * pgst.scalef
}
text: modelData
color: (index == selectionDialog.selectedIndex) ? Constants.colors.highlight : Constants.colors.text
}
onClicked: {
if (selectionDialog.callback !== undefined) {
selectionDialog.callback(index, modelData);
}
selectionDialog.closePage();
}
}
}
}
}
PScrollDecorator { flickable: selectionDialogFlickable }
}

View file

@ -30,6 +30,7 @@ Rectangle {
property alias hasPull: dragging.hasPull
property alias canClose: dragging.canClose
property real pullPhase: (x >= 0) ? 0 : (-x / (width / 4))
property bool isDialog: false
function unPull() {
stacking.fadeInAgain();

View file

@ -21,22 +21,45 @@
import QtQuick 2.0
import 'common/constants.js' as Constants
import 'icons/icons.js' as Icons
Item {
id: slidePageHeader
property alias title: label.text
property alias color: label.color
property alias hasIcon: icon.visible
property alias iconText: icon.text
signal iconClicked()
width: parent.width
height: Constants.layout.header.height * pgst.scalef
IconMenuItem {
id: icon
visible: false
enabled: visible
text: 'Search'
icon: Icons.magnifying_glass
color: label.color
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
}
onClicked: slidePageHeader.iconClicked()
}
PLabel {
id: label
anchors {
left: parent.left
left: icon.right
right: parent.right
rightMargin: 20 * pgst.scalef
leftMargin: 70 * pgst.scalef
leftMargin: 20 * pgst.scalef
verticalCenter: parent.verticalCenter
}

View file

@ -134,10 +134,9 @@ SlidePage {
StartPageButton {
id: freshEpisodes
enabled: freshEpisodesRepeater.count > 0
title: py.refreshing ? 'Refreshing feeds' : 'Fresh episodes'
onClicked: pgst.loadPage('FreshEpisodes.qml');
title: py.refreshing ? 'Refreshing feeds' : 'Episodes'
onClicked: pgst.loadPage('EpisodeQueryPage.qml');
Row {
id: freshEpisodesRow

View file

@ -16,3 +16,4 @@ var aperture = '\ue026';
var eye = '\ue025';
var loop_alt2 = '\ue033';
var folder = '\ue065';
var magnifying_glass = '\ue074';