diff --git a/common/constants.js b/common/constants.js index 21e9832..52ae8b3 100644 --- a/common/constants.js +++ b/common/constants.js @@ -32,11 +32,17 @@ var colors = { destructive: '#cf424f', /* destructive actions */ page: '#dddddd', + toolbar: '#d0d0d0', + dialog: '#dddddd', dialogBackground: '#aa000000', text: '#333333', /* text color */ + dialogText: '#333333', highlight: '#433b67', + dialogHighlight: '#433b67', secondaryHighlight: '#605885', area: '#cccccc', + dialogArea: '#d0d0d0', + toolbarArea: '#bbbbbb', placeholder: '#666666', //page: '#000000', diff --git a/touch/ButtonArea.qml b/touch/ButtonArea.qml index b1d5e7c..2f05a01 100644 --- a/touch/ButtonArea.qml +++ b/touch/ButtonArea.qml @@ -26,6 +26,7 @@ MouseArea { id: mouseArea property bool transparent: false property bool canHighlight: true + property alias color: background.color Rectangle { id: background diff --git a/touch/Dialog.qml b/touch/Dialog.qml index a13eff3..e0ff000 100644 --- a/touch/Dialog.qml +++ b/touch/Dialog.qml @@ -24,6 +24,8 @@ import 'common/constants.js' as Constants Rectangle { id: page + z: 200 + color: Constants.colors.dialogBackground Component.onCompleted: pgst.dialogsVisible = pgst.dialogsVisible + 1; @@ -57,7 +59,7 @@ Rectangle { 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 + color: Constants.colors.dialog clip: true } } diff --git a/touch/EpisodeDetail.qml b/touch/EpisodeDetail.qml index 3145fc4..1f7f7d8 100644 --- a/touch/EpisodeDetail.qml +++ b/touch/EpisodeDetail.qml @@ -31,6 +31,11 @@ SlidePage { property string link property bool ready: false + hasMenuButton: detailPage.link != '' + menuButtonIcon: Icons.link + menuButtonLabel: 'Website' + onMenuButtonClicked: Qt.openUrlExternally(detailPage.link) + PBusyIndicator { anchors.centerIn: parent visible: !detailPage.ready @@ -59,12 +64,7 @@ SlidePage { width: detailPage.width spacing: 10 * pgst.scalef - SlidePageHeader { - title: 'Shownotes' - icon: (detailPage.link != '') ? Icons.link : '' - iconText: 'Website' - onIconClicked: Qt.openUrlExternally(detailPage.link); - } + Item { height: 20 * pgst.scalef; width: parent.width } Column { width: parent.width - 2 * 30 * pgst.scalef diff --git a/touch/EpisodeQueryPage.qml b/touch/EpisodeQueryPage.qml index 34c5c1c..b6985d0 100644 --- a/touch/EpisodeQueryPage.qml +++ b/touch/EpisodeQueryPage.qml @@ -28,6 +28,11 @@ import 'icons/icons.js' as Icons SlidePage { id: allEpisodesPage + hasMenuButton: true + menuButtonIcon: Icons.magnifying_glass + menuButtonLabel: 'Filter' + onMenuButtonClicked: queryControl.showSelectionDialog() + EpisodeQueryControl { id: queryControl model: episodesListModel @@ -48,9 +53,6 @@ SlidePage { id: episodeList property int selectedIndex: -1 title: 'Episodes' - headerIcon: Icons.magnifying_glass - headerIconText: 'Filter' - onHeaderIconClicked: queryControl.showSelectionDialog(); PPlaceholder { text: 'No episodes found' diff --git a/touch/EpisodesPage.qml b/touch/EpisodesPage.qml index f4e0ddb..299a746 100644 --- a/touch/EpisodesPage.qml +++ b/touch/EpisodesPage.qml @@ -34,6 +34,36 @@ SlidePage { width: parent.width height: parent.height + hasMenuButton: true + menuButtonLabel: 'Settings' + onMenuButtonClicked: { + pgst.showSelection([ + { + label: 'Filter list (' + queryControl.currentFilter + ')', + callback: function () { + queryControl.showSelectionDialog(); + } + }, + { + label: 'Mark episodes as old', + callback: function () { + py.call('main.mark_episodes_as_old', [episodesPage.podcast_id]); + }, + }, + { + label: 'Unsubscribe', + callback: function () { + var ctx = { py: py, id: episodesPage.podcast_id, page: episodesPage }; + pgst.showConfirmation('Unsubscribe', Icons.trash, function () { + ctx.py.call('main.unsubscribe', [ctx.id]); + ctx.page.closePage(); + }); + }, + }, + ]); + } + + Component.onCompleted: { episodeListModel.podcast_id = podcast_id; episodeListModel.setQuery(episodeListModel.queries.All); @@ -52,35 +82,6 @@ SlidePage { title: episodesPage.title model: GPodderEpisodeListModel { id: episodeListModel } - headerIcon: Icons.cog - headerIconText: 'Settings' - onHeaderIconClicked: { - pgst.showSelection([ - { - label: 'Filter list (' + queryControl.currentFilter + ')', - callback: function () { - queryControl.showSelectionDialog(); - } - }, - { - label: 'Mark episodes as old', - callback: function () { - py.call('main.mark_episodes_as_old', [episodesPage.podcast_id]); - }, - }, - { - label: 'Unsubscribe', - callback: function () { - var ctx = { py: py, id: episodesPage.podcast_id, page: episodesPage }; - pgst.showConfirmation('Unsubscribe', Icons.trash, function () { - ctx.py.call('main.unsubscribe', [ctx.id]); - ctx.page.closePage(); - }); - }, - }, - ]); - } - PPlaceholder { text: 'No episodes' visible: episodeList.count === 0 diff --git a/touch/IconMenuItem.qml b/touch/IconMenuItem.qml index 474f6c1..28919c7 100644 --- a/touch/IconMenuItem.qml +++ b/touch/IconMenuItem.qml @@ -32,6 +32,8 @@ ButtonArea { property alias size: icon.size property bool alwaysShowText: false + Behavior on _real_color { ColorAnimation { duration: 100 } } + transparent: true canHighlight: false diff --git a/touch/Main.qml b/touch/Main.qml index 4355af4..099f6ac 100644 --- a/touch/Main.qml +++ b/touch/Main.qml @@ -51,10 +51,36 @@ Item { } else { children[index-1].opacity = x / width; } + //children[index-1].pushPhase = x / width; } property bool loadPageInProgress: false + property bool hasBackButton: false + property int bottomSpacing: toolbar.showing ? toolbar.height+toolbar.anchors.bottomMargin : 0 + + property bool hasMenuButton: false + property string menuButtonLabel: '' + property string menuButtonIcon: '' + + function topOfStackChanged(offset) { + if (offset === undefined) { + offset = 0; + } + + var page = children[children.length+offset-1]; + + // TODO: Maybe make these property bindings instead + pgst.hasBackButton = (!page.isDialog && page.canClose); + pgst.hasMenuButton = page.hasMenuButton; + if (pgst.hasMenuButton) { + pgst.menuButtonLabel = page.menuButtonLabel; + pgst.menuButtonIcon = page.menuButtonIcon; + } else { + pgst.menuButtonLabel = 'Menu'; + pgst.menuButtonIcon = Icons.vellipsis; + } + } function showConfirmation(title, icon, callback) { loadPage('Confirmation.qml', { @@ -106,29 +132,67 @@ Item { visible: !py.ready } - IconMenuItem { - id: throbber - - z: 100 - + Image { + z: 101 anchors { + left: parent.left right: parent.right - rightMargin: (opacity-1) * width - top: parent.top - topMargin: (Constants.layout.header.height * pgst.scalef - height) / 2 + bottom: toolbar.top } - text: 'Now Playing' - color: Constants.colors.playback - icon: Icons.play + source: 'images/toolbarshadow.png' + opacity: .1 + height: 10 * pgst.scalef + visible: toolbar.showing + } - transparent: false - enabled: opacity + PToolbar { + id: toolbar + z: 102 - opacity: player.episode != 0 && !pgst.dialogsVisible - Behavior on opacity { PropertyAnimation { duration: 200 } } + Row { + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + } - onClicked: loadPage('PlayerPage.qml'); + PToolbarButton { + id: backButton + + text: 'Back' + icon: Icons.arrow_left + + enabled: pgst.hasBackButton + onClicked: pgst.children[pgst.children.length-1].closePage(); + } + } + + Row { + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + } + + PToolbarButton { + id: throbber + + text: 'Now Playing' + icon: Icons.play + + enabled: player.episode != 0 + onClicked: loadPage('PlayerPage.qml'); + } + + PToolbarButton { + id: menuButton + + text: pgst.menuButtonLabel + icon: pgst.menuButtonIcon + + enabled: pgst.hasMenuButton + onClicked: pgst.children[pgst.children.length-1].menuButtonClicked() + } + } } PodcastsPage { diff --git a/touch/PListView.qml b/touch/PListView.qml index 06c84ed..7f077d6 100644 --- a/touch/PListView.qml +++ b/touch/PListView.qml @@ -27,19 +27,12 @@ ListView { property string title property real pushPhase: 0 - property string headerIcon - property string headerIconText - - signal headerIconClicked() boundsBehavior: Flickable.StopAtBounds header: SlidePageHeader { id: header title: pListView.title - icon: pListView.headerIcon - iconText: pListView.headerIconText - onIconClicked: pListView.headerIconClicked() } PScrollDecorator { flickable: pListView } diff --git a/touch/PToolbar.qml b/touch/PToolbar.qml new file mode 100644 index 0000000..ec64be0 --- /dev/null +++ b/touch/PToolbar.qml @@ -0,0 +1,48 @@ + +/** + * + * 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 + +Rectangle { + id: toolbar + property bool showing: true + + color: Constants.colors.toolbar + + height: 80 * pgst.scalef + + MouseArea { + // Capture all touch events + anchors.fill: parent + } + + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + bottomMargin: toolbar.showing ? 0 : -height + } + + Behavior on anchors.bottomMargin { PropertyAnimation { duration: 100 } } +} diff --git a/touch/PToolbarButton.qml b/touch/PToolbarButton.qml new file mode 100644 index 0000000..839072a --- /dev/null +++ b/touch/PToolbarButton.qml @@ -0,0 +1,59 @@ + +/** + * + * 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 + +Rectangle { + id: toolbarButton + + //property alias color: iconMenuItem.color + //property alias text: iconMenuItem.text + property string text: '' + property alias icon: iconMenuItem.icon + + signal clicked() + + width: iconMenuItem.width + height: iconMenuItem.height + color: iconMenuItem.pressed ? Constants.colors.toolbarArea : 'transparent' + + Rectangle { + height: 5 * pgst.scalef + color: iconMenuItem.pressed ? Constants.colors.secondaryHighlight : 'transparent' + + anchors { + left: parent.left + right: parent.right + top: parent.top + } + } + + IconMenuItem { + id: iconMenuItem + color: Constants.colors.text + transparent: true + enabled: parent.enabled + onClicked: toolbarButton.clicked() + } +} diff --git a/touch/PlayerPage.qml b/touch/PlayerPage.qml index a1870fe..6d51364 100644 --- a/touch/PlayerPage.qml +++ b/touch/PlayerPage.qml @@ -63,6 +63,7 @@ Dialog { } text: player.episode_title elide: Text.ElideRight + color: Constants.colors.dialogText } PLabel { @@ -72,12 +73,14 @@ Dialog { } text: player.podcast_title elide: Text.ElideRight + color: Constants.colors.dialogText } } PLabel { anchors.horizontalCenter: parent.horizontalCenter text: Util.formatPosition(slider.displayedValue/1000, player.duration/1000) + color: Constants.colors.dialogText } PSlider { diff --git a/touch/PodcastsPage.qml b/touch/PodcastsPage.qml index 916b51a..ca70e8e 100644 --- a/touch/PodcastsPage.qml +++ b/touch/PodcastsPage.qml @@ -29,6 +29,43 @@ SlidePage { id: podcastsPage canClose: false + hasMenuButton: true + menuButtonLabel: 'Settings' + onMenuButtonClicked: { + pgst.showSelection([ + { + label: 'Check for new episodes', + callback: function () { + py.call('main.check_for_episodes'); + } + }, + { + label: 'Filter episodes', + callback: function () { + pgst.loadPage('EpisodeQueryPage.qml'); + } + }, + { + label: 'About', + callback: function () { + pgst.loadPage('AboutPage.qml'); + }, + }, + { + label: 'Add new podcast', + callback: function () { + pgst.loadPage('Subscribe.qml'); + }, + }, + { + label: 'Search gpodder.net', + callback: function () { + pgst.loadPage('Directory.qml'); + }, + }, + ]); + } + PListView { id: podcastList title: 'Subscriptions' @@ -36,43 +73,6 @@ SlidePage { section.property: 'section' section.delegate: SectionHeader { text: section } - headerIcon: Icons.cog - headerIconText: 'Settings' - onHeaderIconClicked: { - pgst.showSelection([ - { - label: 'Check for new episodes', - callback: function () { - py.call('main.check_for_episodes'); - } - }, - { - label: 'Filter episodes', - callback: function () { - pgst.loadPage('EpisodeQueryPage.qml'); - } - }, - { - label: 'About', - callback: function () { - pgst.loadPage('AboutPage.qml'); - }, - }, - { - label: 'Add new podcast', - callback: function () { - pgst.loadPage('Subscribe.qml'); - }, - }, - { - label: 'Search gpodder.net', - callback: function () { - pgst.loadPage('Directory.qml'); - }, - }, - ]); - } - PPlaceholder { text: 'No podcasts' visible: podcastList.count === 0 diff --git a/touch/SelectionDialog.qml b/touch/SelectionDialog.qml index b023594..a59a973 100644 --- a/touch/SelectionDialog.qml +++ b/touch/SelectionDialog.qml @@ -46,7 +46,7 @@ Dialog { SlidePageHeader { id: header visible: title != '' - color: Constants.colors.highlight + color: Constants.colors.dialogHighlight title: selectionDialog.title } @@ -56,8 +56,9 @@ Dialog { delegate: ButtonArea { id: buttonArea + color: Constants.colors.dialogArea width: parent.width - height: 60 * pgst.scalef + height: 70 * pgst.scalef transparent: (index != selectionDialog.selectedIndex) @@ -69,7 +70,8 @@ Dialog { } text: modelData - color: (index == selectionDialog.selectedIndex || buttonArea.pressed) ? Constants.colors.highlight : Constants.colors.text + color: (index == selectionDialog.selectedIndex || buttonArea.pressed) ? Constants.colors.dialogHighlight : Constants.colors.dialogText + font.pixelSize: 30 * pgst.scalef } onClicked: { diff --git a/touch/SlidePage.qml b/touch/SlidePage.qml index 186c654..ae945ef 100644 --- a/touch/SlidePage.qml +++ b/touch/SlidePage.qml @@ -21,15 +21,23 @@ import QtQuick 2.0 import 'common/constants.js' as Constants +import 'icons/icons.js' as Icons Rectangle { id: page color: Constants.colors.page + Component.onCompleted: pgst.topOfStackChanged(); + default property alias children: dragging.children property alias canClose: dragging.canClose property bool isDialog: false + property bool hasMenuButton: false + property string menuButtonLabel: 'Menu' + property string menuButtonIcon: Icons.vellipsis + signal menuButtonClicked() + function closePage() { stacking.startFadeOut(); } @@ -37,7 +45,7 @@ Rectangle { onXChanged: pgst.update(page, x) width: parent.width - height: parent.height + height: parent.height - parent.bottomSpacing Stacking { id: stacking } diff --git a/touch/SlidePageHeader.qml b/touch/SlidePageHeader.qml index 46d60df..b92b8de 100644 --- a/touch/SlidePageHeader.qml +++ b/touch/SlidePageHeader.qml @@ -28,45 +28,21 @@ Item { property alias title: label.text property alias color: label.color - property alias iconText: icon.text - property alias icon: icon.icon - signal iconClicked() - width: parent.width height: Constants.layout.header.height * pgst.scalef - IconMenuItem { - id: icon - - visible: icon != '' && icon != undefined - enabled: visible - - text: 'Search' - icon: '' - color: label.color - - anchors { - left: parent.left - verticalCenter: parent.verticalCenter - } - - onClicked: slidePageHeader.iconClicked() - } - PLabel { id: label anchors { - left: icon.visible ? icon.right : parent.left + left: parent.left right: parent.right - rightMargin: 20 * pgst.scalef + (throbber.width * throbber.opacity) + rightMargin: 20 * pgst.scalef leftMargin: 20 * pgst.scalef verticalCenter: parent.verticalCenter } color: Constants.colors.highlight - horizontalAlignment: Text.AlignRight font.pixelSize: parent.height * .4 elide: Text.ElideRight } } - diff --git a/touch/Stacking.qml b/touch/Stacking.qml index bc674db..5523552 100644 --- a/touch/Stacking.qml +++ b/touch/Stacking.qml @@ -48,6 +48,7 @@ Item { function startFadeOut() { fadeOut.start(); + pgst.topOfStackChanged(-1); page.destroy(500); } diff --git a/touch/icons/icons.js b/touch/icons/icons.js index 1bb3281..e9e9f97 100644 --- a/touch/icons/icons.js +++ b/touch/icons/icons.js @@ -19,3 +19,4 @@ var folder = '\ue065'; var magnifying_glass = '\ue074'; var cog = '\u2699'; var link = '\ue077'; +var vellipsis = '\u22ee'; diff --git a/touch/images/toolbarshadow.png b/touch/images/toolbarshadow.png new file mode 100644 index 0000000..0401434 Binary files /dev/null and b/touch/images/toolbarshadow.png differ