1/*
2 * Copyright (C) 2017
3 *      Jean-Luc Barriere <jlbarriere68@gmail.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; version 3.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 */
17
18import QtQuick 2.9
19import QtQuick.Controls 2.2
20import NosonApp 1.0
21import "components"
22import "components/Delegates"
23import "components/Flickables"
24import "components/ListItemActions"
25import "components/Dialog"
26
27
28MusicPage {
29    id: servicePage
30    objectName: "servicePage"
31    isRoot: mediaModel.isRoot
32    multiView: true
33    searchable: true
34
35    property var serviceItem: null
36    property int displayType: 3  // display type for root
37    property int parentDisplayType: 0
38    property bool focusViewIndex: false
39
40    // the model handles search
41    property alias searchableModel: mediaModel
42
43    // used to detect view has updated properties since first load.
44    // - isFavorite
45    property bool taintedView: false
46
47    pageTitle: serviceItem.title
48    pageFlickable: mediaGrid.visible ? mediaGrid : mediaList
49
50    BlurredBackground {
51            id: blurredBackground
52            height: parent.height
53            art: serviceItem.id === "SA_RINCON65031_0" ? "qrc:/images/tunein.png" : serviceItem.icon
54    }
55
56    MediaModel {
57      id: mediaModel
58    }
59
60    function restoreFocusViewIndex() {
61        var idx = mediaModel.viewIndex()
62        if (mediaModel.count <= idx) {
63          mediaModel.asyncLoadMore() // load more !!!
64        } else {
65            focusViewIndex = false;
66            mediaList.positionViewAtIndex(idx, ListView.Center);
67            mediaGrid.positionViewAtIndex(idx, GridView.Center);
68        }
69    }
70
71    Connections {
72        target: mediaModel
73        function onDataUpdated() { mediaModel.asyncLoad() }
74        function onLoaded(succeeded) {
75            if (succeeded) {
76                mediaModel.resetModel()
77                servicePage.displayType = servicePage.parentDisplayType // apply displayType
78                servicePage.taintedView = false // reset
79                if (focusViewIndex) {
80                    // restore index position in view
81                    restoreFocusViewIndex()
82                } else {
83                    mediaList.positionViewAtIndex(0, ListView.Top);
84                    mediaGrid.positionViewAtIndex(0, GridView.Top);
85                }
86
87                if (mediaModel.count > 0) {
88                    if (emptyState.active)
89                        emptyState.active = false;
90                } else {
91                    emptyState.message = qsTr("No items found");
92                    emptyState.active = true;
93                }
94
95            } else {
96                // don't show registration fault
97                if (!mediaModel.isAuthExpired) {
98                    mediaModel.resetModel();
99                    emptyState.message = mediaModel.faultString();
100                    emptyState.active = true;
101                    customdebug("Fault: " + emptyState.message);
102                }
103            }
104        }
105        function onLoadedMore(succeeded) {
106            if (succeeded) {
107                mediaModel.appendModel()
108                if (focusViewIndex) {
109                    // restore index position in view
110                    restoreFocusViewIndex()
111                }
112            } else if (focusViewIndex) {
113                focusViewIndex = false;
114                mediaList.positionViewAtEnd();
115                mediaGrid.positionViewAtEnd();
116            }
117        }
118
119        function onPathChanged() {
120            if (mediaModel.isRoot) {
121                pageTitle = serviceItem.title;
122            } else {
123                var name = mediaModel.pathName();
124                if (name === "SEARCH")
125                    pageTitle = serviceItem.title + " : " + qsTr("Search");
126                else
127                    pageTitle = serviceItem.title + " : " + name;
128            }
129        }
130    }
131
132    onDisplayTypeChanged: {
133        isListView = (displayType === 0 /*Grid*/ || displayType === 3 /*Editorial*/) ? false : true
134    }
135
136    // Overlay to show when no item available
137    Loader {
138        id: emptyState
139        anchors.fill: parent
140        active: false
141        asynchronous: true
142        source: "qrc:/controls2/components/ServiceEmptyState.qml"
143        visible: active
144
145        property string message: ""
146
147        onStatusChanged: {
148            if (emptyState.status === Loader.Ready)
149                item.text = message;
150        }
151    }
152
153    MusicListView {
154        id: mediaList
155        anchors.fill: parent
156        model: mediaModel
157        delegate: MusicListItem {
158            id: listItem
159
160            property bool held: false
161            onPressAndHold: held = true
162            onReleased: held = false
163            onClicked: {
164                clickItem(model)
165            }
166
167            color: listItem.held ? "lightgrey" : "transparent"
168
169            // check favorite on data loaded
170            Connections {
171                target: AllFavoritesModel
172                function onLoaded(succeeded) {
173                    listItem.isFavorite = model.canPlay ? (AllFavoritesModel.findFavorite(model.payload).length > 0) : false
174                }
175            }
176
177            noCover: model.type === 2 ? "qrc:/images/none.png"
178                   : model.canPlay && !model.canQueue ? "qrc:/images/radio.png"
179                   : "qrc:/images/no_cover.png"
180            imageSources: model.art !== "" ? [{art: model.art}]
181                        : model.type === 2 ? [{art: "qrc:/images/none.png"}]
182                        : model.canPlay && !model.canQueue ? [{art: "qrc:/images/radio.png"}]
183                        : [{art: "qrc:/images/no_cover.png"}]
184            description: model.description.length > 0 ? model.description
185                    : model.type === 1 ? model.artist.length > 0 ? model.artist : qsTr("Album")
186                    : model.type === 2 ? qsTr("Artist")
187                    : model.type === 3 ? qsTr("Genre")
188                    : model.type === 4 ? qsTr("Playlist")
189                    : model.type === 5 && model.canQueue ? model.artist.length > 0 ? model.artist : qsTr("Song")
190                    : model.type === 5 ? qsTr("Radio")
191                    : ""
192            onActionPressed: playItem(model)
193            actionVisible: model.canPlay
194            actionIconSource: "qrc:/images/media-preview-start.svg"
195            menuVisible: model.canPlay || model.canQueue
196
197            menuItems: [
198                AddToFavorites {
199                    isFavorite: listItem.isFavorite
200                    enabled: model.canPlay
201                    visible: enabled
202                    description: listItem.description
203                    art: model.art
204
205                    onTriggered: {
206                        servicePage.taintedView = true;
207                    }
208                },
209                //@FIXME add to playlist service item doesn't work
210                AddToPlaylist {
211                    enabled: model.canQueue
212                    visible: enabled
213                },
214                AddToQueue {
215                    enabled: model.canQueue
216                    visible: enabled
217                }
218            ]
219
220            coverSize: units.gu(5)
221
222            column: Column {
223                Label {
224                    id: mediaTitle
225                    color: styleMusic.view.primaryColor
226                    font.pointSize: units.fs("medium")
227                    objectName: "itemtitle"
228                    text: model.title
229                }
230
231                Label {
232                    id: mediaDescription
233                    color: styleMusic.view.secondaryColor
234                    font.pointSize: units.fs("x-small")
235                    text: listItem.description
236                    visible: text !== ""
237                }
238            }
239
240            Component.onCompleted: {
241                listItem.isFavorite = model.canPlay ? (AllFavoritesModel.findFavorite(model.payload).length > 0) : false
242            }
243        }
244
245        opacity: isListView ? 1.0 : 0.0
246        visible: opacity > 0.0
247        Behavior on opacity {
248            NumberAnimation { duration: 250 }
249        }
250
251        onAtYEndChanged: {
252            if (mediaList.atYEnd && mediaModel.totalCount > mediaModel.count) {
253                mediaModel.asyncLoadMore()
254            }
255        }
256    }
257
258    MusicGridView {
259        id: mediaGrid
260        itemWidth: displayType == 3 /*Editorial*/ ? units.gu(12) : units.gu(15)
261        heightOffset: units.gu(9)
262
263        model: mediaModel
264
265        delegate: Card {
266            id: mediaCard
267            height: mediaGrid.cellHeight
268            width: mediaGrid.cellWidth
269            primaryText: model.title
270            secondaryText: model.description.length > 0 ? model.description
271                         : model.type === 1 ? model.artist.length > 0 ? model.artist : qsTr("Album")
272                         : model.type === 2 ? qsTr("Artist")
273                         : model.type === 3 ? qsTr("Genre")
274                         : model.type === 4 ? qsTr("Playlist")
275                         : model.type === 5 && model.canQueue ? model.artist.length > 0 ? model.artist : qsTr("Song")
276                         : model.type === 5 ? qsTr("Radio")
277                         : ""
278
279            // check favorite on data loaded
280            Connections {
281                target: AllFavoritesModel
282                function onLoaded(succeeded) {
283                    isFavorite = model.canPlay ? (AllFavoritesModel.findFavorite(model.payload).length > 0) : false
284                }
285            }
286
287            canPlay: model.canPlay
288
289            overlay: false // item icon could be transparent
290            noCover: model.type === 2 ? "qrc:/images/none.png"
291                   : model.canPlay && !model.canQueue ? "qrc:/images/radio.png"
292                   : "qrc:/images/no_cover.png"
293            coverSources: model.art !== "" ? [{art: model.art}]
294                        : model.type === 2 ? [{art: "qrc:/images/none.png"}]
295                        : model.canPlay && !model.canQueue ? [{art: "qrc:/images/radio.png"}]
296                        : [{art: "qrc:/images/no_cover.png"}]
297
298            onClicked: clickItem(model)
299            onPressAndHold: {
300                if (model.canPlay) {
301                    if (isFavorite && removeFromFavorites(model.payload))
302                        isFavorite = false;
303                    else if (!isFavorite && addItemToFavorites(model, secondaryText, imageSource))
304                        isFavorite = true;
305                    servicePage.taintedView = true;
306                } else {
307                    servicePage.isListView = true
308                }
309            }
310            onPlayClicked: playItem(model)
311
312            Component.onCompleted: {
313                mediaCard.isFavorite = model.canPlay ? (AllFavoritesModel.findFavorite(model.payload).length > 0) : false
314            }
315        }
316
317        opacity: isListView ? 0.0 : 1.0
318        visible: opacity > 0.0
319        Behavior on opacity {
320            NumberAnimation { duration: 250 }
321        }
322
323        onAtYEndChanged: {
324            if (mediaGrid.atYEnd && mediaModel.totalCount > mediaModel.count) {
325                mediaModel.asyncLoadMore()
326            }
327        }
328    }
329
330    Component.onCompleted: {
331        mediaModel.init(Sonos, serviceItem.payload, false)
332        mediaModel.asyncLoad()
333        searchable = (mediaModel.listSearchCategories().length > 0)
334    }
335
336    onGoUpClicked: {
337        // change view depending of parent display type
338        servicePage.parentDisplayType = mediaModel.parentDisplayType();
339        focusViewIndex = true;
340        mediaModel.asyncLoadParent();
341    }
342
343    function clickItem(model) {
344        if (model.isContainer) {
345            servicePage.parentDisplayType = model.displayType;
346            mediaModel.asyncLoadChild(model.id, model.title, servicePage.displayType, model.index);
347        } else {
348            var songModel = {
349                "id": model.id,
350                "payload": model.payload,
351                "title": model.title,
352                "author": model.artist,
353                "album": model.album,
354                "description": model.description
355            };
356            dialogSongInfo.open(songModel, [{art: model.art}],
357                                "", undefined, false,
358                                model.canPlay,
359                                model.canQueue,
360                                model.isContainer
361                                );
362        }
363    }
364
365    function playItem(model) {
366        if (model.canPlay) {
367            if (model.canQueue) {
368                if (model.isContainer)
369                    playAll(model);
370                else
371                    trackClicked(model);
372            } else {
373                radioClicked(model);
374            }
375        }
376    }
377
378    DialogSearchMusic {
379        id: dialogSearch
380        searchableModel: mediaModel
381    }
382
383    onSearchClicked: dialogSearch.open();
384
385    ////////////////////////////////////////////////////////////////////////////
386    ////
387    //// Service registration
388    ////
389
390    Loader {
391        id: registeringService
392        anchors.fill: parent
393        source: "qrc:/controls2/components/ServiceRegistration.qml"
394        active: false
395        visible: active
396    }
397
398    Loader {
399        id: loginService
400        anchors.fill: parent
401        source: "qrc:/controls2/components/ServiceLogin.qml"
402        active: false
403        visible: active
404    }
405
406    Connections {
407        target: mediaModel
408        function onIsAuthExpiredChanged() {
409            var auth;
410            if (mediaModel.isAuthExpired) {
411                if (mediaModel.policyAuth === 1) {
412                    if (!loginService.active) {
413                        // first try with saved login/password
414                        auth = mediaModel.getDeviceAuth();
415                        if (auth['key'].length === 0 || mediaModel.requestSessionId(auth['username'], auth['key']) === 0)
416                            loginService.active = true; // show login registration
417                        else {
418                            // refresh the model
419                            mediaModel.asyncLoad();
420                        }
421                    }
422                } else if (mediaModel.policyAuth === 2 || mediaModel.policyAuth === 3) {
423                    if (registeringService.active)
424                        registeringService.active = false; // restart new registration
425                    else
426                        mediaModel.clearData();
427                    registeringService.active = true;
428                }
429            } else {
430                loginService.active = false;
431                registeringService.active = false;
432                mainView.jobRunning = true; // it will be cleared on load finished
433                // save new incarnation of accounts settings
434                auth = mediaModel.getDeviceAuth();
435                var acls = deserializeACLS(settings.accounts);
436                var _acls = [];
437                for (var i = 0; i < acls.length; ++i) {
438                    if (acls[i].type === auth['type'] && acls[i].sn === auth['serialNum'])
439                        continue;
440                    else
441                        _acls.push(acls[i]);
442                }
443                _acls.push({type: auth['type'], sn: auth['serialNum'], key: auth['key'], token: auth['token'], username: auth['username']});
444                settings.accounts = serializeACLS(_acls);
445                // refresh the model
446                mediaModel.asyncLoad();
447            }
448        }
449    }
450}
451