QML UI: Implement multi-episode selection (bug 1594)
[gpodder.git] / share / gpodder / ui / qml / Main.qml
blob568c61720955a2260a631594200d7fd4ccd7293f
2 import Qt 4.7
3 import com.nokia.meego 1.0
5 import 'config.js' as Config
7 Image {
8     id: main
9     focus: true
11     function _(x) {
12         return controller.translate(x)
13     }
15     function n_(x, y, z) {
16         return controller.ntranslate(x, y, z)
17     }
19     property alias podcastModel: podcastList.model
20     property alias episodeModel: episodeList.model
21     property alias currentEpisode: mediaPlayer.episode
22     property variant currentPodcast: undefined
23     property bool hasPodcasts: podcastList.hasItems
24     property alias currentFilterText: episodeList.currentFilterText
26     property bool playing: mediaPlayer.playing
27     property bool canGoBack: (closeButton.isRequired || mediaPlayer.visible) && !progressIndicator.opacity
28     property bool hasPlayButton: nowPlayingThrobber.shouldAppear && !progressIndicator.opacity
29     property bool hasSearchButton: searchButton.visible && !mediaPlayer.visible && !progressIndicator.opacity
30     property bool hasFilterButton: state == 'episodes' && !mediaPlayer.visible
32     function goBack() {
33         if (nowPlayingThrobber.opened) {
34             nowPlayingThrobber.opened = false
35         } else {
36             closeButton.clicked()
37         }
38     }
40     function showFilterDialog() {
41         episodeList.showFilterDialog()
42     }
44     function clickPlayButton() {
45         nowPlayingThrobber.clicked()
46     }
48     function showMultiEpisodesSheet(label, action) {
49         multiEpisodesSheet.acceptButtonText = label;
50         multiEpisodesSheet.action = action;
51         multiEpisodesList.selected = [];
52         multiEpisodesSheet.open();
53     }
55     function clickSearchButton() {
56         searchButton.clicked()
57     }
59     Keys.onPressed: {
60         console.log(event.key)
61         if (event.key == Qt.Key_Escape) {
62             goBack()
63         }
64         if (event.key == Qt.Key_F && event.modifiers & Qt.ControlModifier) {
65             searchButton.clicked()
66         }
67     }
69     width: 800
70     height: 480
72     property bool useEmptyBackground: !podcastList.hasItems
74     anchors.topMargin: useEmptyBackground?-35:0
75     fillMode: useEmptyBackground?Image.Tile:Image.Stretch
76     source: {
77         if (useEmptyBackground) {
78             '/usr/share/themes/blanco/meegotouch/images/backgrounds/meegotouch-empty-application-background-black-portrait.png'
79         } else {
80             'artwork/background-harmattan.png'
81         }
82     }
84     state: 'podcasts'
86     function enqueueEpisode(episode) {
87         if (currentEpisode === undefined) {
88             togglePlayback(episode);
89         } else {
90             mediaPlayer.enqueueEpisode(episode);
91         }
92     }
94     function removeQueuedEpisodesForPodcast(podcast) {
95         mediaPlayer.removeQueuedEpisodesForPodcast(podcast);
96     }
98     function removeQueuedEpisode(episode) {
99         mediaPlayer.removeQueuedEpisode(episode);
100     }
102     function togglePlayback(episode) {
103         if (episode !== undefined && episode.qfiletype == 'video') {
104             controller.playVideo(episode)
105         } else {
106             mediaPlayer.togglePlayback(episode)
107         }
108     }
110     function openShowNotes(episode) {
111         showNotes.episode = episode
112         main.state = 'shownotes'
113     }
115     function openContextMenu(items) {
116         hrmtnContextMenu.items = items
117         hrmtnContextMenu.open()
118     }
120     function startProgress(text) {
121         progressIndicator.text = text
122         progressIndicator.opacity = 1
123     }
125     function endProgress() {
126         progressIndicator.opacity = 0
127     }
129     states: [
130         State {
131             name: 'podcasts'
132             PropertyChanges {
133                 target: podcastList
134                 opacity: 1
135             }
136             PropertyChanges {
137                 target: episodeList
138                 anchors.leftMargin: 100
139                 opacity: 0
140             }
141             PropertyChanges {
142                 target: showNotes
143                 opacity: 0
144             }
145             StateChangeScript {
146                 script: episodeList.resetSelection()
147             }
148         },
149         State {
150             name: 'episodes'
151             PropertyChanges {
152                 target: episodeList
153                 opacity: 1
154             }
155             PropertyChanges {
156                 target: podcastList
157                 opacity: 0
158                 anchors.leftMargin: -100
159             }
160             PropertyChanges {
161                 target: showNotes
162                 opacity: 0
163                 anchors.leftMargin: main.width
164             }
165         },
166         State {
167             name: 'shownotes'
168             PropertyChanges {
169                 target: listContainer
170                 opacity: 0
171             }
172             PropertyChanges {
173                 target: showNotes
174                 opacity: 1
175                 anchors.leftMargin: 0
176             }
177         }
178     ]
180     Item {
181         id: listContainer
182         anchors.fill: parent
183         anchors.topMargin: titleBar.height
185         PodcastList {
186             id: podcastList
187             opacity: 0
189             anchors.fill: parent
191             onPodcastSelected: {
192                 controller.podcastSelected(podcast)
193                 main.currentPodcast = podcast
194             }
195             onPodcastContextMenu: controller.podcastContextMenu(podcast)
196             onSubscribe: contextMenu.showSubscribe()
198             Behavior on opacity { NumberAnimation { duration: Config.slowTransition } }
199             Behavior on anchors.leftMargin { NumberAnimation { duration: Config.slowTransition } }
200         }
202         EpisodeList {
203             id: episodeList
205             opacity: 0
207             anchors.fill: parent
209             onEpisodeContextMenu: controller.episodeContextMenu(episode)
211             Behavior on opacity { NumberAnimation { duration: Config.slowTransition } }
212             Behavior on anchors.leftMargin { NumberAnimation { duration: Config.slowTransition } }
213         }
215         Behavior on opacity { NumberAnimation { duration: Config.slowTransition } }
216         Behavior on scale { NumberAnimation { duration: Config.fadeTransition } }
217     }
219     ShowNotes {
220         id: showNotes
222         anchors {
223             left: parent.left
224             top: titleBar.bottom
225             bottom: parent.bottom
226         }
227         width: parent.width
229         Behavior on opacity { NumberAnimation { duration: Config.slowTransition } }
230         Behavior on anchors.leftMargin { NumberAnimation { duration: Config.slowTransition } }
231     }
233     Item {
234         id: overlayInteractionBlockWall
235         anchors.fill: parent
236         anchors.topMargin: (nowPlayingThrobber.opened || messageDialog.opacity > 0 || inputDialog.opacity > 0 || progressIndicator.opacity > 0)?0:titleBar.height
237         z: (contextMenu.state != 'opened')?2:0
239         opacity: (nowPlayingThrobber.opened || contextMenu.state == 'opened' || messageDialog.opacity || inputDialog.opacity || progressIndicator.opacity)?1:0
240         Behavior on opacity { NumberAnimation { duration: Config.slowTransition } }
242         MouseArea {
243             anchors.fill: parent
244             onClicked: {
245                 if (contextMenu.state == 'opened') {
246                     // do nothing
247                 } else if (progressIndicator.opacity) {
248                     // do nothing
249                 } else if (inputDialog.opacity) {
250                     inputDialog.close()
251                 } else if (messageDialog.opacity) {
252                     messageDialog.opacity = 0
253                 } else {
254                     nowPlayingThrobber.opened = false
255                 }
256             }
257         }
259         Rectangle {
260             anchors.fill: parent
261             color: 'black'
262             opacity: .7
263         }
265         Image {
266             anchors.fill: parent
267             source: 'artwork/mask.png'
268         }
269     }
271     CornerButton {
272         id: extraCloseButton
273         visible: false
274         z: (contextMenu.state == 'opened')?2:0
275         tab: 'artwork/back-tab.png'
276         icon: 'artwork/back.png'
277         isLeftCorner: true
278         anchors.bottom: parent.bottom
279         anchors.left: parent.left
280         onClicked: closeButton.clicked()
281         opened: !(!Config.hasCloseButton && closeButton.isRequired)
282     }
284     CornerButton {
285         z: 3
287         property bool shouldAppear: ((contextMenu.state != 'opened') && (mediaPlayer.episode !== undefined))
289         id: nowPlayingThrobber
290         visible: false
291         anchors.bottom: parent.bottom
292         anchors.right: parent.right
293         opacity: shouldAppear
295         caption: (mediaPlayer.episode!=undefined)?mediaPlayer.episode.qtitle:''
297         opened: false
298         onClicked: { opened = !opened }
299     }
301     MediaPlayer {
302         id: mediaPlayer
303         visible: nowPlayingThrobber.opened
305         z: 3
307         anchors.top: parent.bottom
308         anchors.left: parent.left
309         anchors.right: parent.right
310         anchors.topMargin: nowPlayingThrobber.opened?-(height+(parent.height-height)/2):0
312         Behavior on anchors.topMargin { PropertyAnimation { duration: Config.quickTransition; easing.type: Easing.OutCirc } }
313     }
315     ContextMenu {
316         id: hrmtnContextMenu
317         property variant items: []
319         MenuLayout {
320             Repeater {
321                 model: hrmtnContextMenu.items
323                 MenuItem {
324                     text: modelData.caption
325                     onClicked: {
326                         hrmtnContextMenu.close()
327                         controller.contextMenuResponse(index)
328                     }
329                 }
330             }
331         }
332     }
334     ContextMenuArea {
335         id: contextMenu
337         width: parent.width
338         opacity: 0
340         anchors {
341             top: parent.top
342             bottom: parent.bottom
343         }
345         onClose: contextMenu.state = 'closed'
346         onResponse: controller.contextMenuResponse(index)
348         state: 'closed'
350         Behavior on opacity { NumberAnimation { duration: Config.fadeTransition } }
352         states: [
353             State {
354                 name: 'opened'
355                 PropertyChanges {
356                     target: contextMenu
357                     opacity: 1
358                 }
359                 AnchorChanges {
360                     target: contextMenu
361                     anchors.right: main.right
362                 }
363             },
364             State {
365                 name: 'closed'
366                 PropertyChanges {
367                     target: contextMenu
368                     opacity: 0
369                 }
370                 AnchorChanges {
371                     target: contextMenu
372                     anchors.right: main.left
373                 }
374                 StateChangeScript {
375                     script: controller.contextMenuClosed()
376                 }
377             }
378         ]
380         transitions: Transition {
381             AnchorAnimation { duration: Config.slowTransition }
382         }
383     }
385     Item {
386         id: titleBar
387         visible: podcastList.hasItems
388         height: visible?taskSwitcher.height*.8:0
389         anchors.left: parent.left
390         anchors.right: parent.right
391         anchors.top: parent.top
393         //anchors.topMargin: mediaPlayer.fullscreen?-height:0
394         //opacity: mediaPlayer.fullscreen?0:1
396         Behavior on opacity { PropertyAnimation { } }
397         Behavior on anchors.topMargin { PropertyAnimation { } }
399         Rectangle {
400             anchors.fill: parent
401             color: "black"
402             opacity: .9
404             MouseArea {
405                 // clicks should not fall through!
406                 anchors.fill: parent
407             }
408         }
410         Item {
411             id: taskSwitcher
412             visible: contextMenu.state != 'opened' && Config.hasTaskSwitcher
413             anchors.left: parent.left
414             anchors.top: parent.top
415             width: Config.switcherWidth
416             height: Config.headerHeight
418             MouseArea {
419                 anchors.fill: parent
420                 onClicked: controller.switcher()
421             }
423             ScaledIcon {
424                 anchors {
425                     verticalCenter: parent.verticalCenter
426                     left: parent.left
427                     leftMargin: (parent.width * .8 - width) / 2
428                 }
429                 source: 'artwork/switch.png'
430             }
431         }
433         Label {
434             id: titleBarText
435             anchors.verticalCenter: parent.verticalCenter
436             anchors.left: taskSwitcher.visible?taskSwitcher.right:taskSwitcher.left
437             anchors.leftMargin: (contextMenu.state == 'opened')?(Config.largeSpacing):(Config.hasTaskSwitcher?0:Config.largeSpacing)
438             anchors.right: searchButton.visible?searchButton.left:searchButton.right
439             wrapMode: Text.NoWrap
440             clip: true
441             text: (contextMenu.state == 'opened')?(contextMenu.subscribeMode?_('Add a new podcast'):_('Context menu')):((main.state == 'episodes' || main.state == 'shownotes')?(controller.episodeListTitle + ' (' + episodeList.count + ')'):"gPodder")
442             color: 'white'
443             font.pixelSize: parent.height * .5
444             font.bold: false
445         }
447         Binding {
448             target: controller
449             property: 'windowTitle'
450             value: titleBarText.text
451         }
453         TitlebarButton {
454             id: searchButton
455             anchors.right: closeButton.visible?closeButton.left:closeButton.right
457             source: 'artwork/subscriptions.png'
459             onClicked: contextMenu.showSubscribe()
461             visible: (contextMenu.state == 'closed' && main.state == 'podcasts')
462             opacity: 0
463         }
465         TitlebarButton {
466             id: closeButton
467             anchors.right: parent.right
468             property bool isRequired: main.state != 'podcasts' || contextMenu.state != 'closed'
469             visible: extraCloseButton.opened && (Config.hasCloseButton || isRequired)
471             source: (main.state == 'podcasts' && contextMenu.state == 'closed')?'artwork/close.png':'artwork/back.png'
472             rotation: 0
474             onClicked: {
475                 if (contextMenu.state == 'opened') {
476                     contextMenu.state = 'closed'
477                 } else if (main.state == 'podcasts') {
478                     mediaPlayer.stop()
479                     controller.quit()
480                 } else if (main.state == 'episodes') {
481                     main.state = 'podcasts'
482                     main.currentPodcast = undefined
483                 } else if (main.state == 'shownotes') {
484                     main.state = 'episodes'
485                 }
486             }
487         }
488     }
490     function showMessage(message) {
491         messageDialogText.text = message
492         messageDialog.opacity = 1
493     }
495     Item {
496         id: messageDialog
497         anchors.fill: parent
498         opacity: 0
499         z: 20
501         Behavior on opacity { PropertyAnimation { } }
503         Label {
504             id: messageDialogText
505             anchors.centerIn: parent
506             color: 'white'
507             font.pixelSize: 20
508             font.bold: true
509         }
510     }
512     function showInputDialog(message, value, accept, reject, textInput) {
513         inputDialogText.text = message
514         inputDialogField.text = value
515         inputDialogAccept.text = accept
516         inputDialogReject.text = reject
517         inputDialogField.visible = textInput
519         if (textInput) {
520             inputSheet.open()
521         } else {
522             queryDialog.open()
523         }
524     }
526     QueryDialog {
527         id: queryDialog
529         acceptButtonText: inputDialogAccept.text
530         rejectButtonText: inputDialogReject.text
532         message: inputDialogText.text
534         onAccepted: inputDialog.accept()
535         onRejected: inputDialog.close()
536     }
538     Sheet {
539         id: multiEpisodesSheet
540         property string action: 'delete'
541         acceptButtonText: _('Delete')
543         rejectButtonText: _('Cancel')
545         visualParent: rootWindow
547         onAccepted: {
548             controller.multiEpisodeAction(multiEpisodesList.selected, action);
549         }
551         content: Item {
552             anchors.fill: parent
553             ListView {
554                 id: multiEpisodesList
555                 property variant selected: []
557                 anchors.fill: parent
558                 anchors.bottomMargin: Config.largeSpacing
559                 model: episodeList.model
561                 delegate: EpisodeItem {
562                     inSelection: multiEpisodesList.selected.indexOf(index) !== -1
563                     onSelected: {
564                         var newSelection = [];
565                         var found = false;
567                         for (var i=0; i<multiEpisodesList.selected.length; i++) {
568                             var value = multiEpisodesList.selected[i];
569                             if (value === index) {
570                                 found = true;
571                             } else {
572                                 newSelection.push(value);
573                             }
574                         }
576                         if (!found) {
577                             if (multiEpisodesSheet.action === 'delete' && item.qarchive) {
578                                 // cannot delete archived episodes
579                             } else {
580                                 newSelection.push(index);
581                             }
582                         }
584                         multiEpisodesList.selected = newSelection;
585                     }
586                 }
587             }
589             ScrollDecorator { flickableItem: multiEpisodesList }
590         }
591     }
593     Sheet {
594         id: inputSheet
596         acceptButtonText: inputDialogAccept.text
597         rejectButtonText: inputDialogReject.text
599         content: Item {
600             anchors.fill: parent
602             MouseArea {
603                 anchors.fill: parent
604                 onClicked: console.log('caught')
605             }
607             Column {
608                 anchors.fill: parent
609                 anchors.margins: Config.smallSpacing
610                 spacing: Config.smallSpacing
612                 Item {
613                     height: 1
614                     width: parent.width
615                 }
617                 Label {
618                     id: inputDialogText
619                     anchors.margins: Config.smallSpacing
620                     width: parent.width
621                 }
623                 Item {
624                     height: 1
625                     width: parent.width
626                 }
628                 InputField {
629                     id: inputDialogField
630                     width: parent.width
631                     onAccepted: {
632                         inputDialog.accept()
633                         inputSheet.close()
634                     }
635                     actionName: inputDialogAccept.text
636                 }
637             }
638         }
640         onAccepted: inputDialog.accept()
641         onRejected: inputDialog.close()
642     }
644     Item {
645         id: inputDialog
646         anchors.fill: parent
647         opacity: 0
649         function accept() {
650             opacity = 0
651             scale = .5
652             controller.inputDialogResponse(true, inputDialogField.text,
653                                            inputDialogField.visible)
654         }
656         function close() {
657             opacity = 0
658             scale = .5
659             controller.inputDialogResponse(false, inputDialogField.text,
660                                            inputDialogField.visible)
661         }
663         SimpleButton {
664             id: inputDialogReject
665             width: parent.width / 2
666             onClicked: inputDialog.close()
667         }
669         SimpleButton {
670             id: inputDialogAccept
671             width: parent.width / 2
672             onClicked: inputDialog.accept()
673         }
674     }
676     Column {
677         id: progressIndicator
678         property string text: '...'
679         anchors.centerIn: parent
680         opacity: 0
681         spacing: Config.largeSpacing * 2
682         z: 40
684         Behavior on opacity { NumberAnimation { duration: Config.slowTransition } }
686         Label {
687             text: parent.text
688             anchors.horizontalCenter: parent.horizontalCenter
689         }
691         BusyIndicator {
692             anchors.horizontalCenter: parent.horizontalCenter
693             running: parent.opacity > 0
694         }
695     }