diff --git a/common/GPodderPlatform.qml b/common/GPodderPlatform.qml new file mode 100644 index 0000000..0c5affc --- /dev/null +++ b/common/GPodderPlatform.qml @@ -0,0 +1,33 @@ + +/** + * + * gPodder QML UI Reference Implementation + * Copyright (c) 2014, 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 + +Item { + property bool emulatingAndroid: false + + property bool android: (typeof(gpodderAndroid) !== 'undefined') || emulatingAndroid + + property bool needsBackButton: !android + property bool toolbarOnTop: android + property bool invertedToolbar: toolbarOnTop + property bool titleInToolbar: toolbarOnTop + property bool floatingPlayButton: android +} diff --git a/common/GPodderPlayback.qml b/common/GPodderPlayback.qml index 9d3238c..80f9262 100644 --- a/common/GPodderPlayback.qml +++ b/common/GPodderPlayback.qml @@ -41,7 +41,7 @@ MediaPlayer { property int playedFrom: 0 property var androidConnections: Connections { - target: (typeof(gpodderAndroid) === 'undefined') ? null : gpodderAndroid + target: platform.android ? gpodderAndroid : null onAudioBecomingNoisy: { if (playbackState === MediaPlayer.PlayingState) { diff --git a/common/constants.js b/common/constants.js index 7d72444..8c7bd12 100644 --- a/common/constants.js +++ b/common/constants.js @@ -36,8 +36,17 @@ var colors = { playback: '#729fcf', /* playback blue */ destructive: '#cf424f', /* destructive actions */ - page: '#dddddd', toolbar: '#d0d0d0', + toolbarText: '#333333', + toolbarDisabled: '#666666', + + inverted: { + toolbar: '#815c86', + toolbarText: '#ffffff', + toolbarDisabled: '#aaffffff', + }, + + page: '#dddddd', dialog: '#dddddd', dialogBackground: '#aa000000', text: '#333333', /* text color */ diff --git a/touch/AboutPage.qml b/touch/AboutPage.qml index 3c97c3e..fa62f11 100644 --- a/touch/AboutPage.qml +++ b/touch/AboutPage.qml @@ -23,7 +23,7 @@ import QtQuick 2.0 import 'common/constants.js' as Constants SlidePage { - id: aboutPage + id: page Flickable { id: flickable @@ -36,7 +36,7 @@ SlidePage { Column { id: detailColumn - width: aboutPage.width + width: page.width spacing: 15 * pgst.scalef SlidePageHeader { @@ -46,6 +46,8 @@ SlidePage { Column { width: parent.width + Item { height: 10 * pgst.scalef; width: 1 } + PLabel { width: parent.width * .95 font.pixelSize: 30 * pgst.scalef diff --git a/touch/EpisodeDetail.qml b/touch/EpisodeDetail.qml index 9b57f49..799dfe7 100644 --- a/touch/EpisodeDetail.qml +++ b/touch/EpisodeDetail.qml @@ -75,11 +75,10 @@ SlidePage { anchors.horizontalCenter: parent.horizontalCenter spacing: Constants.layout.padding * pgst.scalef - PLabel { - text: detailPage.title + SlidePageHeader { + title: detailPage.title width: parent.width wrapMode: Text.WordWrap - font.pixelSize: 35 * pgst.scalef color: Constants.colors.highlight } diff --git a/touch/EpisodeQueryPage.qml b/touch/EpisodeQueryPage.qml index b3b9ac8..ebd8d01 100644 --- a/touch/EpisodeQueryPage.qml +++ b/touch/EpisodeQueryPage.qml @@ -26,7 +26,7 @@ import 'common/constants.js' as Constants import 'icons/icons.js' as Icons SlidePage { - id: allEpisodesPage + id: page hasMenuButton: true menuButtonIcon: Icons.magnifying_glass diff --git a/touch/EpisodesPage.qml b/touch/EpisodesPage.qml index c96b666..e1631dc 100644 --- a/touch/EpisodesPage.qml +++ b/touch/EpisodesPage.qml @@ -26,7 +26,7 @@ import 'common/constants.js' as Constants import 'icons/icons.js' as Icons SlidePage { - id: episodesPage + id: page property int podcast_id property string title @@ -44,7 +44,7 @@ SlidePage { { label: 'Mark episodes as old', callback: function () { - py.call('main.mark_episodes_as_old', [episodesPage.podcast_id]); + py.call('main.mark_episodes_as_old', [page.podcast_id]); }, }, { @@ -56,7 +56,7 @@ SlidePage { { label: 'Unsubscribe', callback: function () { - var ctx = { py: py, id: episodesPage.podcast_id, page: episodesPage }; + var ctx = { py: py, id: page.podcast_id, page: page }; pgst.showConfirmation(title, 'Unsubscribe', 'Cancel', 'Remove this podcast and all downloaded episodes?', Icons.trash, function () { ctx.py.call('main.unsubscribe', [ctx.id]); ctx.page.closePage(); @@ -80,6 +80,6 @@ SlidePage { EpisodeListView { id: episodeList - title: episodesPage.title + title: page.title } } diff --git a/touch/IconMenuItem.qml b/touch/IconMenuItem.qml index 28919c7..438ee0c 100644 --- a/touch/IconMenuItem.qml +++ b/touch/IconMenuItem.qml @@ -27,7 +27,8 @@ ButtonArea { property alias text: label.text property color color: Constants.colors.secondaryHighlight - property color _real_color: enabled ? color : Constants.colors.placeholder + property color colorDisabled: Constants.colors.placeholder + property color _real_color: enabled ? color : colorDisabled property alias icon: icon.icon property alias size: icon.size property bool alwaysShowText: false diff --git a/touch/Main.qml b/touch/Main.qml index 9bd3624..b5fc357 100644 --- a/touch/Main.qml +++ b/touch/Main.qml @@ -30,6 +30,7 @@ Item { GPodderCore { id: py } GPodderPlayback { id: player } + GPodderPlatform { id: platform } GPodderPodcastListModel { id: podcastListModel } GPodderPodcastListModelConnections {} @@ -98,6 +99,7 @@ Item { property bool hasMenuButton: false property string menuButtonLabel: '' property string menuButtonIcon: '' + property string windowTitle: 'gPodder' function topOfStackChanged(offset) { if (offset === undefined) { @@ -107,9 +109,13 @@ Item { var page = children[children.length+offset-1]; pgst.hasBackButton = Qt.binding(function () { return page.isDialog || page.canClose; }); - pgst.hasMenuButton = Qt.binding(function () { return page.hasMenuButton; }); - pgst.menuButtonLabel = Qt.binding(function () { return pgst.hasMenuButton ? page.menuButtonLabel : 'Menu'; }); - pgst.menuButtonIcon = Qt.binding(function () { return pgst.hasMenuButton ? page.menuButtonIcon : Icons.vellipsis; }); + 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; }); + + if (!page.isDialog) { + pgst.windowTitle = page.title || 'gPodder'; + } } function showConfirmation(title, affirmative, negative, description, icon, callback) { @@ -170,10 +176,11 @@ Item { anchors { left: parent.left right: parent.right - bottom: toolbar.top + top: platform.toolbarOnTop ? toolbar.bottom : undefined + bottom: platform.toolbarOnTop ? undefined : toolbar.top } - source: 'images/toolbarshadow.png' + source: platform.toolbarOnTop ? 'images/toolbarshadow-top.png' : 'images/toolbarshadow.png' opacity: .1 height: 10 * pgst.scalef visible: toolbar.showing @@ -183,7 +190,14 @@ Item { id: toolbar z: 102 + anchors { + top: platform.toolbarOnTop ? parent.top : undefined + bottom: platform.toolbarOnTop ? undefined : parent.bottom + } + Row { + id: toolbarButtonsLeft + anchors { verticalCenter: parent.verticalCenter left: parent.left @@ -195,9 +209,7 @@ Item { text: 'Back' icon: Icons.arrow_left - // Don't show back button on Android (Android has its own) - visible: (typeof(gpodderAndroid) === 'undefined') - + visible: platform.needsBackButton enabled: pgst.hasBackButton onClicked: { if (enabled) { @@ -207,7 +219,22 @@ Item { } } + PToolbarLabel { + visible: platform.titleInToolbar + + anchors { + verticalCenter: parent.verticalCenter + left: toolbarButtonsLeft.right + right: toolbarButtonsRight.left + margins: Constants.layout.padding * pgst.scalef + } + + text: pgst.windowTitle + } + Row { + id: toolbarButtonsRight + anchors { verticalCenter: parent.verticalCenter right: parent.right @@ -218,6 +245,7 @@ Item { text: 'Now Playing' icon: Icons.play + visible: !platform.floatingPlayButton enabled: player.episode != 0 onClicked: loadPage('PlayerPage.qml'); @@ -265,6 +293,45 @@ Item { } } + Rectangle { + z: 190 + color: Constants.colors.playback + visible: platform.floatingPlayButton + + Behavior on opacity { NumberAnimation { } } + opacity: (player.episode != 0) ? (player.isPlaying ? 1 : .5) : 0 + + width: Constants.layout.item.height * 1.1 * pgst.scalef + height: width + radius: height / 2 + + anchors { + right: parent.right + margins: Constants.layout.padding * 2 * pgst.scalef + } + + y: pgst.height - height - anchors.margins + + PIcon { + id: icon + anchors.centerIn: parent + icon: Icons.headphones + size: 60 + color: Constants.colors.inverted.toolbarText + } + + MouseArea { + anchors.fill: parent + onClicked: loadPage('PlayerPage.qml'); + drag { + target: parent + axis: Drag.YAxis + minimumY: pgst.bottomSpacing + parent.anchors.margins + maximumY: pgst.height - parent.height - parent.anchors.margins + } + } + } + PodcastsPage { visible: py.ready } diff --git a/touch/PToolbar.qml b/touch/PToolbar.qml index ec64be0..748cdd6 100644 --- a/touch/PToolbar.qml +++ b/touch/PToolbar.qml @@ -28,7 +28,7 @@ Rectangle { id: toolbar property bool showing: true - color: Constants.colors.toolbar + color: platform.invertedToolbar ? Constants.colors.inverted.toolbar : Constants.colors.toolbar height: 80 * pgst.scalef @@ -40,9 +40,10 @@ Rectangle { anchors { left: parent.left right: parent.right - bottom: parent.bottom - bottomMargin: toolbar.showing ? 0 : -height + topMargin: toolbar.showing ? 0 : -toolbar.height + bottomMargin: toolbar.showing ? 0 : -toolbar.height } Behavior on anchors.bottomMargin { PropertyAnimation { duration: 100 } } + Behavior on anchors.topMargin { PropertyAnimation { duration: 100 } } } diff --git a/touch/PToolbarButton.qml b/touch/PToolbarButton.qml index 839072a..10f5fe0 100644 --- a/touch/PToolbarButton.qml +++ b/touch/PToolbarButton.qml @@ -36,7 +36,7 @@ Rectangle { width: iconMenuItem.width height: iconMenuItem.height - color: iconMenuItem.pressed ? Constants.colors.toolbarArea : 'transparent' + color: iconMenuItem.pressed ? (platform.invertedToolbar ? Constants.colors.toolbarArea : Constants.colors.inverted.toolbarArea) : 'transparent' Rectangle { height: 5 * pgst.scalef @@ -51,7 +51,8 @@ Rectangle { IconMenuItem { id: iconMenuItem - color: Constants.colors.text + color: platform.invertedToolbar ? Constants.colors.inverted.toolbarText : Constants.colors.toolbarText + colorDisabled: platform.invertedToolbar ? Constants.colors.inverted.toolbarDisabled : Constants.colors.toolbarDisabled transparent: true enabled: parent.enabled onClicked: toolbarButton.clicked() diff --git a/touch/PToolbarLabel.qml b/touch/PToolbarLabel.qml new file mode 100644 index 0000000..3d2e76d --- /dev/null +++ b/touch/PToolbarLabel.qml @@ -0,0 +1,84 @@ + +/** + * + * gPodder QML UI Reference Implementation + * Copyright (c) 2014, 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' + +import 'common/constants.js' as Constants +import 'icons/icons.js' as Icons + +Item { + id: toolbarLabel + + property string text: '' + property bool firstLable: false + + onTextChanged: { + state = (state === 'a') ? 'b': 'a'; + if (state === 'a') { + a.text = text; + } else { + b.text = text; + } + } + + states: [ + State { + name: 'a' + PropertyChanges { target: a; opacity: 1; anchors.leftMargin: 0 } + PropertyChanges { target: b; opacity: 0; anchors.leftMargin: 10 * pgst.scalef } + }, + State { + name: 'b' + PropertyChanges { target: a; opacity: 0; anchors.leftMargin: -10 * pgst.scalef } + PropertyChanges { target: b; opacity: 1; anchors.leftMargin: 0 } + } + ] + + PLabel { + id: a + + anchors { + left: parent.left + right: parent.right + verticalCenter: parent.verticalCenter + } + color: platform.invertedToolbar ? Constants.colors.inverted.toolbarText : Constants.colors.toolbarText + elide: Text.ElideRight + + Behavior on anchors.leftMargin { NumberAnimation { } } + Behavior on opacity { NumberAnimation { } } + } + + PLabel { + id: b + + anchors { + left: parent.left + right: parent.right + verticalCenter: parent.verticalCenter + } + color: platform.invertedToolbar ? Constants.colors.inverted.toolbarText : Constants.colors.toolbarText + elide: Text.ElideRight + + Behavior on anchors.leftMargin { NumberAnimation { } } + Behavior on opacity { NumberAnimation { } } + } +} diff --git a/touch/PodcastDetail.qml b/touch/PodcastDetail.qml index e1396b1..a1273a2 100644 --- a/touch/PodcastDetail.qml +++ b/touch/PodcastDetail.qml @@ -25,7 +25,7 @@ import 'common/util.js' as Util import 'icons/icons.js' as Icons SlidePage { - id: podcastDetail + id: page property int podcast_id property string title @@ -43,7 +43,7 @@ SlidePage { { label: 'Visit website', callback: function () { - Qt.openUrlExternally(podcastDetail.link); + Qt.openUrlExternally(page.link); } }, { @@ -51,14 +51,14 @@ SlidePage { callback: function () { pgst.loadPage('TextInputDialog.qml', { placeholderText: 'Feed URL', - text: podcastDetail.url, + text: page.url, }); } }, { label: 'Change section', callback: function () { - var ctx = { py: py, id: podcastDetail.podcast_id }; + var ctx = { py: py, id: page.podcast_id }; pgst.loadPage('TextInputDialog.qml', { buttonText: 'Change section', placeholderText: 'New section', @@ -74,18 +74,18 @@ SlidePage { PBusyIndicator { anchors.centerIn: parent - visible: !podcastDetail.ready + visible: !page.ready } Component.onCompleted: { py.call('main.show_podcast', [podcast_id], function (podcast) { - podcastDetail.title = podcast.title; - podcastDetail.description = podcast.description; - podcastDetail.link = podcast.link; - podcastDetail.section = podcast.section; - podcastDetail.coverart = podcast.coverart; - podcastDetail.url = podcast.url; - podcastDetail.ready = true; + page.title = podcast.title; + page.description = podcast.description; + page.link = podcast.link; + page.section = podcast.section; + page.coverart = podcast.coverart; + page.url = podcast.url; + page.ready = true; }); } @@ -100,7 +100,7 @@ SlidePage { Column { id: detailColumn - width: podcastDetail.width + width: page.width spacing: Constants.layout.padding * pgst.scalef Item { height: Constants.layout.padding * pgst.scalef; width: parent.width } @@ -116,23 +116,22 @@ SlidePage { Image { id: coverImage - source: podcastDetail.coverart + source: page.coverart fillMode: Image.PreserveAspectFit width: parent.width } } - PLabel { - text: podcastDetail.title + SlidePageHeader { + title: page.title width: parent.width wrapMode: Text.WordWrap - font.pixelSize: 35 * pgst.scalef color: Constants.colors.highlight } PLabel { visible: text !== '' - text: podcastDetail.link + text: page.link width: parent.width wrapMode: Text.WordWrap font.pixelSize: 20 * pgst.scalef @@ -140,7 +139,7 @@ SlidePage { } PLabel { - text: 'Section: ' + podcastDetail.section + text: 'Section: ' + page.section width: parent.width wrapMode: Text.WordWrap font.pixelSize: 20 * pgst.scalef @@ -148,7 +147,7 @@ SlidePage { } PLabel { - text: podcastDetail.description + text: page.description width: parent.width font.pixelSize: 30 * pgst.scalef wrapMode: Text.WordWrap diff --git a/touch/PodcastsPage.qml b/touch/PodcastsPage.qml index aa97778..cc48c19 100644 --- a/touch/PodcastsPage.qml +++ b/touch/PodcastsPage.qml @@ -26,7 +26,8 @@ import 'icons/icons.js' as Icons import 'common/constants.js' as Constants SlidePage { - id: podcastsPage + id: page + canClose: false hasMenuButton: true diff --git a/touch/SlidePage.qml b/touch/SlidePage.qml index 8c83cda..182fdf6 100644 --- a/touch/SlidePage.qml +++ b/touch/SlidePage.qml @@ -33,6 +33,7 @@ Rectangle { property alias canClose: dragging.canClose property bool isDialog: false + property string title: '' property bool hasMenuButton: false property string menuButtonLabel: 'Menu' property string menuButtonIcon: Icons.vellipsis @@ -49,6 +50,8 @@ Rectangle { width: parent.width height: parent.height - parent.bottomSpacing + y: platform.toolbarOnTop ? parent.bottomSpacing : 0 + Stacking { id: stacking } Dragging { diff --git a/touch/SlidePageHeader.qml b/touch/SlidePageHeader.qml index b92b8de..a6955b2 100644 --- a/touch/SlidePageHeader.qml +++ b/touch/SlidePageHeader.qml @@ -27,9 +27,20 @@ Item { id: slidePageHeader property alias title: label.text property alias color: label.color + property alias wrapMode: label.wrapMode + property bool isOnSlidePage: (typeof(page) !== 'undefined') ? page : null width: parent.width - height: Constants.layout.header.height * pgst.scalef + + visible: !platform.titleInToolbar || !isOnSlidePage + height: visible ? (Constants.layout.header.height * pgst.scalef) : 0 + + Binding { + target: isOnSlidePage ? page : null + property: 'title' + value: slidePageHeader.title + when: platform.titleInToolbar + } PLabel { id: label diff --git a/touch/icons/icons.js b/touch/icons/icons.js index 780910f..67e331f 100644 --- a/touch/icons/icons.js +++ b/touch/icons/icons.js @@ -22,3 +22,4 @@ var link = '\ue077'; var vellipsis = '\u22ee'; var paperclip = '\ue08a'; var tag_fill = '\ue02b'; +var headphones = '\ue061'; diff --git a/touch/images/toolbarshadow-top.png b/touch/images/toolbarshadow-top.png new file mode 100644 index 0000000..fa03372 Binary files /dev/null and b/touch/images/toolbarshadow-top.png differ