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