From 289849b8a28eda0d331831f8f776eb5cd9777db3 Mon Sep 17 00:00:00 2001 From: Thomas Perl Date: Sun, 8 Mar 2015 00:16:24 +0100 Subject: [PATCH 01/16] Three panes --- common/GPodderEpisodeListModel.qml | 4 +++ touch/Dragging.qml | 6 ++-- touch/EpisodeDetail.qml | 4 ++- touch/EpisodeListView.qml | 5 ++++ touch/EpisodesPage.qml | 9 ++---- touch/Main.qml | 46 ++++++++++++++++++++++++++++-- touch/PodcastDetail.qml | 2 ++ touch/PodcastsPage.qml | 2 ++ touch/SlidePage.qml | 8 ++++++ touch/Stacking.qml | 6 ++-- touch/gpodder.qml | 2 +- 11 files changed, 77 insertions(+), 17 deletions(-) diff --git a/common/GPodderEpisodeListModel.qml b/common/GPodderEpisodeListModel.qml index 33535b6..f8e632f 100644 --- a/common/GPodderEpisodeListModel.qml +++ b/common/GPodderEpisodeListModel.qml @@ -62,6 +62,10 @@ ListModel { }); } + onPodcast_idChanged: { + reload(); + } + property var worker: ModelWorkerScript { id: modelWorker } diff --git a/touch/Dragging.qml b/touch/Dragging.qml index ed9f72d..4225681 100644 --- a/touch/Dragging.qml +++ b/touch/Dragging.qml @@ -31,8 +31,8 @@ MouseArea { drag { target: parent axis: Drag.XAxis - minimumX: 0 - maximumX: canClose ? parent.width : 0 + minimumX: parent.leftDragLimit + maximumX: canClose ? pgst.width : 0 filterChildren: true } @@ -44,7 +44,7 @@ MouseArea { if (pressed) { dragging.stacking.stopAllAnimations(); } else { - if (parent.x > parent.width / 3) { + if (parent.x > pgst.width * 3 / 4) { dragging.stacking.startFadeOut(); } else { dragging.stacking.fadeInAgain(); diff --git a/touch/EpisodeDetail.qml b/touch/EpisodeDetail.qml index 536ed23..2b5a0f9 100644 --- a/touch/EpisodeDetail.qml +++ b/touch/EpisodeDetail.qml @@ -27,6 +27,8 @@ import 'icons/icons.js' as Icons SlidePage { id: detailPage + width: pgst.width / 3 + property int episode_id property string title property string link @@ -43,7 +45,7 @@ SlidePage { visible: !detailPage.ready } - Component.onCompleted: { + onEpisode_idChanged: { py.call('main.show_episode', [episode_id], function (episode) { detailPage.title = episode.title; descriptionLabel.text = episode.description; diff --git a/touch/EpisodeListView.qml b/touch/EpisodeListView.qml index 4fd0088..3430ac9 100644 --- a/touch/EpisodeListView.qml +++ b/touch/EpisodeListView.qml @@ -27,9 +27,14 @@ import 'common/util.js' as Util PListView { id: episodeList + property alias podcast_id: episodeListModel.podcast_id property int selectedIndex: -1 + onPodcast_idChanged: { + selectedIndex = -1; + } + PScrollIntoView { id: scrollIntoView } onSelectedIndexChanged: { diff --git a/touch/EpisodesPage.qml b/touch/EpisodesPage.qml index dc046c2..a4ca5b9 100644 --- a/touch/EpisodesPage.qml +++ b/touch/EpisodesPage.qml @@ -28,6 +28,8 @@ import 'icons/icons.js' as Icons SlidePage { id: page + width: pgst.width / 3 + property int podcast_id property string title @@ -80,12 +82,6 @@ SlidePage { ], undefined, undefined, true); } - - Component.onCompleted: { - episodeList.model.podcast_id = podcast_id; - // List model will be loaded automatically on load - } - EpisodeQueryControl { id: queryControl model: episodeList.model @@ -95,5 +91,6 @@ SlidePage { EpisodeListView { id: episodeList title: page.title + podcast_id: page.podcast_id } } diff --git a/touch/Main.qml b/touch/Main.qml index 28c89f7..c55ebea 100644 --- a/touch/Main.qml +++ b/touch/Main.qml @@ -68,7 +68,7 @@ Item { // Initial focus focus: true - property real scalef: (width < height) ? (width / 480) : (height / 480) + property real scalef: 1.0 //(width < height) ? (width / 480) : (height / 480) property int shorterSide: (width < height) ? width : height property int dialogsVisible: 0 @@ -84,9 +84,9 @@ Item { } if (page.isDialog) { - children[index-1].opacity = 1; + //children[index-1].opacity = 1; } else { - children[index-1].opacity = x / width; + //children[index-1].opacity = x / width; } //children[index-1].pushPhase = x / width; @@ -115,6 +115,26 @@ Item { } } + onChildrenChanged: resizePages() + + function resizePages() { + var pages = []; + for (var i=0; i Date: Mon, 16 Mar 2015 20:20:38 +0100 Subject: [PATCH 02/16] IconContextMenu: Right-align icons --- touch/IconContextMenu.qml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/touch/IconContextMenu.qml b/touch/IconContextMenu.qml index 68711ea..b27fdf6 100644 --- a/touch/IconContextMenu.qml +++ b/touch/IconContextMenu.qml @@ -27,6 +27,10 @@ Item { Row { id: contextMenuRow - anchors.centerIn: parent + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + margins: Constants.layout.padding * pgst.scalef + } } } From 953905ba3b442402b57d98cea257f4a094c02f41 Mon Sep 17 00:00:00 2001 From: Thomas Perl Date: Mon, 16 Mar 2015 21:30:23 +0100 Subject: [PATCH 03/16] Use rectangle indicator instead of left-bar indicator --- touch/EpisodeItem.qml | 26 ++++++++------------------ touch/PodcastItem.qml | 21 +++++++-------------- touch/RectangleIndicator.qml | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 32 deletions(-) create mode 100644 touch/RectangleIndicator.qml diff --git a/touch/EpisodeItem.qml b/touch/EpisodeItem.qml index 8e28d15..8a8a55e 100644 --- a/touch/EpisodeItem.qml +++ b/touch/EpisodeItem.qml @@ -135,22 +135,22 @@ Item { Rectangle { anchors { top: parent.top - left: downloadIndicator.right + left: parent.left } height: Constants.layout.padding * pgst.scalef - width: (parent.width - downloadIndicator.width) * progress + width: parent.width * progress color: Constants.colors.download } Rectangle { anchors { bottom: parent.bottom - left: downloadIndicator.right + left: parent.left } height: Constants.layout.padding * pgst.scalef - width: (parent.width - downloadIndicator.width) * playbackProgress + width: parent.width * playbackProgress color: titleLabel.color opacity: episodeItem.isPlaying ? 1 : .2 } @@ -163,27 +163,17 @@ Item { right: parent.right } - Rectangle { + RectangleIndicator { id: downloadIndicator - - width: Constants.layout.padding * pgst.scalef * (downloadState == Constants.state.downloaded) - - Behavior on width { PropertyAnimation { } } - - anchors { - top: parent.top - bottom: parent.bottom - left: parent.left - } - + enabled: downloadState == Constants.state.downloaded color: titleLabel.color } Column { anchors { left: parent.left - leftMargin: 2 * Constants.layout.padding * pgst.scalef - right: parent.right + leftMargin: Constants.layout.padding * pgst.scalef + right: downloadIndicator.left rightMargin: Constants.layout.padding * pgst.scalef verticalCenter: parent.verticalCenter } diff --git a/touch/PodcastItem.qml b/touch/PodcastItem.qml index 91c15d5..3eb80fd 100644 --- a/touch/PodcastItem.qml +++ b/touch/PodcastItem.qml @@ -33,19 +33,6 @@ ButtonArea { right: parent.right } - Rectangle { - width: Constants.layout.padding * pgst.scalef * (newEpisodes > 0) - Behavior on width { PropertyAnimation { } } - - anchors { - top: cover.top - bottom: cover.bottom - left: parent.left - } - - color: Constants.colors.fresh - } - CoverArt { id: cover visible: !updating @@ -85,7 +72,7 @@ ButtonArea { PLabel { id: downloadsLabel anchors { - right: parent.right + right: newEpisodesIndicator.enabled ? newEpisodesIndicator.left : parent.right rightMargin: Constants.layout.padding * pgst.scalef verticalCenter: parent.verticalCenter } @@ -93,4 +80,10 @@ ButtonArea { text: downloaded ? downloaded : '' color: Constants.colors.text } + + RectangleIndicator { + id: newEpisodesIndicator + enabled: newEpisodes > 0 + color: Constants.colors.fresh + } } diff --git a/touch/RectangleIndicator.qml b/touch/RectangleIndicator.qml new file mode 100644 index 0000000..08adccd --- /dev/null +++ b/touch/RectangleIndicator.qml @@ -0,0 +1,36 @@ + +/** + * + * gPodder QML UI Reference Implementation + * Copyright (c) 2015, Thomas Perl + * + * 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 { + width: Constants.layout.padding * 2 * pgst.scalef * enabled + height: width + + Behavior on width { PropertyAnimation { } } + + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + margins: Constants.layout.padding * 2 * pgst.scalef - width / 2 + } +} From e57690923531e796edf1d3d31faba9cccbf776a0 Mon Sep 17 00:00:00 2001 From: Thomas Perl Date: Mon, 16 Mar 2015 21:36:53 +0100 Subject: [PATCH 04/16] Use stack icon instead of vellipsis for menu vellipsis doesn't seem to be available under Android 4.0. --- touch/Main.qml | 2 +- touch/SlidePage.qml | 2 +- touch/icons/icons.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/touch/Main.qml b/touch/Main.qml index 28c89f7..9d4f850 100644 --- a/touch/Main.qml +++ b/touch/Main.qml @@ -125,7 +125,7 @@ Item { pgst.hasBackButton = Qt.binding(function () { return page.isDialog || page.canClose; }); pgst.hasMenuButton = Qt.binding(function () { return !page.isDialog && page.hasMenuButton; }); pgst.menuButtonLabel = Qt.binding(function () { return (!page.isDialog && pgst.hasMenuButton) ? page.menuButtonLabel : 'Menu'; }); - pgst.menuButtonIcon = Qt.binding(function () { return (!page.isDialog && pgst.hasMenuButton) ? page.menuButtonIcon : Icons.vellipsis; }); + pgst.menuButtonIcon = Qt.binding(function () { return (!page.isDialog && pgst.hasMenuButton) ? page.menuButtonIcon : Icons.stack; }); if (!page.isDialog) { pgst.windowTitle = page.title || 'gPodder'; diff --git a/touch/SlidePage.qml b/touch/SlidePage.qml index 182fdf6..cdad75f 100644 --- a/touch/SlidePage.qml +++ b/touch/SlidePage.qml @@ -36,7 +36,7 @@ Rectangle { property string title: '' property bool hasMenuButton: false property string menuButtonLabel: 'Menu' - property string menuButtonIcon: Icons.vellipsis + property string menuButtonIcon: Icons.stack signal menuButtonClicked() function closePage() { diff --git a/touch/icons/icons.js b/touch/icons/icons.js index 4372966..344a50b 100644 --- a/touch/icons/icons.js +++ b/touch/icons/icons.js @@ -19,8 +19,8 @@ var folder = '\ue065'; var magnifying_glass = '\ue074'; var cog = '\u2699'; var link = '\ue077'; -var vellipsis = '\u22ee'; var paperclip = '\ue08a'; var tag_fill = '\ue02b'; var headphones = '\ue061'; var sleep = '\u263e'; +var stack = '\ue020'; From 7a1c0c61735d680c4e0542ead9772195b63f497d Mon Sep 17 00:00:00 2001 From: Thomas Perl Date: Mon, 16 Mar 2015 21:42:35 +0100 Subject: [PATCH 05/16] Platform style: Use toolbar on top by default --- common/GPodderPlatform.qml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/common/GPodderPlatform.qml b/common/GPodderPlatform.qml index 3fe70b1..57fb97d 100644 --- a/common/GPodderPlatform.qml +++ b/common/GPodderPlatform.qml @@ -26,9 +26,11 @@ Item { property bool android: (typeof(gpodderAndroid) !== 'undefined') || emulatingAndroid property bool needsBackButton: !android - property bool toolbarOnTop: android + + property bool toolbarOnTop: true property bool invertedToolbar: toolbarOnTop property bool titleInToolbar: toolbarOnTop - property bool floatingPlayButton: android - property bool hideDisabledMenu: android + + property bool floatingPlayButton: true + property bool hideDisabledMenu: true } From 1fb4c0df401c32dc23a68debc1098750a00fe14d Mon Sep 17 00:00:00 2001 From: Thomas Perl Date: Mon, 16 Mar 2015 21:48:04 +0100 Subject: [PATCH 06/16] PEP-8 fixes --- main.py | 18 ++++++++---------- setup.cfg | 2 ++ 2 files changed, 10 insertions(+), 10 deletions(-) create mode 100644 setup.cfg diff --git a/main.py b/main.py index 08695ba..85a5b80 100644 --- a/main.py +++ b/main.py @@ -40,6 +40,7 @@ import datetime logger = logging.getLogger(__name__) + def run_in_background_thread(f): """Decorator for functions that take longer to finish @@ -142,8 +143,7 @@ class gPotherSide: def _get_podcasts_sorted(self): sort_key = self.core.model.podcast_sort_key - return sorted(self.core.model.get_podcasts(), - key=lambda podcast: (podcast.section, sort_key(podcast))) + return sorted(self.core.model.get_podcasts(), key=lambda podcast: (podcast.section, sort_key(podcast))) def load_podcasts(self): podcasts = self._get_podcasts_sorted() @@ -319,8 +319,7 @@ class gPotherSide: try: podcast.update() except Exception as e: - logger.warn('Could not update %s: %s', podcast.url, - e, exc_info=True) + logger.warn('Could not update %s: %s', podcast.url, e, exc_info=True) pyotherside.send('updated-podcast', self.convert_podcast(podcast)) pyotherside.send('update-stats') @@ -336,9 +335,7 @@ class gPotherSide: return { 'title': episode.title, 'podcast_title': episode.podcast.title, - 'source': episode.local_filename(False) - if episode.state == gpodder.STATE_DOWNLOADED - else episode.url, + 'source': episode.local_filename(False) if episode.state == gpodder.STATE_DOWNLOADED else episode.url, 'position': episode.current_position, 'total': episode.total_time, 'video': episode.file_type() == 'video', @@ -372,7 +369,9 @@ class gPotherSide: yield '%.2f MiB' % (episode.file_size / (1024 * 1024)) if episode.total_time > 0: - yield '%02d:%02d:%02d' % (episode.total_time / (60 * 60), (episode.total_time / 60) % 60, episode.total_time % 60) + yield '%02d:%02d:%02d' % (episode.total_time / (60 * 60), + (episode.total_time / 60) % 60, + episode.total_time % 60) def show_podcast(self, podcast_id): podcast = self._get_podcast_by_id(podcast_id) @@ -404,8 +403,7 @@ class gPotherSide: return [{ 'label': provider.name, 'can_search': provider.kind == provider.PROVIDER_SEARCH - } for provider in sorted(registry.directory.select(select_provider), - key=provider_sort_key, reverse=True)] + } for provider in sorted(registry.directory.select(select_provider), key=provider_sort_key, reverse=True)] def get_directory_entries(self, provider, query): def match_provider(p): diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..68859ad --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[pep8] +max-line-length = 120 From bfb22d0d3f7f8316aa7523e51fc3ae16d3a4817a Mon Sep 17 00:00:00 2001 From: Thomas Perl Date: Mon, 16 Mar 2015 21:51:08 +0100 Subject: [PATCH 07/16] IconContextMenu: Import fix --- touch/IconContextMenu.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/touch/IconContextMenu.qml b/touch/IconContextMenu.qml index b27fdf6..7703956 100644 --- a/touch/IconContextMenu.qml +++ b/touch/IconContextMenu.qml @@ -20,6 +20,8 @@ import QtQuick 2.0 +import 'common/constants.js' as Constants + Item { id: contextMenu default property alias children: contextMenuRow.children From ad275cf1fbfe44e3c1504df961cd49e7564f23bf Mon Sep 17 00:00:00 2001 From: Thomas Perl Date: Mon, 16 Mar 2015 21:59:29 +0100 Subject: [PATCH 08/16] Improve section header --- touch/EpisodeQueryPage.qml | 6 +++++- touch/SectionHeader.qml | 20 +++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/touch/EpisodeQueryPage.qml b/touch/EpisodeQueryPage.qml index ebd8d01..bf8f423 100644 --- a/touch/EpisodeQueryPage.qml +++ b/touch/EpisodeQueryPage.qml @@ -44,6 +44,10 @@ SlidePage { title: 'Episodes' section.property: 'section' - section.delegate: SectionHeader { text: section } + section.delegate: SectionHeader { + text: section + color: episodeList.selectedIndex === -1 ? Constants.colors.secondaryHighlight : Constants.colors.text + opacity: episodeList.selectedIndex === -1 ? 1 : 0.2 + } } } diff --git a/touch/SectionHeader.qml b/touch/SectionHeader.qml index 55234f6..7ce75e0 100644 --- a/touch/SectionHeader.qml +++ b/touch/SectionHeader.qml @@ -24,15 +24,29 @@ import 'common/constants.js' as Constants Item { property alias text: pLabel.text + property alias color: pLabel.color height: 70 * pgst.scalef + width: parent.width + + Rectangle { + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + right: pLabel.left + margins: Constants.layout.padding * pgst.scalef + } + + color: pLabel.color + height: 1 * pgst.scalef + } PLabel { id: pLabel anchors { - left: parent.left - bottom: parent.bottom - margins: 10 * pgst.scalef + right: parent.right + verticalCenter: parent.verticalCenter + margins: Constants.layout.padding * pgst.scalef } color: Constants.colors.secondaryHighlight From ab4dfc611bc9aafc9ae5cf83fc0234bdc34a2e93 Mon Sep 17 00:00:00 2001 From: Thomas Perl Date: Sat, 23 May 2015 15:36:11 +0200 Subject: [PATCH 09/16] Settings page --- common/GPodderCore.qml | 10 ++++ touch/PSlider.qml | 32 +++++++++--- touch/PTextField.qml | 1 + touch/PodcastsPage.qml | 4 +- touch/SettingsLabel.qml | 32 ++++++++++++ touch/SettingsPage.qml | 113 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 184 insertions(+), 8 deletions(-) create mode 100644 touch/SettingsLabel.qml create mode 100644 touch/SettingsPage.qml diff --git a/common/GPodderCore.qml b/common/GPodderCore.qml index a5983f3..7920d32 100644 --- a/common/GPodderCore.qml +++ b/common/GPodderCore.qml @@ -77,6 +77,16 @@ Python { }); } + function setConfig(key, value) { + py.call('main.set_config_value', [key, value]); + } + + function getConfig(key, callback) { + py.call('main.get_config_value', [key], function (result) { + callback(result); + }); + } + onReceived: { console.log('unhandled message: ' + data); } diff --git a/touch/PSlider.qml b/touch/PSlider.qml index eb32ad3..2f6855f 100644 --- a/touch/PSlider.qml +++ b/touch/PSlider.qml @@ -28,6 +28,7 @@ Item { property real value property real min: 0.0 property real max: 1.0 + property real step: 0.0 property color color: Constants.colors.highlight property real displayedValue: mouseArea.pressed ? temporaryValue : value @@ -42,13 +43,22 @@ Item { MouseArea { id: mouseArea anchors.fill: parent - onClicked: slider.valueChangeRequested(min + (max - min) * (mouse.x / width)) - onPressed: { - slider.temporaryValue = (min + (max - min) * (mouse.x / width)); - } - onPositionChanged: { - slider.temporaryValue = (min + (max - min) * (mouse.x / width)); + function updateValue(x) { + if (x > width) { + x = width; + } else if (x < 0) { + x = 0; + } + var v = (max - min) * (x / width); + if (slider.step > 0.0) { + v = slider.step * parseInt(0.5 + v / slider.step); + } + slider.temporaryValue = min + v; + return slider.temporaryValue; } + onClicked: slider.valueChangeRequested(updateValue(mouse.x)); + onPressed: updateValue(mouse.x); + onPositionChanged: updateValue(mouse.x); preventStealing: true } @@ -68,4 +78,14 @@ Item { verticalCenter: parent.verticalCenter } } + + Repeater { + model: slider.step ? ((slider.max - slider.min) / slider.step - 1) : 0 + delegate: Rectangle { + width: Math.max(1, 1 * pgst.scalef) + height: parent.height + color: Constants.colors.area + x: parent.width * ((slider.step * (1 + index)) / (slider.max - slider.min)); + } + } } diff --git a/touch/PTextField.qml b/touch/PTextField.qml index 660d630..6c33d2e 100644 --- a/touch/PTextField.qml +++ b/touch/PTextField.qml @@ -50,6 +50,7 @@ Item { right: clipboardIcon.left margins: 5 * pgst.scalef } + clip: true inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText diff --git a/touch/PodcastsPage.qml b/touch/PodcastsPage.qml index b07129f..882252b 100644 --- a/touch/PodcastsPage.qml +++ b/touch/PodcastsPage.qml @@ -47,9 +47,9 @@ SlidePage { } }, { - label: 'About', + label: 'Settings', callback: function () { - pgst.loadPage('AboutPage.qml'); + pgst.loadPage('SettingsPage.qml'); }, }, { diff --git a/touch/SettingsLabel.qml b/touch/SettingsLabel.qml new file mode 100644 index 0000000..643189e --- /dev/null +++ b/touch/SettingsLabel.qml @@ -0,0 +1,32 @@ + +/** + * + * gPodder QML UI Reference Implementation + * Copyright (c) 2015, Thomas Perl + * + * 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 + +PLabel { + anchors { + left: parent.left + right: parent.right + margins: Constants.layout.padding * pgst.scalef + } + color: Constants.colors.secondaryHighlight +} diff --git a/touch/SettingsPage.qml b/touch/SettingsPage.qml new file mode 100644 index 0000000..8d47886 --- /dev/null +++ b/touch/SettingsPage.qml @@ -0,0 +1,113 @@ + +/** + * + * gPodder QML UI Reference Implementation + * Copyright (c) 2015, Thomas Perl + * + * 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 + +SlidePage { + id: page + + Component.onCompleted: { + py.getConfig('plugins.youtube.api_key_v3', function (value) { + youtube_api_key_v3.text = value; + }); + py.getConfig('limit.episodes', function (value) { + limit_episodes.value = value; + }); + } + + Component.onDestruction: { + py.setConfig('plugins.youtube.api_key_v3', youtube_api_key_v3.text); + py.setConfig('limit.episodes', parseInt(limit_episodes.value)); + } + + Flickable { + id: flickable + anchors.fill: parent + boundsBehavior: Flickable.StopAtBounds + + contentWidth: detailColumn.width + contentHeight: detailColumn.height + detailColumn.spacing + + Column { + id: detailColumn + + width: page.width + spacing: 15 * pgst.scalef + + SlidePageHeader { title: 'Settings' } + + SectionHeader { text: 'YouTube' } + + SettingsLabel { text: 'API Key (v3)' } + + PTextField { + id: youtube_api_key_v3 + anchors { + left: parent.left + right: parent.right + margins: Constants.layout.padding * pgst.scalef + } + } + + SectionHeader { text: 'Limits' } + + SettingsLabel { text: 'Maximum episodes per feed' } + + PSlider { + id: limit_episodes + min: 100 + step: 100 + max: 1000 + anchors { + left: parent.left + right: parent.right + margins: Constants.layout.padding * pgst.scalef + } + onValueChangeRequested: { value = newValue; } + } + + PLabel { + text: parseInt(limit_episodes.displayedValue) + anchors { + left: parent.left + right: parent.right + margins: Constants.layout.padding * pgst.scalef + } + } + + SectionHeader { text: 'About' } + + ButtonArea { + width: parent.width + height: Constants.layout.item.height * pgst.scalef + PLabel { + anchors.centerIn: parent + text: 'About gPodder ' + py.uiversion + } + onClicked: pgst.loadPage('AboutPage.qml') + } + } + } + + PScrollDecorator { flickable: flickable } +} + From 4a0b3f1b128a2578665b5b7e24aec61c1862bdc2 Mon Sep 17 00:00:00 2001 From: Thomas Perl Date: Sun, 24 May 2015 17:44:39 +0200 Subject: [PATCH 10/16] Revert "Use QML WorkerScript for loading and updating models" This reverts commit d8fd1ff3dd90d0172bc018ebc4305f05bfc546fb. --- common/GPodderEpisodeListModel.qml | 15 ++--- common/GPodderEpisodeListModelConnections.qml | 4 +- common/GPodderPodcastListModel.qml | 6 +- common/ModelWorkerScript.qml | 67 ------------------- common/modelworker.js | 42 ------------ common/util.js | 25 +++++++ 6 files changed, 33 insertions(+), 126 deletions(-) delete mode 100644 common/ModelWorkerScript.qml delete mode 100644 common/modelworker.js diff --git a/common/GPodderEpisodeListModel.qml b/common/GPodderEpisodeListModel.qml index 33535b6..a51b540 100644 --- a/common/GPodderEpisodeListModel.qml +++ b/common/GPodderEpisodeListModel.qml @@ -62,10 +62,6 @@ ListModel { }); } - property var worker: ModelWorkerScript { - id: modelWorker - } - function forEachEpisode(callback) { // Go from bottom up (= chronological order) for (var i=count-1; i>=0; i--) { @@ -126,12 +122,11 @@ ListModel { ready = false; py.call('main.load_episodes', [podcast_id, query], function (episodes) { - modelWorker.updateModelFrom(episodeListModel, episodes, function () { - episodeListModel.ready = true; - if (callback !== undefined) { - callback(); - } - }); + Util.updateModelFrom(episodeListModel, episodes); + episodeListModel.ready = true; + if (callback !== undefined) { + callback(); + } }); } } diff --git a/common/GPodderEpisodeListModelConnections.qml b/common/GPodderEpisodeListModelConnections.qml index 22806ad..9744dab 100644 --- a/common/GPodderEpisodeListModelConnections.qml +++ b/common/GPodderEpisodeListModelConnections.qml @@ -26,11 +26,11 @@ Connections { target: py onDownloadProgress: { - episodeListModel.worker.updateModelWith(episodeListModel, 'id', episode_id, + Util.updateModelWith(episodeListModel, 'id', episode_id, {'progress': progress}); } onPlaybackProgress: { - episodeListModel.worker.updateModelWith(episodeListModel, 'id', episode_id, + Util.updateModelWith(episodeListModel, 'id', episode_id, {'playbackProgress': progress}); } onUpdatedEpisode: { diff --git a/common/GPodderPodcastListModel.qml b/common/GPodderPodcastListModel.qml index aa17c2f..285bf16 100644 --- a/common/GPodderPodcastListModel.qml +++ b/common/GPodderPodcastListModel.qml @@ -25,13 +25,9 @@ import 'util.js' as Util ListModel { id: podcastListModel - property var worker: ModelWorkerScript { - id: modelWorker - } - function reload() { py.call('main.load_podcasts', [], function (podcasts) { - modelWorker.updateModelFrom(podcastListModel, podcasts); + Util.updateModelFrom(podcastListModel, podcasts); }); } } diff --git a/common/ModelWorkerScript.qml b/common/ModelWorkerScript.qml deleted file mode 100644 index 19c0057..0000000 --- a/common/ModelWorkerScript.qml +++ /dev/null @@ -1,67 +0,0 @@ - -/** - * - * gPodder QML UI Reference Implementation - * Copyright (c) 2015, Thomas Perl - * - * 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 - - -WorkerScript { - source: 'modelworker.js' - - property int callbacks_seq: 1 - property var callbacks: ({}) - - function refCallback(callback) { - var id = callbacks_seq++; - callbacks[id] = callback; - return id; - } - - function unrefCallback(callback) { - var result = callbacks[callback]; - delete callbacks[callback]; - return result; - } - - function updateModelFrom(model, data, callback) { - sendMessage({ - action: 'updateModelFrom', - model: model, - data: data, - callback: refCallback(callback), - }); - } - - function updateModelWith(model, key, value, update, callback) { - sendMessage({ - action: 'updateModelWith', - model: model, - key: key, - value: value, - update: update, - callback: refCallback(callback), - }); - } - - onMessage: { - if (messageObject.callback !== undefined) { - unrefCallback(messageObject.callback)(); - } - } -} diff --git a/common/modelworker.js b/common/modelworker.js deleted file mode 100644 index 44561bd..0000000 --- a/common/modelworker.js +++ /dev/null @@ -1,42 +0,0 @@ -function updateModelFrom(model, data) { - // TODO: This is very naive at the moment, we should do proper remove(), - // move(), set() and insert() calls, so that the UI can animate changes. - for (var i=0; i data.length) { - model.remove(model.count-1); - } - - model.sync(); -} - -function updateModelWith(model, key, value, update) { - for (var row=0; row data.length) { + model.remove(model.count-1); + } +} + +function updateModelWith(model, key, value, update) { + for (var row=0; row Date: Sun, 24 May 2015 18:44:33 +0200 Subject: [PATCH 11/16] PListView: Fix layout issues on resize --- touch/PListView.qml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/touch/PListView.qml b/touch/PListView.qml index 7f077d6..e607207 100644 --- a/touch/PListView.qml +++ b/touch/PListView.qml @@ -30,6 +30,19 @@ ListView { boundsBehavior: Flickable.StopAtBounds + function relayout() { + var _contentY = contentY; + var _model = model; + model = null; + model = _model; + contentY = _contentY; + } + + Connections { + target: pgst + onScalefChanged: relayout(); + } + header: SlidePageHeader { id: header title: pListView.title From 5186558174e1165852b72e93d4467f91b4159c8d Mon Sep 17 00:00:00 2001 From: Thomas Perl Date: Sun, 24 May 2015 20:41:56 +0200 Subject: [PATCH 12/16] gPodder QML UI 4.6.0 --- main.py | 2 +- makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 85a5b80..eb03f31 100644 --- a/main.py +++ b/main.py @@ -19,7 +19,7 @@ # of gpodder-core, but we might have a different release schedule later on. If # we decide to have parallel releases, we can at least start using this version # to check if the core version is compatible with the QML UI version. -__version__ = '4.4.0' +__version__ = '4.6.0' import sys import os diff --git a/makefile b/makefile index 27a4fd0..0d05e9a 100644 --- a/makefile +++ b/makefile @@ -1,5 +1,5 @@ PROJECT := gpodder-ui-qml -VERSION := 4.4.0 +VERSION := 4.6.0 all: @echo "" From cfed87916b5b8cb4a2a35a1692fb7d8b78414ea5 Mon Sep 17 00:00:00 2001 From: Thomas Perl Date: Wed, 8 Jul 2015 11:00:33 +0200 Subject: [PATCH 13/16] Welcome to 2015 --- touch/AboutPage.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/touch/AboutPage.qml b/touch/AboutPage.qml index fa62f11..36944a4 100644 --- a/touch/AboutPage.qml +++ b/touch/AboutPage.qml @@ -73,7 +73,7 @@ SlidePage { anchors.horizontalCenter: parent.horizontalCenter wrapMode: Text.WordWrap text: [ - '© 2005-2014 Thomas Perl and the gPodder Team', + '© 2005-2015 Thomas Perl and the gPodder Team', 'License: ISC / GPLv3 or later', 'Website: http://gpodder.org/', '', From f3fb2585dc327f78e56331acff708bbc7fd326b4 Mon Sep 17 00:00:00 2001 From: Thomas Perl Date: Wed, 3 Feb 2016 22:54:33 +0100 Subject: [PATCH 14/16] desktop: Add/remove podcasts --- desktop/dialogs/AddPodcastDialog.qml | 36 ++++++++++ desktop/dialogs/EpisodeDetailsDialog.qml | 26 +++++++ desktop/gpodder.qml | 86 +++++++++++++++++++++--- 3 files changed, 140 insertions(+), 8 deletions(-) create mode 100644 desktop/dialogs/AddPodcastDialog.qml create mode 100644 desktop/dialogs/EpisodeDetailsDialog.qml diff --git a/desktop/dialogs/AddPodcastDialog.qml b/desktop/dialogs/AddPodcastDialog.qml new file mode 100644 index 0000000..88defa1 --- /dev/null +++ b/desktop/dialogs/AddPodcastDialog.qml @@ -0,0 +1,36 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.0 +import QtQuick.Layouts 1.0 +import QtQuick.Dialogs 1.2 + +import '../common' +import '../common/util.js' as Util + +Dialog { + signal addUrl(string url) + + width: 300 + height: 100 + title: 'Add new podcast' + standardButtons: StandardButton.Open | StandardButton.Cancel + + RowLayout { + anchors.fill: parent + + Label { + text: 'URL:' + } + + TextField { + id: urlEntry + focus: true + + Layout.fillWidth: true + } + } + + onAccepted: { + addUrl(urlEntry.text); + visible = false; + } +} diff --git a/desktop/dialogs/EpisodeDetailsDialog.qml b/desktop/dialogs/EpisodeDetailsDialog.qml new file mode 100644 index 0000000..e8abe77 --- /dev/null +++ b/desktop/dialogs/EpisodeDetailsDialog.qml @@ -0,0 +1,26 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.0 +import QtQuick.Layouts 1.0 +import QtQuick.Dialogs 1.2 + +import '../common' +import '../common/util.js' as Util + +Rectangle { + property var episode + color: '#aa000000' + + anchors.fill: parent + + MouseArea { + anchors.fill: parent + onClicked: parent.destroy(); + } + + TextArea { + anchors.fill: parent + anchors.margins: 50 + readOnly: true + text: episode ? episode.description : '...' + } +} diff --git a/desktop/gpodder.qml b/desktop/gpodder.qml index 06329eb..f411fee 100644 --- a/desktop/gpodder.qml +++ b/desktop/gpodder.qml @@ -1,11 +1,16 @@ import QtQuick 2.0 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 +import QtQuick.Dialogs 1.2 + +import 'dialogs' import 'common' import 'common/util.js' as Util ApplicationWindow { + id: appWindow + width: 500 height: 400 @@ -15,47 +20,102 @@ ApplicationWindow { id: py } + function openDialog(filename, callback) { + var component = Qt.createComponent(filename); + + function createDialog() { + if (component.status === Component.Ready) { + var dialog = component.createObject(appWindow, {}); + dialog.visible = true; + callback(dialog); + } + } + + if (component.status == Component.Ready) { + createDialog(); + } else { + component.statusChanged.connect(createDialog); + } + } + menuBar: MenuBar { - Menu { title: 'File'; MenuItem { text: 'Quit' } } + Menu { + title: 'File' + + MenuItem { + text: 'Add podcast' + onTriggered: { + openDialog('dialogs/AddPodcastDialog.qml', function (dialog) { + dialog.addUrl.connect(function (url) { + py.call('main.subscribe', [url]); + }); + }); + } + } + + MenuItem { + text: 'Quit' + onTriggered: Qt.quit() + } + } } SplitView { anchors.fill: parent TableView { + id: podcastListView + width: 200 model: GPodderPodcastListModel { id: podcastListModel } GPodderPodcastListModelConnections {} headerVisible: false alternatingRowColors: false + Menu { + id: podcastContextMenu + MenuItem { + text: 'Unsubscribe' + onTriggered: { + var podcast_id = podcastListModel.get(podcastListView.currentRow).id; + py.call('main.unsubscribe', [podcast_id]); + } + } + } + rowDelegate: Rectangle { - height: 60 + height: 40 color: styleData.selected ? '#eee' : '#fff' + + MouseArea { + acceptedButtons: Qt.RightButton + anchors.fill: parent + onClicked: podcastContextMenu.popup() + } } TableViewColumn { role: 'coverart' title: 'Image' delegate: Item { - height: 60 - width: 60 + height: 32 + width: 32 Image { source: styleData.value - width: 50 - height: 50 + width: 32 + height: 32 anchors.centerIn: parent } } - width: 60 + width: 40 } TableViewColumn { role: 'title' title: 'Podcast' delegate: Item { - height: 60 + height: 40 Text { text: styleData.value anchors.verticalCenter: parent.verticalCenter @@ -75,6 +135,16 @@ ApplicationWindow { GPodderEpisodeListModelConnections {} TableViewColumn { role: 'title'; title: 'Title' } + + onActivated: { + var episode_id = episodeListModel.get(currentRow).id; + + openDialog('dialogs/EpisodeDetailsDialog.qml', function (dialog) { + py.call('main.show_episode', [episode_id], function (episode) { + dialog.episode = episode; + }); + }); + } } } } From 1386245b50284cae3f33ed646037539d297e087c Mon Sep 17 00:00:00 2001 From: Thomas Perl Date: Thu, 4 Feb 2016 20:42:27 +0100 Subject: [PATCH 15/16] Desktop UI: Port pill for podcast list --- desktop/Pill.qml | 8 ++ desktop/gpodder.qml | 322 +++++++++++++++++++++++++++++++++++--------- main.py | 114 ++++++++++++++++ 3 files changed, 381 insertions(+), 63 deletions(-) create mode 100644 desktop/Pill.qml diff --git a/desktop/Pill.qml b/desktop/Pill.qml new file mode 100644 index 0000000..dc0f530 --- /dev/null +++ b/desktop/Pill.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 + +Image { + property int leftCount: 0 + property int rightCount: 0 + source: 'image://python/pill/' + leftCount + '/' + rightCount + cache: true +} diff --git a/desktop/gpodder.qml b/desktop/gpodder.qml index f411fee..54a822e 100644 --- a/desktop/gpodder.qml +++ b/desktop/gpodder.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick 2.3 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 import QtQuick.Dialogs 1.2 @@ -7,6 +7,7 @@ import 'dialogs' import 'common' import 'common/util.js' as Util +import 'common/constants.js' as Constants ApplicationWindow { id: appWindow @@ -63,87 +64,282 @@ ApplicationWindow { SplitView { anchors.fill: parent - TableView { - id: podcastListView + ColumnLayout { + TableView { + Layout.fillHeight: true + Layout.fillWidth: true - width: 200 - model: GPodderPodcastListModel { id: podcastListModel } - GPodderPodcastListModelConnections {} - headerVisible: false - alternatingRowColors: false + id: podcastListView - Menu { - id: podcastContextMenu - MenuItem { - text: 'Unsubscribe' - onTriggered: { - var podcast_id = podcastListModel.get(podcastListView.currentRow).id; - py.call('main.unsubscribe', [podcast_id]); + model: GPodderPodcastListModel { id: podcastListModel } + GPodderPodcastListModelConnections {} + headerVisible: false + alternatingRowColors: false + horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff + + Menu { + id: podcastContextMenu + + MenuItem { + text: 'Unsubscribe' + onTriggered: { + var podcast_id = podcastListModel.get(podcastListView.currentRow).id; + py.call('main.unsubscribe', [podcast_id]); + } } - } - } - rowDelegate: Rectangle { - height: 40 - color: styleData.selected ? '#eee' : '#fff' - - MouseArea { - acceptedButtons: Qt.RightButton - anchors.fill: parent - onClicked: podcastContextMenu.popup() - } - } - - TableViewColumn { - role: 'coverart' - title: 'Image' - delegate: Item { - height: 32 - width: 32 - Image { - source: styleData.value - width: 32 - height: 32 - anchors.centerIn: parent + MenuItem { + text: 'Mark episodes as old' + onTriggered: { + var podcast_id = podcastListModel.get(podcastListView.currentRow).id; + py.call('main.mark_episodes_as_old', [podcast_id]); + } } } - width: 40 - } - - TableViewColumn { - role: 'title' - title: 'Podcast' - delegate: Item { + rowDelegate: Rectangle { height: 40 - Text { - text: styleData.value - anchors.verticalCenter: parent.verticalCenter + color: styleData.selected ? Constants.colors.select : 'transparent' + + MouseArea { + acceptedButtons: Qt.RightButton + anchors.fill: parent + onClicked: podcastContextMenu.popup() } } + + TableViewColumn { + id: coverartColumn + width: 40 + + role: 'coverart' + title: 'Image' + delegate: Item { + height: 32 + width: 32 + Image { + mipmap: true + source: styleData.value + width: 32 + height: 32 + anchors.centerIn: parent + } + } + } + + TableViewColumn { + role: 'title' + title: 'Podcast' + width: podcastListView.width - coverartColumn.width - indicatorColumn.width - 2 * 5 + + delegate: Item { + property var row: podcastListModel.get(styleData.row) + + width: parent.width + height: 40 + + Column { + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.right: parent.right + spacing: -5 + + Text { + width: parent.width + color: styleData.selected ? 'white' : 'black' + + font.bold: row.newEpisodes + text: styleData.value + elide: styleData.elideMode + } + + Text { + width: parent.width + font.pointSize: 10 + color: styleData.selected ? 'white' : 'black' + + text: row.description + elide: styleData.elideMode + } + } + } + } + + TableViewColumn { + id: indicatorColumn + width: 50 + + role: 'indicator' + title: 'Indicator' + + delegate: Item { + height: 32 + width: 50 + property var row: podcastListModel.get(styleData.row) + + Rectangle { + anchors.centerIn: parent + visible: row.updating + + color: styleData.selected ? '#ffffff' : '#000000' + + property real phase: 0 + + width: 15 + 3 * Math.sin(phase) + height: width + + PropertyAnimation on phase { + loops: Animation.Infinite + duration: 2000 + running: parent.visible + from: 0 + to: 2*Math.PI + } + } + + Pill { + anchors.centerIn: parent + + visible: !row.updating + + leftCount: row.unplayed + rightCount: row.downloaded + } + } + } + + onCurrentRowChanged: { + var id = podcastListModel.get(currentRow).id; + episodeListModel.loadEpisodes(id); + } } - onCurrentRowChanged: { - var id = podcastListModel.get(currentRow).id; - episodeListModel.loadEpisodes(id); + + Button { + Layout.fillWidth: true + Layout.margins: 3 + text: 'Check for new episodes' + onClicked: py.call('main.check_for_episodes'); + enabled: !py.refreshing } } - TableView { - Layout.fillWidth: true - model: GPodderEpisodeListModel { id: episodeListModel } - GPodderEpisodeListModelConnections {} + SplitView { + orientation: Orientation.Vertical - TableViewColumn { role: 'title'; title: 'Title' } + TableView { + id: episodeListView - onActivated: { - var episode_id = episodeListModel.get(currentRow).id; + Layout.fillWidth: true + model: GPodderEpisodeListModel { id: episodeListModel } + GPodderEpisodeListModelConnections {} + selectionMode: SelectionMode.MultiSelection - openDialog('dialogs/EpisodeDetailsDialog.qml', function (dialog) { - py.call('main.show_episode', [episode_id], function (episode) { - dialog.episode = episode; + function forEachSelectedEpisode(callback) { + episodeListView.selection.forEach(function(rowIndex) { + var episode_id = episodeListModel.get(rowIndex).id; + callback(episode_id); }); - }); + } + + Menu { + id: episodeContextMenu + + MenuItem { + text: 'Toggle new' + onTriggered: { + episodeListView.forEachSelectedEpisode(function (episode_id) { + py.call('main.toggle_new', [episode_id]); + }); + } + } + + MenuItem { + text: 'Download' + onTriggered: { + episodeListView.forEachSelectedEpisode(function (episode_id) { + py.call('main.download_episode', [episode_id]); + }); + } + } + + MenuItem { + text: 'Delete' + onTriggered: { + episodeListView.forEachSelectedEpisode(function (episode_id) { + py.call('main.delete_episode', [episode_id]); + }); + } + } + } + + rowDelegate: Rectangle { + height: 40 + color: styleData.selected ? Constants.colors.select : 'transparent' + + MouseArea { + acceptedButtons: Qt.RightButton + anchors.fill: parent + onClicked: episodeContextMenu.popup() + } + } + + TableViewColumn { + role: 'title' + title: 'Episode' + + delegate: Row { + property var row: episodeListModel.get(styleData.row) + height: 32 + + Item { + width: 32 + height: 32 + + Rectangle { + anchors.centerIn: parent + width: 10 + height: 10 + color: episodeTitle.color + } + + anchors.verticalCenter: parent.verticalCenter + opacity: row.downloadState === Constants.state.downloaded + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: -5 + + Text { + id: episodeTitle + text: styleData.value + elide: styleData.elideMode + color: styleData.selected ? 'white' : 'black' + font.bold: row.isNew + } + + Text { + text: row.progress ? ('Downloading: ' + parseInt(100*row.progress) + '%') : row.subtitle + elide: styleData.elideMode + color: styleData.selected ? 'white' : 'black' + } + } + } + } + + onActivated: { + var episode_id = episodeListModel.get(currentRow).id; + + openDialog('dialogs/EpisodeDetailsDialog.qml', function (dialog) { + py.call('main.show_episode', [episode_id], function (episode) { + dialog.episode = episode; + }); + }); + } + } + + Label { } } } diff --git a/main.py b/main.py index eb03f31..f7ec09e 100644 --- a/main.py +++ b/main.py @@ -37,6 +37,7 @@ import logging import functools import time import datetime +import re logger = logging.getLogger(__name__) @@ -113,6 +114,7 @@ class gPotherSide: 'episodes': total, 'newEpisodes': new, 'downloaded': downloaded, + 'unplayed': unplayed, } def _get_cover(self, podcast): @@ -133,8 +135,10 @@ class gPotherSide: return { 'id': podcast.id, 'title': podcast.title, + 'description': podcast.one_line_description(), 'newEpisodes': new, 'downloaded': downloaded, + 'unplayed': unplayed, 'coverart': self._get_cover(podcast), 'updating': podcast._updating, 'section': podcast.section, @@ -420,6 +424,116 @@ class gPotherSide: return [] + +PILL_TEMPLATE = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {left_text} + {left_text} + + + + + + + {right_text} + {right_text} + + + +""" + + +class PillExpression(object): + def __init__(self, **kwargs): + self.kwargs = kwargs + + def __call__(self, matchobj): + return str(eval(matchobj.group(1), self.kwargs)) + + +def pill_image_provider(image_id, requested_size): + left_text, right_text = (int(x) for x in image_id.split('/')) + + width = 44 + height = 24 + radius = 6 + font_size = 13 + + text_lx = width / 4 + text_rx = width * 3 / 4 + + charheight = font_size + charwidth = font_size / 1.3 + + if left_text: + text_lx -= charwidth * len(str(left_text)) / 2 + if right_text: + text_rx -= charwidth * len(str(right_text)) / 2 + + outer_style = 'stroke: #333333; stroke-width: 1; fill-opacity: 0; stroke-opacity: 0.6;' + inner_style = 'stroke: #ffffff; stroke-width: 1; fill-opacity: 0; stroke-opacity: 0.3;' + + expression = PillExpression(height=height, width=width, left_text=left_text, + right_text=right_text, radius=radius, + lx=text_lx, rx=text_rx, font_size=font_size, + outer_style=outer_style, inner_style=inner_style) + svg = re.sub(r'[{]([^}]+)[}]', expression, PILL_TEMPLATE) + return bytearray(svg.encode('utf-8')), (width, height), pyotherside.format_data + + +@pyotherside.set_image_provider +def gpotherside_image_provider(image_id, requested_size): + provider, args = image_id.split('/', 1) + if provider == 'pill': + return pill_image_provider(args, requested_size) + + raise ValueError('Unknown provider: %s' % (provider,)) + + gpotherside = gPotherSide() pyotherside.atexit(gpotherside.atexit) From 8aab9adb4d9e0b4d669e44824d2e193f7278d480 Mon Sep 17 00:00:00 2001 From: Jeena Date: Wed, 10 Feb 2016 19:54:53 +0100 Subject: [PATCH 16/16] Implement OPML import in the desktop and touch ui The core has already implemented parsing OPML files from URLs and files, it only needs to be exposed by the UI which this parch does. --- desktop/dialogs/AddPodcastDialog.qml | 2 ++ desktop/gpodder.qml | 13 +++++++++++++ main.py | 17 +++++++++++++++++ touch/PodcastsPage.qml | 14 ++++++++++++++ 4 files changed, 46 insertions(+) diff --git a/desktop/dialogs/AddPodcastDialog.qml b/desktop/dialogs/AddPodcastDialog.qml index 88defa1..9a2d199 100644 --- a/desktop/dialogs/AddPodcastDialog.qml +++ b/desktop/dialogs/AddPodcastDialog.qml @@ -7,6 +7,7 @@ import '../common' import '../common/util.js' as Util Dialog { + property alias labelText: urlEntyLabel.text signal addUrl(string url) width: 300 @@ -18,6 +19,7 @@ Dialog { anchors.fill: parent Label { + id: urlEntyLabel text: 'URL:' } diff --git a/desktop/gpodder.qml b/desktop/gpodder.qml index 54a822e..7d4dec7 100644 --- a/desktop/gpodder.qml +++ b/desktop/gpodder.qml @@ -54,6 +54,19 @@ ApplicationWindow { } } + MenuItem { + text: 'Add from OPML' + onTriggered: { + openDialog('dialogs/AddPodcastDialog.qml', function(dialog) { + dialog.title = "Add from OPML" + dialog.labelText = "OPML URL:" + dialog.addUrl.connect(function (url) { + py.call('main.import_opml', [url]); + }) + }) + } + } + MenuItem { text: 'Quit' onTriggered: Qt.quit() diff --git a/main.py b/main.py index f7ec09e..3af1676 100644 --- a/main.py +++ b/main.py @@ -32,6 +32,7 @@ from gpodder.api import core from gpodder.api import util from gpodder.api import query from gpodder.api import registry +from gpodder import opml import logging import functools @@ -221,6 +222,21 @@ class gPotherSide: summary.sort(key=lambda e: e['newEpisodes'], reverse=True) return summary[:int(count)] + @run_in_background_thread + def import_opml(self, url): + """Import subscriptions from an OPML file + + import http://example.com/subscriptions.opml + + Import subscriptions from the given URL + + import ./feeds.opml + + Import subscriptions from a local file + """ + for channel in opml.Importer(url).items: + self.subscribe(channel['url']) + @run_in_background_thread def subscribe(self, url): url = self.core.model.normalize_feed_url(url) @@ -545,6 +561,7 @@ load_podcasts = gpotherside.load_podcasts load_episodes = gpotherside.load_episodes show_episode = gpotherside.show_episode play_episode = gpotherside.play_episode +import_opml = gpotherside.import_opml subscribe = gpotherside.subscribe unsubscribe = gpotherside.unsubscribe check_for_episodes = gpotherside.check_for_episodes diff --git a/touch/PodcastsPage.qml b/touch/PodcastsPage.qml index 882252b..6cdab93 100644 --- a/touch/PodcastsPage.qml +++ b/touch/PodcastsPage.qml @@ -66,6 +66,20 @@ SlidePage { }); }, }, + { + label: 'Add from OPML', + callback: function () { + var ctx = { py: py }; + pgst.loadPage('TextInputDialog.qml', { + buttonText: 'Subscribe', + placeholderText: 'OPML URL', + pasteOnLoad: true, + callback: function (url) { + ctx.py.call('main.import_opml', [url]); + } + }); + }, + }, { label: 'Discover new podcasts', callback: function () {