diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e1fb574..bb0a63df 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,7 +25,7 @@ set(QT_IMPORTS_DIR "lib/${ARCH_TRIPLET}") option(INSTALL_TESTS "Install the tests on make install" on) option(CLICK_MODE "Installs to a contained location" on) -set(APP_VERSION 2.4.13) +set(APP_VERSION 2.5.0) set(APP_NAME noson) set(APP_ID "noson.janbar") set(MAIN_QML "Main.qml") diff --git a/app/Main.qml b/app/Main.qml index 750181fd..6945f670 100644 --- a/app/Main.qml +++ b/app/Main.qml @@ -39,6 +39,7 @@ MainView { applicationName: "noson.janbar" id: mainView + focus: true backgroundColor: styleMusic.mainView.backgroundColor Binding { @@ -119,7 +120,7 @@ MainView { property bool wideAspect: width >= units.gu(100) && loadedUI // property to enable pop info on index loaded - property bool infoLoadedIndex: false + property bool infoLoadedIndex: true // enabled at startup // Constants readonly property int queueBatchSize: 100 @@ -129,13 +130,81 @@ MainView { //// Events //// + Connections { + target: Sonos + + onJobCountChanged: jobRunning = Sonos.jobCount > 0 ? true : false + + onInitDone: { + if (succeeded) { + if (noZone) + noZone = false; + } else { + if (!noZone) + noZone = true; + } + } + + onLoadingFinished: { + if (infoLoadedIndex) { + infoLoadedIndex = false; + popInfo.open(i18n.tr("Index loaded")); + } + } + + onTopologyChanged: { + AllZonesModel.asyncLoad() + delayReloadZone.start() + } + } + + Timer { + id: delayReloadZone + interval: 250 + onTriggered: { + if (jobRunning) { + restart(); + } else { + // Reload the zone and start the content loader thread + customdebug("Reloading the zone ..."); + if (connectZone(currentZone)) { + Sonos.runLoader(); + } + } + } + } + // Run on startup Component.onCompleted: { - currentlyWorking = true - if (args.values.debug) { debugLevel = 4 } - delayStartup.start() - + if (args.values.debug) { + mainView.debugLevel = 4 + } customdebug("LANG=" + Qt.locale().name); + Sonos.setLocale(Qt.locale().name); + // initialize all data models + AllZonesModel.init(Sonos, "", false); + AllFavoritesModel.init(Sonos, "", false); + AllServicesModel.init(Sonos, false); + AllAlbumsModel.init(Sonos, "", false); + AllArtistsModel.init(Sonos, "", false); + AllGenresModel.init(Sonos, "", false); + AllRadiosModel.init(Sonos, "R:0/0", false); + AllPlaylistsModel.init(Sonos, "", false); + + // push the page to view + mainPageStack.push(tabs) + + // launch connection + connectSonos(); + + // if a tab index exists restore it, otherwise goto Recent if there are items otherwise go to Albums + tabs.selectedTabIndex = startupSettings.tabIndex === -1 + ? servicesTab.index + : (startupSettings.tabIndex > tabs.count - 1 + ? tabs.count - 1 : startupSettings.tabIndex) + + // signal UI has finished + loadedUI = true; // resize main view according to user settings mainView.width = (startupSettings.width >= units.gu(44) ? startupSettings.width : units.gu(44)); @@ -146,9 +215,13 @@ MainView { customdebug("register account: type=" + acls[i].type + " sn=" + acls[i].sn + " token=" + acls[i].token.substr(0, 1) + "..."); Sonos.addServiceOAuth(acls[i].type, acls[i].sn, acls[i].key, acls[i].token); } + + //@TODO add url to play list + //if (args.values.url) { + //} } - Timer { +/* Timer { id: delayStartup interval: 100 onTriggered: { @@ -171,12 +244,11 @@ MainView { //@TODO add url to play list } } - } + }*/ // Show/hide page NoZoneState onNoZoneChanged: { if (noZone) { - currentlyWorking = false // hide actvity indicator emptyPage = mainPageStack.push(Qt.resolvedUrl("ui/NoZoneState.qml"), {}) } else { mainPageStack.popPage(emptyPage) @@ -186,7 +258,7 @@ MainView { // Refresh player state when application becomes active onApplicationStateChanged: { if (!noZone && applicationState && player.connected) { - mainView.currentlyWorking = true + mainView.jobRunning = true delayPlayerWakeUp.start() } } @@ -201,7 +273,7 @@ MainView { Sonos.renewSubscriptions() noZone = false } - mainView.currentlyWorking = false + mainView.jobRunning = false } } @@ -212,55 +284,57 @@ MainView { Connections { target: AllZonesModel onDataUpdated: AllZonesModel.asyncLoad() + onLoaded: AllZonesModel.resetModel() } Connections { - target: AllAlbumsModel - onDataUpdated: AllAlbumsModel.asyncLoad() + target: AllServicesModel + onDataUpdated: AllServicesModel.asyncLoad() + onLoaded: AllServicesModel.resetModel() } Connections { - target: AllArtistsModel - onDataUpdated: AllArtistsModel.asyncLoad() + target: AllRadiosModel + onDataUpdated: AllRadiosModel.asyncLoad() + onLoaded: AllRadiosModel.resetModel() } Connections { - target: AllGenresModel - onDataUpdated: AllGenresModel.asyncLoad() + target: AllFavoritesModel + onDataUpdated: AllFavoritesModel.asyncLoad() + onLoaded: AllFavoritesModel.resetModel() } Connections { - target: AllRadiosModel - onDataUpdated: AllRadiosModel.asyncLoad() + target: AllArtistsModel + onDataUpdated: AllArtistsModel.asyncLoad() + onLoaded: AllArtistsModel.resetModel() } Connections { - target: AllPlaylistsModel - onDataUpdated: AllPlaylistsModel.asyncLoad() + target: AllAlbumsModel + onDataUpdated: AllAlbumsModel.asyncLoad() + onLoaded: AllAlbumsModel.resetModel() } Connections { - target: AllFavoritesModel - onDataUpdated: AllFavoritesModel.asyncLoad() + target: AllGenresModel + onDataUpdated: AllGenresModel.asyncLoad() + onLoaded: AllGenresModel.resetModel() } Connections { - target: Sonos - onLoadingFinished: { - if (infoLoadedIndex) { - infoLoadedIndex = false; - popInfo.open(i18n.tr("Index loaded")); - } - currentlyWorking = false; // hide actvity indicator - } - - onTopologyChanged: { - reloadZone() - } + target: AllPlaylistsModel + onDataUpdated: AllPlaylistsModel.asyncLoad() + onLoaded: AllPlaylistsModel.resetModel() } - // Global keyboard shortcuts - focus: true + + //////////////////////////////////////////////////////////////////////////// + //// + //// Global keyboard shortcuts + //// + Keys.onPressed: { if(event.key === Qt.Key_Escape) { if (mainPageStack.currentMusicPage.currentDialog !== null) { @@ -366,24 +440,15 @@ MainView { Connections { target: ContentHub onShareRequested: { - delayPlayURL.url = transfer.items[0].url - delayPlayURL.start() - } - } - - Timer { - id: delayPlayURL - interval: 100 - property string url: "" - onTriggered: { - if (!player.playStream(url, "")) + var url = transfer.items[0].url + if (!player.startPlayStream(url, "")) popInfo.open(i18n.tr("Action can't be performed")) else inputStreamUrl = url - mainView.currentlyWorking = false } } + //////////////////////////////////////////////////////////////////////////// //// //// Global actions & helpers @@ -423,48 +488,30 @@ MainView { return acls; } - // Try to connect to SONOS system - // On failure: noZone is set to true + // Try connect to SONOS system function connectSonos() { - if (Sonos.init(debugLevel)) { - Sonos.setLocale(Qt.locale().name); - AllFavoritesModel.init(Sonos, ""); - AllServicesModel.init(Sonos); - AllAlbumsModel.init(Sonos, ""); - AllArtistsModel.init(Sonos, ""); - AllGenresModel.init(Sonos, ""); - AllRadiosModel.init(Sonos, "R:0/0"); - AllPlaylistsModel.init(Sonos, ""); - // enable info on index loaded - infoLoadedIndex = true; - return true; - } - // Signal change if any - if (!noZone) - noZone = true; - return false; + return Sonos.startInit(mainView.debugLevel); } - // Reload zones and try connect - // On success: noZone is set to false and content loader thread is started - // to fill data in global models - function reloadZone() { - AllZonesModel.init(Sonos, true); // force load now - customdebug("Reloading zone ..."); - if ((Sonos.connectZone(currentZone) || Sonos.connectZone("")) && player.connect()) { + signal zoneChanged + + // Try to change zone + // On success noZone is set to false + function connectZone(name) { + var oldZone = currentZone; + customdebug("Connecting zone '" + name + "'"); + if ((Sonos.connectZone(name) || Sonos.connectZone("")) && player.connect()) { currentZone = Sonos.getZoneName(); currentZoneTag = Sonos.getZoneShortName(); - customdebug("Connected zone is '" + currentZone + "'"); - // It is time to fill models - Sonos.runLoader(); - // Signal change if any + if (currentZone !== oldZone) + zoneChanged(); if (noZone) noZone = false; return true; + } else { + if (!noZone) + noZone = true; } - // Signal change if any - if (!noZone) - noZone = true; return false; } @@ -1035,9 +1082,11 @@ MainView { height: status === Loader.Ready ? item.height : 0 } - property alias currentlyWorking: loading.visible + + property bool jobRunning: false LoadingSpinnerComponent { id: loading + visible: jobRunning } } diff --git a/app/components/Dialog/DialogSearchMusic.qml b/app/components/Dialog/DialogSearchMusic.qml index c4c9c96a..39eacd52 100644 --- a/app/components/Dialog/DialogSearchMusic.qml +++ b/app/components/Dialog/DialogSearchMusic.qml @@ -96,7 +96,7 @@ DialogBase { color: styleMusic.dialog.confirmButtonColor onClicked: { if (searchableModel !== null && selector.selectedIndex >= 0 && searchField.text.length) { - searchableModel.loadSearch(selectorModel.get(selector.selectedIndex).id, searchField.text); + searchableModel.asyncLoadSearch(selectorModel.get(selector.selectedIndex).id, searchField.text); } PopupUtils.close(dialogSearchMusic); } diff --git a/app/components/Dialog/DialogSelectSource.qml b/app/components/Dialog/DialogSelectSource.qml index d53b4dc8..8d877b74 100644 --- a/app/components/Dialog/DialogSelectSource.qml +++ b/app/components/Dialog/DialogSelectSource.qml @@ -26,7 +26,7 @@ DialogBase { title: i18n.tr("Select source") Label { - id: urlOutput + id: sourceOutput anchors.left: parent.left anchors.right: parent.right wrapMode: Text.WordWrap @@ -35,6 +35,7 @@ DialogBase { font.weight: Font.Normal visible: false // should only be visible when an error is made. } + TextField { id: url text: inputStreamUrl @@ -42,36 +43,37 @@ DialogBase { inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase color: theme.palette.selected.baseText } + + Connections { + target: player.zonePlayer + onJobFailed: { + sourceOutput.color = UbuntuColors.red + sourceOutput.text = i18n.tr("Playing failed.") + sourceOutput.visible = true + } + } + Button { id: buttonPlayStream text: i18n.tr("Play stream") color: UbuntuColors.green onClicked: { - urlOutput.visible = false // make sure its hidden now if there was an error last time + sourceOutput.visible = false // make sure its hidden now if there was an error last time if (url.text.length > 0) { // make sure something is acually inputed color = UbuntuColors.lightGrey - delayPlayStream.start() - } - else { - urlOutput.visible = true - urlOutput.text = i18n.tr("Please type in an URL.") - } - } - } - - Timer { - id: delayPlayStream - interval: 100 - onTriggered: { - if (player.playStream(url.text, "")) { - inputStreamUrl = url.text - PopupUtils.close(dialogSelectSource) + if (player.playStream(url.text, "")) { + inputStreamUrl = url.text + } + else { + sourceOutput.color = UbuntuColors.red + sourceOutput.text = i18n.tr("Playing failed.") + sourceOutput.visible = true + } + color = UbuntuColors.green } else { - urlOutput.color = UbuntuColors.red - urlOutput.text = i18n.tr("Playing failed.") - urlOutput.visible = true - buttonPlayStream.color = UbuntuColors.green + sourceOutput.visible = true + sourceOutput.text = i18n.tr("Please type in an URL.") } } } diff --git a/app/components/HeadState/AlbumSongsHeadState.qml b/app/components/HeadState/AlbumSongsHeadState.qml index b5c3993b..a4021466 100644 --- a/app/components/HeadState/AlbumSongsHeadState.qml +++ b/app/components/HeadState/AlbumSongsHeadState.qml @@ -44,7 +44,7 @@ State { onTriggered: { if (isFavorite && removeFromFavorites(containerItem.payload)) isFavorite = false - else if (!isFavorite && addItemToFavorites(containerItem, title, albumtrackslist.headerItem.firstSource)) + else if (!isFavorite && addItemToFavorites(containerItem, title, songList.headerItem.firstSource)) isFavorite = true } } diff --git a/app/components/HeadState/PlaylistHeadState.qml b/app/components/HeadState/PlaylistHeadState.qml index c271bd90..6a56b921 100644 --- a/app/components/HeadState/PlaylistHeadState.qml +++ b/app/components/HeadState/PlaylistHeadState.qml @@ -47,7 +47,7 @@ State { onTriggered: { if (isFavorite && removeFromFavorites(containerItem.payload)) isFavorite = false - else if (!isFavorite && addItemToFavorites(containerItem, title, albumtrackslist.headerItem.firstSource)) + else if (!isFavorite && addItemToFavorites(containerItem, title, songList.headerItem.firstSource)) isFavorite = true } }, diff --git a/app/components/HeadState/SearchableHeadState.qml b/app/components/HeadState/SearchableHeadState.qml index 2fd53ce9..71b2f94c 100644 --- a/app/components/HeadState/SearchableHeadState.qml +++ b/app/components/HeadState/SearchableHeadState.qml @@ -50,8 +50,8 @@ State { onTriggered: { thisPage.isListView = !thisPage.isListView if (thisPage.taintedView !== undefined && thisPage.taintedView) { - mainView.currentlyWorking = true; - delayLoadModel.start(); + thisPage.model.asyncLoad(); + thisPage.taintedView = false; // reset } } }, diff --git a/app/components/HeadState/ServiceHeadState.qml b/app/components/HeadState/ServiceHeadState.qml index b13396f4..3f9b7e75 100644 --- a/app/components/HeadState/ServiceHeadState.qml +++ b/app/components/HeadState/ServiceHeadState.qml @@ -47,8 +47,8 @@ State { onTriggered: { thisPage.isListView = !thisPage.isListView; if (thisPage.taintedView) { - mainView.currentlyWorking = true; - delayLoadModel.start(); + thisPage.model.asyncLoad(); + thisPage.taintedView = false; // reset } } }, diff --git a/app/components/Player.qml b/app/components/Player.qml index 2e5f2b8b..5b64094d 100644 --- a/app/components/Player.qml +++ b/app/components/Player.qml @@ -28,6 +28,7 @@ import NosonApp 1.0 Item { id: player objectName: "controller" + property alias zonePlayer: playerLoader.item property bool connected: false property string currentMetaAlbum: "" property string currentMetaArt: "" @@ -72,7 +73,8 @@ Item { if (trackQueue.canLoad) { // When switching zone, updateid cannot drive correctly the queue refreshing // so new force refreshing of queue - return trackQueue.loadQueue(); + trackQueue.loadQueue(); + return true; } return trackQueue.canLoad = true; } @@ -118,8 +120,12 @@ Item { return play(); } + function playSource(modelItem) { + return playerLoader.item.startPlaySource(modelItem.payload); + } + function playSong(modelItem) { - return (setSource(modelItem) && play()); + return playSource(modelItem); } function previousSong(startPlaying) { @@ -260,7 +266,7 @@ Item { } function playStream(url, title) { - return playerLoader.item.playStream(url, (title === "" ? i18n.tr("Untitled") : title)) + return playerLoader.item.startPlayStream(url, (title === "" ? i18n.tr("Untitled") : title)) } function playLineIN() { @@ -280,7 +286,7 @@ Item { } function playFavorite(modelItem) { - return playerLoader.item.playFavorite(modelItem.payload); + return playerLoader.item.startPlayFavorite(modelItem.payload); } property alias renderingModel: renderingModelLoader.item @@ -299,6 +305,7 @@ Item { asynchronous: true sourceComponent: Component { ZonePlayer { + onJobFailed: popInfo.open(i18n.tr("Action can't be performed")); onConnectedChanged: player.connected = connected onRenderingGroupChanged: player.refreshRenderingGroup() onRenderingChanged: player.refreshRendering() diff --git a/app/components/Queue.qml b/app/components/Queue.qml index d52437a9..292e7609 100644 --- a/app/components/Queue.qml +++ b/app/components/Queue.qml @@ -74,7 +74,6 @@ Item { actions: [ Remove { onTriggered: { - mainView.currentlyWorking = true delayRemoveTrackFromQueue.start() } } @@ -105,12 +104,10 @@ Item { interval: 100 onTriggered: { removeTrackFromQueue(model) - mainView.currentlyWorking = false } } onItemClicked: { - mainView.currentlyWorking = true delayIndexQueueClicked.start() } @@ -119,7 +116,6 @@ Item { interval: 100 onTriggered: { indexQueueClicked(index) // toggle track state - mainView.currentlyWorking = false } } } @@ -127,7 +123,6 @@ Item { onReorder: { delayReorderTrackInQueue.argFrom = from delayReorderTrackInQueue.argTo = to - mainView.currentlyWorking = true delayReorderTrackInQueue.start() } @@ -138,7 +133,6 @@ Item { property int argTo: 0 onTriggered: { reorderTrackInQueue(argFrom, argTo) - mainView.currentlyWorking = false } } diff --git a/app/components/ServiceLogin.qml b/app/components/ServiceLogin.qml index ce3e9e0d..d86fb399 100644 --- a/app/components/ServiceLogin.qml +++ b/app/components/ServiceLogin.qml @@ -108,7 +108,7 @@ Rectangle { width: parent.width onClicked: { - mainView.currentlyWorking = true + mainView.jobRunning = true delayLoginService.start() } } @@ -119,7 +119,7 @@ Rectangle { onTriggered: { loginOutput.visible = false; var ret = mediaModel.requestSessionId(username.text, password.text); - mainView.currentlyWorking = false; + mainView.jobRunning = false; if (ret === 0) { customdebug("Service login failed."); loginOutput.text = i18n.tr("Login failed."); diff --git a/app/components/ServiceRegistration.qml b/app/components/ServiceRegistration.qml index d8dd7c33..03e7f924 100644 --- a/app/components/ServiceRegistration.qml +++ b/app/components/ServiceRegistration.qml @@ -94,7 +94,7 @@ Rectangle { width: parent.width onClicked: { - mainView.currentlyWorking = true + mainView.jobRunning = true delayRegisterService.start() } } @@ -110,7 +110,7 @@ Rectangle { regUrl.text = "" + mediaModel.regURL + ""; requestAuthForTime.start(); } - mainView.currentlyWorking = false + mainView.jobRunning = false } } diff --git a/app/components/TrackQueue.qml b/app/components/TrackQueue.qml index 976ceacd..57c48bd1 100644 --- a/app/components/TrackQueue.qml +++ b/app/components/TrackQueue.qml @@ -27,36 +27,31 @@ Item { } function loadQueue() { - if (canLoad) { - if (model.load()) { - player.currentCount = model.count - return completed = true - } - player.currentCount = 0 - return completed = false - } - return false + if (canLoad) + model.asyncLoad() } onCanLoadChanged: { - mainView.currentlyWorking = true - delayLoadQueue.start() - } - - Timer { - id: delayLoadQueue - interval: 100 - onTriggered: { - loadQueue() - mainView.currentlyWorking = false - } + if (canLoad) + model.asyncLoad() } Connections { target: model onDataUpdated: { - mainView.currentlyWorking = true - delayLoadQueue.start() + if (canLoad) + model.asyncLoad() + } + onLoaded: { + if (succeeded) { + model.resetModel() + player.currentCount = model.count + completed = true + } else { + model.resetModel() + player.currentCount = 0 + completed = false + } } } } diff --git a/app/components/ViewButton/PlayAllButton.qml b/app/components/ViewButton/PlayAllButton.qml index 8f0ea175..3578e3ad 100644 --- a/app/components/ViewButton/PlayAllButton.qml +++ b/app/components/ViewButton/PlayAllButton.qml @@ -40,7 +40,6 @@ Button { } onClicked: { - mainView.currentlyWorking = true delayPlayAll.start() } @@ -49,7 +48,6 @@ Button { interval: 100 onTriggered: { playAll(containerItem) - mainView.currentlyWorking = false } } diff --git a/app/components/ViewButton/QueueAllButton.qml b/app/components/ViewButton/QueueAllButton.qml index e4df605f..c95d1040 100644 --- a/app/components/ViewButton/QueueAllButton.qml +++ b/app/components/ViewButton/QueueAllButton.qml @@ -40,7 +40,6 @@ Button { } onClicked: { - mainView.currentlyWorking = true delayAddQueue.start() } @@ -49,7 +48,6 @@ Button { interval: 100 onTriggered: { addQueue(containerItem) - mainView.currentlyWorking = false } } diff --git a/app/components/ViewButton/ShuffleButton.qml b/app/components/ViewButton/ShuffleButton.qml index de7e475f..b2c023b4 100644 --- a/app/components/ViewButton/ShuffleButton.qml +++ b/app/components/ViewButton/ShuffleButton.qml @@ -39,7 +39,6 @@ Button { } onClicked: { - mainView.currentlyWorking = true delayShuffleModel.start() } @@ -48,7 +47,6 @@ Button { interval: 100 onTriggered: { shuffleModel(model) - mainView.currentlyWorking = false } } diff --git a/app/ui/Albums.qml b/app/ui/Albums.qml index 64058c3b..48d48e83 100644 --- a/app/ui/Albums.qml +++ b/app/ui/Albums.qml @@ -108,6 +108,15 @@ MusicPage { "line2": model.title !== undefined && model.title !== "" ? model.title : i18n.tr("Unknown Album") }) } + + // check favorite on data loaded + Connections { + target: AllFavoritesModel + onCountChanged: { + isFavorite = (AllFavoritesModel.findFavorite(model.payload).length > 0) + } + } + onPressAndHold: { if (isFavorite && removeFromFavorites(model.payload)) isFavorite = false diff --git a/app/ui/ArtistView.qml b/app/ui/ArtistView.qml index 7f626688..05bc162e 100644 --- a/app/ui/ArtistView.qml +++ b/app/ui/ArtistView.qml @@ -159,10 +159,17 @@ MusicPage { } } } + model: AlbumsModel { id: albumsModel - Component.onCompleted: init(Sonos, artistSearch, true) + onDataUpdated: albumsModel.asyncLoad() + onLoaded: albumsModel.resetModel() + Component.onCompleted: { + init(Sonos, artistSearch, false) + albumsModel.asyncLoad() + } } + delegate: Card { id: albumCard coverSources: [ @@ -191,24 +198,15 @@ MusicPage { } } + // Query total count of artist's songs TracksModel { id: songArtistModel - } - - Timer { - id: delayInitModel - interval: 100 - onTriggered: { - isFavorite = (AllFavoritesModel.findFavorite(containerItem.payload) !== "") - songArtistModel.init(Sonos, artistSearch + "/", true) - mainView.currentlyWorking = false - loaded = true + onDataUpdated: songArtistModel.asyncLoad() + onLoaded: songArtistModel.resetModel() + Component.onCompleted: { + songArtistModel.init(Sonos, artistSearch + "/", false) + songArtistModel.asyncLoad() } } - - Component.onCompleted: { - mainView.currentlyWorking = true - delayInitModel.start() - } } diff --git a/app/ui/Artists.qml b/app/ui/Artists.qml index 0b99ba82..4b4bfd02 100644 --- a/app/ui/Artists.qml +++ b/app/ui/Artists.qml @@ -102,6 +102,15 @@ MusicPage { "pageTitle": i18n.tr("Artist") }) } + + // check favorite on data loaded + Connections { + target: AllFavoritesModel + onCountChanged: { + isFavorite = (AllFavoritesModel.findFavorite(model.payload).length > 0) + } + } + onPressAndHold: { if (isFavorite && removeFromFavorites(model.payload)) isFavorite = false diff --git a/app/ui/Favorites.qml b/app/ui/Favorites.qml index 40a7b652..e9afdd09 100644 --- a/app/ui/Favorites.qml +++ b/app/ui/Favorites.qml @@ -133,7 +133,6 @@ MusicPage { actions: [ Remove { onTriggered: { - mainView.currentlyWorking = true delayRemoveFavorite.start() } } @@ -184,7 +183,6 @@ MusicPage { onTriggered: { if (!player.removeFavorite(model.id)) popInfo.open(i18n.tr("Action can't be performed")); - mainView.currentlyWorking = false } } @@ -254,7 +252,6 @@ MusicPage { property QtObject model onTriggered: { player.playFavorite(model) // play favorite - mainView.currentlyWorking = false } } @@ -314,12 +311,10 @@ MusicPage { }) } else if (model.type === 5) { - mainView.currentlyWorking = true delayfavoriteClicked.model = model delayfavoriteClicked.start() } } else { - mainView.currentlyWorking = true delayfavoriteClicked.model = model delayfavoriteClicked.start() } diff --git a/app/ui/Genres.qml b/app/ui/Genres.qml index 6c939791..963ecae8 100644 --- a/app/ui/Genres.qml +++ b/app/ui/Genres.qml @@ -84,6 +84,7 @@ MusicPage { var root = modelItem.id + "/"; // register and load directory content for root childModel.init(Sonos, root, true); + childModel.resetModel(); var count = childModel.count; var index = 0; while (index < count && index < 4) { @@ -145,6 +146,15 @@ MusicPage { "line2": model.genre }) } + + // check favorite on data loaded + Connections { + target: AllFavoritesModel + onCountChanged: { + isFavorite = (AllFavoritesModel.findFavorite(model.payload).length > 0) + } + } + onPressAndHold: { if (isFavorite && removeFromFavorites(model.payload)) isFavorite = false diff --git a/app/ui/Group.qml b/app/ui/Group.qml index 4a293db6..4eade268 100644 --- a/app/ui/Group.qml +++ b/app/ui/Group.qml @@ -44,8 +44,8 @@ Page { iconName: "back" objectName: "backAction" onTriggered: { - mainView.currentlyWorking = true - delayHandleGroupChanges.start() + if (handleUnjoinRooms()) + mainPageStack.pop(); } } ] @@ -76,41 +76,33 @@ Page { } } - Timer { - id: delayHandleGroupChanges - interval: 100 - onTriggered: { - var failure = false; - var items = []; - var indicies = groupList.getSelectedIndices(); - for (var i = 0; i < groupList.model.count; ++i) { - var keep = false; - for (var j = 0; j < indicies.length; j++) { - if (indicies[j] === i) { - keep = true; - break; - } - } - if (!keep) { - items.push(groupList.model.get(i)); + function handleUnjoinRooms() { + // keep back unselected rooms + var rooms = []; + var indicies = groupList.getSelectedIndices(); + var keep = false; + for (var i = 0; i < roomsModel.count; ++i) { + keep = false; + for (var j = 0; j < indicies.length; j++) { + if (indicies[j] === i) { + keep = true; + break; } } - if (items.length == 0) { - mainView.currentlyWorking = false; - } - else { - for (var i = 0; i < items.length; ++i) { - if (!Sonos.unjoinRoom(items[i].payload)) { - failure = true; - break; - } - } - // Zones will be reloaded on signal topologyChanged - // Signal is handled in MainView - mainView.currentlyWorking = false + if (!keep) { + rooms.push(roomsModel.get(i).payload); } - mainPageStack.pop() } + // start unjoin rooms + if (rooms.length > 0) { + if (!Sonos.startUnjoinRooms(rooms)) + return false; + } + return true; + } + + RoomsModel { + id: roomsModel } MultiSelectListView { @@ -121,12 +113,10 @@ Page { footer: Item { height: mainView.height - (styleMusic.common.expandHeight + groupList.currentHeight) + units.gu(8) } - model: RoomsModel { - } + model: roomsModel Component.onCompleted: { - model.init(Sonos, false) - model.load(zoneId) + roomsModel.load(Sonos, zoneId) selectAll() } diff --git a/app/ui/NoZoneState.qml b/app/ui/NoZoneState.qml index b783c73c..a273deb3 100644 --- a/app/ui/NoZoneState.qml +++ b/app/ui/NoZoneState.qml @@ -129,17 +129,7 @@ Page { width: parent.width onClicked: { - mainView.currentlyWorking = true - delayConnectSonos.start() - } - - Timer { - id: delayConnectSonos - interval: 100 - onTriggered: { - connectSonos() - mainView.currentlyWorking = false - } + connectSonos() } } } diff --git a/app/ui/NowPlaying.qml b/app/ui/NowPlaying.qml index 1db54330..3c55fbb6 100644 --- a/app/ui/NowPlaying.qml +++ b/app/ui/NowPlaying.qml @@ -140,7 +140,7 @@ MusicPage { }, MultiSelectHeadState { addToQueue: false - listview: queueLoader.item.listview + listview: queueLoader.status === Loader.Ready ? queueLoader.item.listview : null removable: true thisPage: nowPlaying thisHeader { diff --git a/app/ui/Playlists.qml b/app/ui/Playlists.qml index 269a2439..9980ebcf 100644 --- a/app/ui/Playlists.qml +++ b/app/ui/Playlists.qml @@ -114,6 +114,15 @@ MusicPage { "line2": model.title, }) } + + // check favorite on data loaded + Connections { + target: AllFavoritesModel + onLoaded: { + isFavorite = (AllFavoritesModel.findFavorite(model.payload).length > 0) + } + } + onPressAndHold: { if (isFavorite && removeFromFavorites(model.payload)) isFavorite = false diff --git a/app/ui/Radios.qml b/app/ui/Radios.qml index d2023a44..7fd49706 100644 --- a/app/ui/Radios.qml +++ b/app/ui/Radios.qml @@ -84,16 +84,6 @@ MusicPage { filterCaseSensitivity: Qt.CaseInsensitive } - Timer { - id: delayLoadModel - interval: 100 - onTriggered: { - AllRadiosModel.load(); - radiosPage.taintedView = false; // reset - mainView.currentlyWorking = false; - } - } - // Hack for autopilot otherwise Albums appears as MusicPage // due to bug 1341671 it is required that there is a property so that // qml doesn't optimise using the parent type @@ -176,9 +166,7 @@ MusicPage { } onItemClicked: { - mainView.currentlyWorking = true - delayRadioClicked.model = model - delayRadioClicked.start() + radioClicked(model) // play radio } } @@ -226,10 +214,17 @@ MusicPage { ""}] onClicked: { - mainView.currentlyWorking = true - delayRadioClicked.model = model - delayRadioClicked.start() + radioClicked(model) // play radio } + + // check favorite on data updated + Connections { + target: AllFavoritesModel + onLoaded: { + isFavorite = (AllFavoritesModel.findFavorite(model.payload).length > 0) + } + } + onPressAndHold: { if (isFavorite && removeFromFavorites(model.payload)) isFavorite = false; @@ -246,14 +241,5 @@ MusicPage { } } - Timer { - id: delayRadioClicked - interval: 100 - property QtObject model - onTriggered: { - radioClicked(model) // play radio - mainView.currentlyWorking = false - } - } } diff --git a/app/ui/Service.qml b/app/ui/Service.qml index 6d3e2f87..3f26e8b0 100644 --- a/app/ui/Service.qml +++ b/app/ui/Service.qml @@ -30,11 +30,13 @@ MusicPage { id: servicePage objectName: "servicePage" + property bool isListView: false property var serviceItem: null property bool loaded: false // used to detect difference between first and further loads property bool isRoot: mediaModel.isRoot property int displayType: 3 // display type for root - property bool isListView: false + property int parentDisplayType: 0 + property bool focusViewIndex: false // the model handles search property alias searchableModel: mediaModel @@ -76,59 +78,54 @@ MusicPage { art: serviceItem.id === "SA_RINCON65031_" ? Qt.resolvedUrl("../graphics/tunein.png") : serviceItem.icon } + property alias model: mediaModel // used in ServiceHeadState + MediaModel { id: mediaModel } - Timer { - id: delayInitModel - interval: 100 - onTriggered: { - mediaModel.init(Sonos, serviceItem.payload, true) - mainView.currentlyWorking = false - servicePage.loaded = true; - } - } - - Timer { - id: delayLoadModel - interval: 100 - onTriggered: { - mediaModel.load(); - servicePage.taintedView = false; // reset - mainView.currentlyWorking = false; - } - } - - Timer { - id: delayLoadMore - interval: 100 - onTriggered: { - mediaModel.loadMore() - mainView.currentlyWorking = false - } - } - - Timer { - id: delayLoadRootModel - interval: 100 - onTriggered: { - mediaModel.loadRoot(); - servicePage.taintedView = false; // reset - mainView.currentlyWorking = false; + function restoreFocusViewIndex() { + var idx = mediaModel.viewIndex() + if (mediaModel.count <= idx) { + mediaModel.asyncLoadMore() // load more !!! + } else { + focusViewIndex = false; + mediaList.positionViewAtIndex(idx, ListView.Center); + mediaGrid.positionViewAtIndex(idx, GridView.Center); } } Connections { target: mediaModel - onDataUpdated: { - mainView.currentlyWorking = true - delayLoadModel.start() + onDataUpdated: mediaModel.asyncLoad() + onLoaded: { + if (succeeded) { + mediaModel.resetModel() + servicePage.displayType = servicePage.parentDisplayType // apply displayType + servicePage.taintedView = false // reset + if (focusViewIndex) { + // restore index position in view + restoreFocusViewIndex() + } else { + mediaList.positionViewAtIndex(0, ListView.Top); + mediaGrid.positionViewAtIndex(0, GridView.Top); + } + } + } + onLoadedMore: { + if (succeeded) { + mediaModel.appendModel() + if (focusViewIndex) { + // restore index position in view + restoreFocusViewIndex() + } + } else if (focusViewIndex) { + focusViewIndex = false; + mediaList.positionViewAtEnd(); + mediaGrid.positionViewAtEnd(); + } } - } - Connections { - target: mediaModel onPathChanged: { if (mediaModel.isRoot) { pageTitle = serviceItem.title; @@ -206,7 +203,7 @@ MusicPage { : model.canPlay && !model.canQueue ? Qt.resolvedUrl("../graphics/radio.png") : Qt.resolvedUrl("../graphics/no_cover.png") - imageSource: model.art !== "" ? model.art + imageSource: model.art !== undefined && model.art.length > 0 ? model.art : model.type === 2 ? Qt.resolvedUrl("../graphics/none.png") : model.canPlay && !model.canQueue ? Qt.resolvedUrl("../graphics/radio.png") : Qt.resolvedUrl("../graphics/no_cover.png") @@ -271,8 +268,8 @@ MusicPage { onAtYEndChanged: { if (mediaList.atYEnd && mediaModel.totalCount > mediaModel.count) { - mainView.currentlyWorking = true - delayLoadMore.start() + focusViewIndex = true; + mediaModel.asyncLoadMore() } } } @@ -296,7 +293,7 @@ MusicPage { delegate: Card { id: favoriteCard primaryText: model.title - secondaryText: model.description.length > 0 ? model.description + secondaryText: model.description !== undefined && model.description.length > 0 ? model.description : model.type === 1 ? model.artist.length > 0 ? model.artist : i18n.tr("Album") : model.type === 2 ? i18n.tr("Artist") : model.type === 3 ? i18n.tr("Genre") @@ -317,6 +314,15 @@ MusicPage { : [{art: Qt.resolvedUrl("../graphics/no_cover.png")}] onClicked: clickItem(model) + + // check favorite on data loaded + Connections { + target: AllFavoritesModel + onCountChanged: { + isFavorite = (AllFavoritesModel.findFavorite(model.payload).length > 0) + } + } + onPressAndHold: { if (model.canPlay) { if (isFavorite && removeFromFavorites(model.payload)) @@ -335,89 +341,42 @@ MusicPage { onAtYEndChanged: { if (mediaGrid.atYEnd && mediaModel.totalCount > mediaModel.count) { - mainView.currentlyWorking = true - delayLoadMore.start() + mediaModel.asyncLoadMore() } } } Component.onCompleted: { - mainView.currentlyWorking = true; - delayInitModel.start(); - } - - Timer { - id: delayGoUp - interval: 100 - onTriggered: { - // change view depending of parent display type - servicePage.displayType = mediaModel.parentDisplayType(); - mediaModel.loadParent(); - // restore index position in view - var idx = mediaModel.viewIndex(); - while (mediaModel.count <= idx && mediaModel.loadMore()); - if (idx < mediaModel.count) { - mediaList.positionViewAtIndex(idx, ListView.Center); - mediaGrid.positionViewAtIndex(idx, GridView.Center); - } else { - mediaList.positionViewAtEnd(); - mediaGrid.positionViewAtEnd(); - } - mainView.currentlyWorking = false; - } + mediaModel.init(Sonos, serviceItem.payload, false) + mediaModel.asyncLoad() } function goUp() { - mainView.currentlyWorking = true - delayGoUp.start() - } - - Timer { - id: delayMediaClicked - interval: 100 - property QtObject model - onTriggered: { - if (model.isContainer) { - var pdt = servicePage.displayType; - servicePage.displayType = model.displayType; - mediaModel.loadChild(model.id, model.title, pdt, model.index); - mediaList.positionViewAtIndex(0, ListView.Top); - mediaGrid.positionViewAtIndex(0, GridView.Top); - } else if (model.canPlay) { - if (model.canQueue) - trackClicked(model); - else - radioClicked(model); - } - mainView.currentlyWorking = false - } + // change view depending of parent display type + servicePage.parentDisplayType = mediaModel.parentDisplayType(); + focusViewIndex = true; + mediaModel.asyncLoadParent(); } function clickItem(model) { - mainView.currentlyWorking = true - delayMediaClicked.model = model - delayMediaClicked.start() - } - - Timer { - id: delayPlayMedia - interval: 100 - property QtObject model - onTriggered: { - if (model.canPlay) { - if (model.canQueue) - trackClicked(model); - else - radioClicked(model); - } - mainView.currentlyWorking = false + if (model.isContainer) { + servicePage.parentDisplayType = model.displayType; + mediaModel.asyncLoadChild(model.id, model.title, servicePage.displayType, model.index); + } else if (model.canPlay) { + if (model.canQueue) + trackClicked(model); + else + radioClicked(model); } } function playItem(model) { - mainView.currentlyWorking = true - delayPlayMedia.model = model - delayPlayMedia.start() + if (model.canPlay) { + if (model.canQueue) + trackClicked(model); + else + radioClicked(model); + } } //////////////////////////////////////////////////////////////////////////// @@ -445,27 +404,26 @@ MusicPage { if (mediaModel.isAuthExpired) { if (mediaModel.policyAuth == 1) { if (!loginService.active) { - mediaModel.clear(); // first try with saved login/password var auth = mediaModel.getDeviceAuth(); if (auth.key.length === 0 || mediaModel.requestSessionId(mediaModel.username, auth.key) === 0) loginService.active = true; // show login registration else { // refresh the model - delayLoadModel.start(); + mediaModel.asyncLoad(); } } } else if (mediaModel.policyAuth == 2 || mediaModel.policyAuth == 3) { if (registeringService.active) registeringService.active = false; // restart new registration else - mediaModel.clear(); + mediaModel.clearData(); registeringService.active = true; } } else { loginService.active = false; registeringService.active = false; - mainView.currentlyWorking = true; + mainView.jobRunning = true; // save new incarnation of accounts settings var auth = mediaModel.getDeviceAuth(); var acls = deserializeACLS(startupSettings.accounts); @@ -479,7 +437,7 @@ MusicPage { _acls.push({type: auth.type, sn: auth.serialNum, key: auth.key, token: auth.token}); startupSettings.accounts = serializeACLS(_acls); // refresh the model - delayLoadModel.start(); + mediaModel.asyncLoad(); } } } diff --git a/app/ui/SongsView.qml b/app/ui/SongsView.qml index 81b80efc..e11a90e1 100644 --- a/app/ui/SongsView.qml +++ b/app/ui/SongsView.qml @@ -34,7 +34,7 @@ MusicPage { id: songStackPage objectName: "songsPage" visible: false - pageFlickable: albumtrackslist + pageFlickable: songList property string line1: "" property string line2: "" @@ -50,13 +50,11 @@ MusicPage { property string artist: "" property string genre: "" - property bool loaded: false // used to detect difference between first and further loads - width: mainPageStack.width property bool isFavorite: false - state: albumtrackslist.state === "multiselectable" ? "selection" : (isPlaylist ? "playlist" : "album") + state: songList.state === "multiselectable" ? "selection" : (isPlaylist ? "playlist" : "album") states: [ AlbumSongsHeadState { thisPage: songStackPage @@ -72,7 +70,7 @@ MusicPage { }, MultiSelectHeadState { containerItem: songStackPage.containerItem - listview: albumtrackslist + listview: songList removable: isPlaylist thisPage: songStackPage thisHeader { @@ -80,7 +78,6 @@ MusicPage { } onRemoved: { - mainView.currentlyWorking = false delayRemoveSelectedFromPlaylist.selectedIndices = selectedIndices delayRemoveSelectedFromPlaylist.start() } @@ -92,47 +89,57 @@ MusicPage { interval: 100 property var selectedIndices: [] onTriggered: { - var cnt = songsModel.count; + songList.focusIndex = selectedIndices[selectedIndices.length-1]; if (removeTracksFromPlaylist(containerItem.id, selectedIndices, songsModel.containerUpdateID())) { - songsModel.load(); - while (songsModel.count < cnt && songsModel.loadMore()); + songsModel.asyncLoad(); } - mainView.currentlyWorking = false } } TracksModel { id: songsModel + onDataUpdated: songsModel.asyncLoad() + onLoaded: songsModel.resetModel() + Component.onCompleted: { + songsModel.init(Sonos, songSearch, false) + songsModel.asyncLoad() + } } - Timer { - id: delayInitModel - interval: 100 - onTriggered: { - isFavorite = (AllFavoritesModel.findFavorite(containerItem.payload).length > 0) - songsModel.init(Sonos, songSearch, true) - mainView.currentlyWorking = false - songStackPage.loaded = true; + function restoreFocusIndex() { + if (songsModel.count <= songList.focusIndex) { + songsModel.asyncLoadMore() // load more !!! + } else { + songList.positionViewAtIndex(songList.focusIndex, ListView.Center); + songList.focusIndex = -1 } } Connections { target: songsModel - onDataUpdated: { - mainView.currentlyWorking = true - delayLoadTrackModel.start() + onDataUpdated: songsModel.asyncLoad() + onLoaded: { + songsModel.resetModel() + if (succeeded) { + if (songList.focusIndex > 0) { + // restore index position in view + restoreFocusIndex() + } + } } - } - - Timer { - id: delayLoadTrackModel - interval: 100 - onTriggered: { - var cnt = songsModel.count; - songsModel.load(); - while (songsModel.count < cnt && songsModel.loadMore()); - mainView.currentlyWorking = false + onLoadedMore: { + if (succeeded) { + songsModel.appendModel(); + if (songList.focusIndex > 0) { + // restore index position in view + restoreFocusIndex() + } + } else if (songList.focusIndex > 0) { + songList.positionViewAtEnd(); + songList.focusIndex = -1; + } } + } Repeater { @@ -160,7 +167,7 @@ MusicPage { } MultiSelectListView { - id: albumtrackslist + id: songList anchors { fill: parent } @@ -170,7 +177,7 @@ MusicPage { rightColumn: Column { spacing: units.gu(2) ShuffleButton { - model: albumtrackslist.model + model: songList.model width: blurredHeader.width > units.gu(60) ? units.gu(23.5) : (blurredHeader.width - units.gu(13)) / 2 } QueueAllButton { @@ -255,11 +262,13 @@ MusicPage { model: songsModel + property int focusIndex: 0 + delegate: MusicListItem { id: track color: "transparent" - noCover: !songStackPage.isAlbum ? Qt.resolvedUrl("../graphics/no_cover.png") : "" - imageSource: !songStackPage.isAlbum ? makeCoverSource(model.art, model.author, model.album) : "" + noCover: Qt.resolvedUrl("../graphics/no_cover.png") + imageSource: !songStackPage.isAlbum ? makeCoverSource(model.art, model.author, model.album) : noCover column: Column { Label { id: trackTitle @@ -304,7 +313,6 @@ MusicPage { actions: [ Remove { onTriggered: { - mainView.currentlyWorking = true delayRemoveTrackFromPlaylist.start() } } @@ -316,12 +324,10 @@ MusicPage { id: delayRemoveTrackFromPlaylist interval: 100 onTriggered: { - var cnt = songsModel.count; + songList.focusIndex = index > 0 ? index - 1 : 0; if (removeTracksFromPlaylist(containerItem.id, [index], songsModel.containerUpdateID())) { - songsModel.load(); - while (songsModel.count < cnt && songsModel.loadMore()); + songsModel.asyncLoad(); } - mainView.currentlyWorking = false } } @@ -333,7 +339,9 @@ MusicPage { } onReorder: { - mainView.currentlyWorking = true + customdebug("Reorder queue item " + from + " to " + to); + songList.focusIndex = to + mainView.jobRunning = true delayReorderTrackInPlaylist.argFrom = from delayReorderTrackInPlaylist.argTo = to delayReorderTrackInPlaylist.start() @@ -346,32 +354,33 @@ MusicPage { property int argTo: 0 onTriggered: { if (reorderTrackInPlaylist(containerItem.id, argFrom, argTo, songsModel.containerUpdateID())) { - songsModel.load(); + songsModel.asyncLoad(); } - mainView.currentlyWorking = false } } onAtYEndChanged: { - if (albumtrackslist.atYEnd && songsModel.totalCount > songsModel.count) { - mainView.currentlyWorking = true - delayLoadMoreTracks.start() + if (songList.atYEnd && songsModel.totalCount > songsModel.count) { + songsModel.asyncLoadMore() } } - Timer { - id: delayLoadMoreTracks - interval: 100 - onTriggered: { - songsModel.loadMore() - mainView.currentlyWorking = false - } - } + } + + Scrollbar { + flickableItem: songList + align: Qt.AlignTrailing + } + // check favorite on data loaded + Connections { + target: AllFavoritesModel + onCountChanged: { + isFavorite = (AllFavoritesModel.findFavorite(containerItem.payload).length > 0) + } } Component.onCompleted: { - mainView.currentlyWorking = true - delayInitModel.start() + isFavorite = (AllFavoritesModel.findFavorite(containerItem.payload).length > 0) } } diff --git a/app/ui/Zones.qml b/app/ui/Zones.qml index 0f18eac7..c3d67542 100644 --- a/app/ui/Zones.qml +++ b/app/ui/Zones.qml @@ -93,8 +93,7 @@ BottomEdgePage { text: i18n.tr("Reload zones") visible: true onTriggered: { - mainView.currentlyWorking = true - delayResetController.start() + connectSonos() } }, Action { @@ -134,12 +133,9 @@ BottomEdgePage { iconName: "back" onTriggered: { if (zoneList.getSelectedIndices().length > 1) { - mainView.currentlyWorking = true - delayJoinZones.start() - } - else { - zoneList.closeSelection() + handleJoinZones() } + zoneList.closeSelection() } } ] @@ -177,27 +173,6 @@ BottomEdgePage { } ] - Timer { - id: delayResetController - interval: 100 - onTriggered: { - connectSonos() - // activity indicator will be hidden after finished loading - } - } - - Timer { - id: delayJoinZones - interval: 100 - onTriggered: { - handleJoinZones() - zoneList.closeSelection() - // Zones will be reloaded on signal topologyChanged - // Signal is handled in MainView - mainView.currentlyWorking = false - } - } - function handleJoinZones() { var indicies = zoneList.getSelectedIndices(); // get current as master @@ -255,8 +230,7 @@ BottomEdgePage { Clear { visible: model.isGroup onTriggered: { - mainView.currentlyWorking = true - delayUnjoinZone.start() + Sonos.unjoinZone(model.payload) } } ] @@ -283,40 +257,7 @@ BottomEdgePage { } onItemClicked: { - mainView.currentlyWorking = true - delayChangeZone.start() - } - - Timer { - id: delayChangeZone - interval: 100 - onTriggered: { - if (currentZone !== model.name) { - customdebug("Connecting zone '" + name + "'"); - if ((Sonos.connectZone(model.name) || Sonos.connectZone("")) && player.connect()) { - currentZone = Sonos.getZoneName(); - currentZoneTag = Sonos.getZoneShortName(); - if (noZone) - noZone = false; - } - else { - if (!noZone) - noZone = true; - } - } - mainView.currentlyWorking = false - } - } - - Timer { - id: delayUnjoinZone - interval: 100 - onTriggered: { - Sonos.unjoinZone(model.payload) - // Zones will be reloaded on signal topologyChanged - // Signal is handled in MainView - mainView.currentlyWorking = false - } + connectZone(model.name) } onSelectedChanged: { @@ -357,6 +298,5 @@ BottomEdgePage { } } } - } } diff --git a/backend/CMakeLists.txt b/backend/CMakeLists.txt index 4e6ed479..629a9866 100644 --- a/backend/CMakeLists.txt +++ b/backend/CMakeLists.txt @@ -1,33 +1,58 @@ -cmake_policy (VERSION 3.0) -add_subdirectory (${CMAKE_CURRENT_SOURCE_DIR}/lib/noson/noson) +cmake_minimum_required(VERSION 2.8.9) +# Automatically create moc files +set(CMAKE_AUTOMOC ON) + +find_package(Qt5Core REQUIRED) +find_package(Qt5Gui REQUIRED) +find_package(Qt5Qml REQUIRED) +find_package(Qt5Quick REQUIRED) + +add_subdirectory (${CMAKE_CURRENT_SOURCE_DIR}/lib) ############################################################################### # configure include_directories( ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_CURRENT_BINARY_DIR}/lib/noson/noson/include/. - ${CMAKE_CURRENT_SOURCE_DIR}/lib/noson/src/. + ${CMAKE_CURRENT_BINARY_DIR}/lib/noson/noson/include ) set( NosonAppbackend_SRCS + modules/NosonApp/tools.h modules/NosonApp/backend.cpp + modules/NosonApp/backend.h modules/NosonApp/sonos.cpp + modules/NosonApp/sonos.h modules/NosonApp/player.cpp + modules/NosonApp/player.h modules/NosonApp/listmodel.cpp + modules/NosonApp/listmodel.h modules/NosonApp/albumsmodel.cpp + modules/NosonApp/albumsmodel.h modules/NosonApp/artistsmodel.cpp + modules/NosonApp/artistsmodel.h modules/NosonApp/genresmodel.cpp + modules/NosonApp/genresmodel.h modules/NosonApp/tracksmodel.cpp + modules/NosonApp/tracksmodel.h modules/NosonApp/queuemodel.cpp + modules/NosonApp/queuemodel.h modules/NosonApp/radiosmodel.cpp + modules/NosonApp/radiosmodel.h modules/NosonApp/playlistsmodel.cpp + modules/NosonApp/playlistsmodel.h modules/NosonApp/zonesmodel.cpp + modules/NosonApp/zonesmodel.h modules/NosonApp/renderingmodel.cpp + modules/NosonApp/renderingmodel.h modules/NosonApp/roomsmodel.cpp + modules/NosonApp/roomsmodel.h modules/NosonApp/favoritesmodel.cpp + modules/NosonApp/favoritesmodel.h modules/NosonApp/servicesmodel.cpp + modules/NosonApp/servicesmodel.h modules/NosonApp/mediamodel.cpp + modules/NosonApp/mediamodel.h ) add_library(NosonAppbackend MODULE @@ -49,6 +74,7 @@ add_custom_target(NosonAppbackend-qmldir ALL ) # Install plugin file -install(TARGETS NosonAppbackend DESTINATION ${QT_IMPORTS_DIR}/NosonApp/) -install(FILES modules/NosonApp/qmldir DESTINATION ${QT_IMPORTS_DIR}/NosonApp/) +MESSAGE(STATUS "PlugIns install path: ${PLUGINS_DIR}") +install(TARGETS NosonAppbackend DESTINATION ${PLUGINS_DIR}/NosonApp/) +install(FILES modules/NosonApp/qmldir DESTINATION ${PLUGINS_DIR}/NosonApp/) diff --git a/backend/lib/CMakeLists.txt b/backend/lib/CMakeLists.txt new file mode 100644 index 00000000..024b1e51 --- /dev/null +++ b/backend/lib/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 2.8.9) + +find_package(ZLIB) +find_package(OpenSSL) + +# Provides noson +add_subdirectory( + ${CMAKE_CURRENT_SOURCE_DIR}/noson/noson + EXCLUDE_FROM_ALL +) diff --git a/backend/lib/noson/noson/CMakeLists.txt b/backend/lib/noson/noson/CMakeLists.txt index 96072916..baf02965 100644 --- a/backend/lib/noson/noson/CMakeLists.txt +++ b/backend/lib/noson/noson/CMakeLists.txt @@ -95,7 +95,9 @@ else () set (HAVE_GMTIME_R 0) endif () -find_package (ZLIB REQUIRED) +if (NOT ZLIB_FOUND) + find_package (ZLIB REQUIRED) +endif() if (ZLIB_FOUND) include_directories (${ZLIB_INCLUDE_DIRS}) set (HAVE_ZLIB 1) @@ -103,7 +105,9 @@ else () set (HAVE_ZLIB 0) endif () -find_package(OpenSSL REQUIRED) +if (NOT OPENSSL_FOUND) + find_package(OpenSSL REQUIRED) +endif() if (OPENSSL_FOUND) include_directories (${OPENSSL_INCLUDE_DIR}) set (HAVE_OPENSSL 1) diff --git a/backend/modules/NosonApp/albumsmodel.cpp b/backend/modules/NosonApp/albumsmodel.cpp index ace1b3a7..b6506840 100644 --- a/backend/modules/NosonApp/albumsmodel.cpp +++ b/backend/modules/NosonApp/albumsmodel.cpp @@ -54,7 +54,9 @@ AlbumsModel::AlbumsModel(QObject* parent) AlbumsModel::~AlbumsModel() { - clear(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); } void AlbumsModel::addItem(AlbumItem* item) @@ -101,6 +103,23 @@ QVariant AlbumsModel::data(const QModelIndex& index, int role) const } } +bool AlbumsModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + SONOS::LockGuard lock(m_lock); + if (index.row() < 0 || index.row() >= m_items.count()) + return false; + + AlbumItem* item = m_items[index.row()]; + switch (role) + { + case ArtRole: + item->setArt(value.toString()); + return true; + default: + return false; + } +} + QHash AlbumsModel::roleNames() const { QHash roles; @@ -140,29 +159,32 @@ bool AlbumsModel::init(QObject* sonos, const QString& root, bool fill) return ListModel::init(sonos, _root, fill); } -void AlbumsModel::clear() +void AlbumsModel::clearData() { - { - SONOS::LockGuard lock(m_lock); - beginRemoveRows(QModelIndex(), 0, m_items.count()); - qDeleteAll(m_items); - m_items.clear(); - endRemoveRows(); - } - emit countChanged(); + SONOS::LockGuard lock(m_lock); + qDeleteAll(m_data); + m_data.clear(); } -bool AlbumsModel::load() +bool AlbumsModel::loadData() { setUpdateSignaled(false); if (!m_provider) + { + emit loaded(false); return false; - clear(); + } const SONOS::PlayerPtr player = m_provider->getPlayer(); if (!player) + { + emit loaded(false); return false; + } + SONOS::LockGuard lock(m_lock); + clearData(); + m_dataState = ListModel::NoData; QString port; port.setNum(player->GetPort()); QString url = "http://"; @@ -174,20 +196,52 @@ bool AlbumsModel::load() { AlbumItem* item = new AlbumItem(*it, url); if (item->isValid()) - addItem(item); + m_data << item; else delete item; } + if (cl.failure()) - return m_loaded = false; + { + emit loaded(false); + return false; + } m_updateID = cl.GetUpdateID(); // sync new baseline - return m_loaded = true; + m_dataState = ListModel::Loaded; + emit loaded(true); + return true; } bool AlbumsModel::asyncLoad() { if (m_provider) + { m_provider->runModelLoader(this); + return true; + } + return false; +} + +void AlbumsModel::resetModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (AlbumItem* item, m_data) + m_items << item; + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + endResetModel(); + } + emit countChanged(); } void AlbumsModel::handleDataUpdate() diff --git a/backend/modules/NosonApp/albumsmodel.h b/backend/modules/NosonApp/albumsmodel.h index c681ce0b..5b8d8ce4 100644 --- a/backend/modules/NosonApp/albumsmodel.h +++ b/backend/modules/NosonApp/albumsmodel.h @@ -46,6 +46,8 @@ class AlbumItem const QString& normalized() const { return m_normalized; } + void setArt(const QString& art) { m_art = art; } + private: SONOS::DigitalItemPtr m_ptr; bool m_valid; @@ -81,16 +83,22 @@ class AlbumsModel : public QAbstractListModel, public ListModel QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole); + Q_INVOKABLE QVariantMap get(int row); Q_INVOKABLE bool init(QObject* sonos, const QString& root, bool fill = false); - Q_INVOKABLE void clear(); + virtual void clearData(); - Q_INVOKABLE bool load(); + virtual bool loadData(); Q_INVOKABLE bool asyncLoad(); - + + Q_INVOKABLE void resetModel(); + + Q_INVOKABLE void appendModel() { } + virtual void handleDataUpdate(); Q_INVOKABLE int containerUpdateID() { return m_updateID; } @@ -98,12 +106,14 @@ class AlbumsModel : public QAbstractListModel, public ListModel signals: void dataUpdated(); void countChanged(); + void loaded(bool succeeded); protected: QHash roleNames() const; private: QList m_items; + QList m_data; }; #endif // ALBUMSMODEL diff --git a/backend/modules/NosonApp/artistsmodel.cpp b/backend/modules/NosonApp/artistsmodel.cpp index 821f077b..d959561d 100644 --- a/backend/modules/NosonApp/artistsmodel.cpp +++ b/backend/modules/NosonApp/artistsmodel.cpp @@ -32,7 +32,7 @@ ArtistItem::ArtistItem(const SONOS::DigitalItemPtr& ptr, const QString& baseURL) { m_artist = QString::fromUtf8(ptr->GetValue("dc:title").c_str()); m_normalized = normalizedString(m_artist); - //m_art.append(baseURL).append(uri); + (void)baseURL; //m_art.append(baseURL).append(uri); m_valid = true; } } @@ -51,7 +51,9 @@ ArtistsModel::ArtistsModel(QObject* parent) ArtistsModel::~ArtistsModel() { - clear(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); } void ArtistsModel::addItem(ArtistItem* item) @@ -133,29 +135,32 @@ bool ArtistsModel::init(QObject* sonos, const QString& root, bool fill) return ListModel::init(sonos, _root, fill); } -void ArtistsModel::clear() +void ArtistsModel::clearData() { - { - SONOS::LockGuard lock(m_lock); - beginRemoveRows(QModelIndex(), 0, m_items.count()); - qDeleteAll(m_items); - m_items.clear(); - endRemoveRows(); - } - emit countChanged(); + SONOS::LockGuard lock(m_lock); + qDeleteAll(m_data); + m_data.clear(); } -bool ArtistsModel::load() +bool ArtistsModel::loadData() { setUpdateSignaled(false); if (!m_provider) + { + emit loaded(false); return false; - clear(); + } const SONOS::PlayerPtr player = m_provider->getPlayer(); if (!player) + { + emit loaded(false); return false; + } + SONOS::LockGuard lock(m_lock); + clearData(); + m_dataState = ListModel::NoData; QString port; port.setNum(player->GetPort()); QString url = "http://"; @@ -167,20 +172,51 @@ bool ArtistsModel::load() { ArtistItem* item = new ArtistItem(*it, url); if (item->isValid()) - addItem(item); + m_data << item; else delete item; } if (cl.failure()) - return m_loaded = false; + { + emit loaded(false); + return false; + } m_updateID = cl.GetUpdateID(); // sync new baseline - return m_loaded = true; + m_dataState = ListModel::Loaded; + emit loaded(true); + return true; } bool ArtistsModel::asyncLoad() { if (m_provider) + { m_provider->runModelLoader(this); + return true; + } + return false; +} + +void ArtistsModel::resetModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (ArtistItem* item, m_data) + m_items << item; + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + endResetModel(); + } + emit countChanged(); } void ArtistsModel::handleDataUpdate() diff --git a/backend/modules/NosonApp/artistsmodel.h b/backend/modules/NosonApp/artistsmodel.h index dd066122..f32b20f2 100644 --- a/backend/modules/NosonApp/artistsmodel.h +++ b/backend/modules/NosonApp/artistsmodel.h @@ -81,12 +81,16 @@ class ArtistsModel : public QAbstractListModel, public ListModel Q_INVOKABLE bool init(QObject* sonos, const QString& root, bool fill = false); - Q_INVOKABLE void clear(); + virtual void clearData(); - Q_INVOKABLE bool load(); + virtual bool loadData(); Q_INVOKABLE bool asyncLoad(); + Q_INVOKABLE void resetModel(); + + Q_INVOKABLE void appendModel() { } + virtual void handleDataUpdate(); Q_INVOKABLE int containerUpdateID() { return m_updateID; } @@ -94,12 +98,14 @@ class ArtistsModel : public QAbstractListModel, public ListModel signals: void dataUpdated(); void countChanged(); + void loaded(bool succeeded); protected: QHash roleNames() const; private: QList m_items; + QList m_data; }; #endif /* ARTISTSMODEL_H */ diff --git a/backend/modules/NosonApp/favoritesmodel.cpp b/backend/modules/NosonApp/favoritesmodel.cpp index e044cb43..f79cb50f 100644 --- a/backend/modules/NosonApp/favoritesmodel.cpp +++ b/backend/modules/NosonApp/favoritesmodel.cpp @@ -98,7 +98,9 @@ FavoritesModel::FavoritesModel(QObject* parent) FavoritesModel::~FavoritesModel() { - clear(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); } void FavoritesModel::addItem(FavoriteItem* item) @@ -160,6 +162,23 @@ QVariant FavoritesModel::data(const QModelIndex& index, int role) const } } +bool FavoritesModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + SONOS::LockGuard lock(m_lock); + if (index.row() < 0 || index.row() >= m_items.count()) + return false; + + FavoriteItem* item = m_items[index.row()]; + switch (role) + { + case ArtRole: + item->setArt(value.toString()); + return true; + default: + return false; + } +} + QHash FavoritesModel::roleNames() const { QHash roles; @@ -213,30 +232,32 @@ bool FavoritesModel::init(QObject* sonos, const QString& root, bool fill) return ListModel::init(sonos, _root, fill); } -void FavoritesModel::clear() +void FavoritesModel::clearData() { - { - SONOS::LockGuard lock(m_lock); - beginRemoveRows(QModelIndex(), 0, m_items.count()); - qDeleteAll(m_items); - m_items.clear(); - m_objectIDs.clear(); - endRemoveRows(); - } - emit countChanged(); + SONOS::LockGuard lock(m_lock); + qDeleteAll(m_data); + m_data.clear(); } -bool FavoritesModel::load() +bool FavoritesModel::loadData() { setUpdateSignaled(false); if (!m_provider) + { + emit loaded(false); return false; - clear(); + } const SONOS::PlayerPtr player = m_provider->getPlayer(); if (!player) + { + emit loaded(false); return false; + } + SONOS::LockGuard lock(m_lock); + clearData(); + m_dataState = ListModel::NoData; QString port; port.setNum(player->GetPort()); QString url = "http://"; @@ -248,20 +269,54 @@ bool FavoritesModel::load() { FavoriteItem* item = new FavoriteItem(*it, url); if (item->isValid()) - addItem(item); + m_data << item; else delete item; } if (cl.failure()) - return m_loaded = false; + { + emit loaded(false); + return false; + } m_updateID = cl.GetUpdateID(); // sync new baseline - return m_loaded = true; + m_dataState = ListModel::Loaded; + emit loaded(true); + return true; } bool FavoritesModel::asyncLoad() { if (m_provider) + { m_provider->runModelLoader(this); + return true; + } + return false; +} + +void FavoritesModel::resetModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + m_objectIDs.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (FavoriteItem* item, m_data) { + m_items << item; + m_objectIDs.insert(item->objectId(), item->id()); + } + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + endResetModel(); + } + emit countChanged(); } void FavoritesModel::handleDataUpdate() @@ -275,6 +330,8 @@ void FavoritesModel::handleDataUpdate() QString FavoritesModel::findFavorite(const QVariant& payload) const { + if (!m_provider) + return ""; SONOS::DigitalItemPtr ptr = payload.value(); SONOS::PlayerPtr player = m_provider->getPlayer(); if (ptr && player) diff --git a/backend/modules/NosonApp/favoritesmodel.h b/backend/modules/NosonApp/favoritesmodel.h index cfa5363e..769db0a8 100644 --- a/backend/modules/NosonApp/favoritesmodel.h +++ b/backend/modules/NosonApp/favoritesmodel.h @@ -31,16 +31,19 @@ class FavoriteType : public QObject Q_OBJECT Q_ENUMS(itemType) - public: - enum itemType - { - unknown = 0, - album = 1, - person = 2, - genre = 3, - playlist = 4, - audioItem = 5, - }; +public: + enum itemType + { + unknown = 0, + album = 1, + person = 2, + genre = 3, + playlist = 4, + audioItem = 5, + }; + + FavoriteType(QObject* parent = 0) + : QObject(parent) { } }; class FavoriteItem @@ -78,6 +81,8 @@ class FavoriteItem bool isService() const { return m_isService; } + void setArt(const QString& art) { m_art = art; } + private: SONOS::DigitalItemPtr m_ptr; bool m_valid; @@ -127,16 +132,22 @@ class FavoritesModel : public QAbstractListModel, public ListModel QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole); + Q_INVOKABLE QVariantMap get(int row); Q_INVOKABLE bool init(QObject* sonos, const QString& root, bool fill = false); - Q_INVOKABLE void clear(); + virtual void clearData(); - Q_INVOKABLE bool load(); + virtual bool loadData(); Q_INVOKABLE bool asyncLoad(); + Q_INVOKABLE void resetModel(); + + Q_INVOKABLE void appendModel() { } + virtual void handleDataUpdate(); Q_INVOKABLE int containerUpdateID() { return m_updateID; } @@ -146,12 +157,14 @@ class FavoritesModel : public QAbstractListModel, public ListModel signals: void dataUpdated(); void countChanged(); + void loaded(bool succeeded); protected: QHash roleNames() const; private: QList m_items; + QList m_data; QMap m_objectIDs; }; diff --git a/backend/modules/NosonApp/genresmodel.cpp b/backend/modules/NosonApp/genresmodel.cpp index 9fe611f1..ccfb60ab 100644 --- a/backend/modules/NosonApp/genresmodel.cpp +++ b/backend/modules/NosonApp/genresmodel.cpp @@ -32,7 +32,7 @@ GenreItem::GenreItem(const SONOS::DigitalItemPtr& ptr, const QString& baseURL) { m_genre = QString::fromUtf8(ptr->GetValue("dc:title").c_str()); m_normalized = normalizedString(m_genre); - //m_art.append(baseURL).append(uri); + (void)baseURL; //m_art.append(baseURL).append(uri); m_valid = true; } } @@ -51,7 +51,9 @@ GenresModel::GenresModel(QObject* parent) GenresModel::~GenresModel() { - clear(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); } void GenresModel::addItem(GenreItem* item) @@ -129,29 +131,32 @@ bool GenresModel::init(QObject* sonos, const QString& root, bool fill) return ListModel::init(sonos, _root, fill); } -void GenresModel::clear() +void GenresModel::clearData() { - { - SONOS::LockGuard lock(m_lock); - beginRemoveRows(QModelIndex(), 0, m_items.count()); - qDeleteAll(m_items); - m_items.clear(); - endRemoveRows(); - } - emit countChanged(); + SONOS::LockGuard lock(m_lock); + qDeleteAll(m_data); + m_data.clear(); } -bool GenresModel::load() +bool GenresModel::loadData() { setUpdateSignaled(false); if (!m_provider) + { + emit loaded(false); return false; - clear(); + } const SONOS::PlayerPtr player = m_provider->getPlayer(); if (!player) + { + emit loaded(false); return false; + } + SONOS::LockGuard lock(m_lock); + clearData(); + m_dataState = ListModel::NoData; QString port; port.setNum(player->GetPort()); QString url = "http://"; @@ -163,20 +168,51 @@ bool GenresModel::load() { GenreItem* item = new GenreItem(*it, url); if (item->isValid()) - addItem(item); + m_data << item; else delete item; } if (cl.failure()) - return m_loaded = false; + { + emit loaded(false); + return false; + } m_updateID = cl.GetUpdateID(); // sync new baseline - return m_loaded = true; + m_dataState = ListModel::Loaded; + emit loaded(true); + return true; } bool GenresModel::asyncLoad() { if (m_provider) + { m_provider->runModelLoader(this); + return true; + } + return false; +} + +void GenresModel::resetModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (GenreItem* item, m_data) + m_items << item; + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + endResetModel(); + } + emit countChanged(); } void GenresModel::handleDataUpdate() diff --git a/backend/modules/NosonApp/genresmodel.h b/backend/modules/NosonApp/genresmodel.h index 6bb4330e..b7605776 100644 --- a/backend/modules/NosonApp/genresmodel.h +++ b/backend/modules/NosonApp/genresmodel.h @@ -77,12 +77,16 @@ class GenresModel : public QAbstractListModel, public ListModel Q_INVOKABLE bool init(QObject* sonos, const QString& root, bool fill = false); - Q_INVOKABLE void clear(); + virtual void clearData(); - Q_INVOKABLE bool load(); + virtual bool loadData(); Q_INVOKABLE bool asyncLoad(); + Q_INVOKABLE void resetModel(); + + Q_INVOKABLE void appendModel() { } + virtual void handleDataUpdate(); Q_INVOKABLE int containerUpdateID() { return m_updateID; } @@ -90,12 +94,14 @@ class GenresModel : public QAbstractListModel, public ListModel signals: void dataUpdated(); void countChanged(); + void loaded(bool succeeded); protected: QHash roleNames() const; private: QList m_items; + QList m_data; }; #endif /* GENRESMODEL_H */ diff --git a/backend/modules/NosonApp/listmodel.cpp b/backend/modules/NosonApp/listmodel.cpp index 0bbe0d89..9890d8f3 100644 --- a/backend/modules/NosonApp/listmodel.cpp +++ b/backend/modules/NosonApp/listmodel.cpp @@ -26,7 +26,7 @@ ListModel::ListModel() , m_provider(0) , m_updateID(0) , m_pending(false) -, m_loaded(false) +, m_dataState(ListModel::NoData) , m_updateSignaled(false) { m_lock = SONOS::LockGuard::CreateLock(); @@ -53,8 +53,8 @@ bool ListModel::init(QObject* sonos, const QString& root, bool fill /*= false*/) m_provider = _sonos; m_root = root; // Reset container status to allow async reload - m_loaded = false; + m_dataState = ListModel::NoData; if (fill) - return this->load(); + return this->loadData(); return false; // not filled } diff --git a/backend/modules/NosonApp/listmodel.h b/backend/modules/NosonApp/listmodel.h index d6d68ed4..8bf4b916 100644 --- a/backend/modules/NosonApp/listmodel.h +++ b/backend/modules/NosonApp/listmodel.h @@ -42,25 +42,35 @@ class ListModel ListModel(); virtual ~ListModel(); - virtual void clear() = 0; + virtual void clearData() = 0; - virtual bool load() = 0; + virtual bool loadData() = 0; virtual void handleDataUpdate() = 0; + enum dataState { + NoData = 0, + Loaded = 1, + Synced = 2 + }; + protected: SONOS::LockGuard::Lockable* m_lock; Sonos* m_provider; unsigned m_updateID; QString m_root; bool m_pending; - bool m_loaded; + dataState m_dataState; virtual bool init(QObject* sonos, const QString& root, bool fill = false); + virtual bool init(QObject* sonos, bool fill = false) { return init(sonos, QString(""), fill); } + virtual bool init(QObject* sonos, const QVariant&, bool fill = false) { return init(sonos, QString(""), fill); } bool updateSignaled() { return m_updateSignaled.Load(); } void setUpdateSignaled(bool val) { m_updateSignaled.Store(val); } + virtual bool customizedLoad(int id) { (void)id; return false; } + private: SONOS::Locked m_updateSignaled; }; diff --git a/backend/modules/NosonApp/mediamodel.cpp b/backend/modules/NosonApp/mediamodel.cpp index 3b2c726b..28a071c8 100644 --- a/backend/modules/NosonApp/mediamodel.cpp +++ b/backend/modules/NosonApp/mediamodel.cpp @@ -111,7 +111,9 @@ MediaModel::MediaModel(QObject* parent) MediaModel::~MediaModel() { - clear(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); SAFE_DELETE(m_smapi); } @@ -245,29 +247,28 @@ bool MediaModel::init(QObject* sonos, const QVariant& service, bool fill) m_auth.token = oa.token; // initialize path to root m_path.clear(); - return ListModel::init(sonos, "", fill); + return ListModel::init(sonos, fill); } -void MediaModel::clear() +void MediaModel::clearData() { - { - SONOS::LockGuard lock(m_lock); - beginRemoveRows(QModelIndex(), 0, m_items.count()); - qDeleteAll(m_items); - m_items.clear(); - endRemoveRows(); - } - emit countChanged(); + SONOS::LockGuard lock(m_lock); + qDeleteAll(m_data); + m_data.clear(); } -bool MediaModel::load() +bool MediaModel::loadData() { setUpdateSignaled(false); SONOS::LockGuard lock(m_lock); if (!m_smapi) + { + emit loaded(false); return false; + } - clear(); + clearData(); + m_dataState = ListModel::NoData; m_searching = false; // enable browse state m_nextIndex = m_totalCount = 0; SONOS::SMAPIMetadata meta; @@ -276,94 +277,32 @@ bool MediaModel::load() emit totalCountChanged(); if (m_smapi->AuthTokenExpired()) emit authStatusChanged(); + emit loaded(false); return false; } m_totalCount = meta.TotalCount(); m_nextIndex = meta.ItemCount(); - emit totalCountChanged(); SONOS::SMAPIItemList list = meta.GetItems(); for (SONOS::SMAPIItemList::const_iterator it = list.begin(); it != list.end(); ++it) { MediaItem* item = new MediaItem(*it); if (item->isValid()) - addItem(item); - else - delete item; - } - return m_loaded = true; -} - -bool MediaModel::loadMore() -{ - SONOS::LockGuard lock(m_lock); - if (!m_smapi) - return false; - // At end return false - if (m_nextIndex >= m_totalCount) - return false; - - SONOS::SMAPIMetadata meta; - // browse or search for next items depending of current state - if ((!m_searching && !m_smapi->GetMetadata(pathId().toUtf8().constData(), m_nextIndex, LOAD_BULKSIZE, false, meta)) || - (m_searching && !m_smapi->Search(m_searchCategory, m_searchTerm, m_nextIndex, LOAD_BULKSIZE, meta))) - { - if (m_smapi->AuthTokenExpired()) - emit authStatusChanged(); - return false; - } - if (m_totalCount != meta.TotalCount()) - { - m_totalCount = meta.TotalCount(); - emit totalCountChanged(); - } - m_nextIndex += meta.ItemCount(); - - SONOS::SMAPIItemList list= meta.GetItems(); - for (SONOS::SMAPIItemList::const_iterator it = list.begin(); it != list.end(); ++it) - { - MediaItem* item = new MediaItem(*it); - if (item->isValid()) - addItem(item); + m_data << item; else + { delete item; + // Also decrease total count + if (m_totalCount > 0) + --m_totalCount; + } } + emit totalCountChanged(); + m_dataState = ListModel::Loaded; + emit loaded(true); return true; } -bool MediaModel::loadChild(const QString& id, const QString& title, int displayType, int viewIndex /*= 0*/) -{ - if (id.isEmpty()) - return false; - SONOS::LockGuard lock(m_lock); - // save current view index for this path item - if (!m_path.empty()) - m_path.top().viewIndex = viewIndex; - m_path.push(Path(id, title, displayType)); - emit pathChanged(); - return load(); -} - -bool MediaModel::loadParent() -{ - SONOS::LockGuard lock(m_lock); - if (!m_path.empty()) - m_path.pop(); - // reload current search else the parent item - if (pathName() == SEARCH_TAG) - { - m_searching = true; // reset state before signal the change - emit pathChanged(); - return search(); - } - else - { - m_searching = false; // reset state before signal the change - emit pathChanged(); - return load(); - } -} - QString MediaModel::pathName() const { SONOS::LockGuard lock(m_lock); @@ -413,47 +352,6 @@ QList MediaModel::listSearchCategories() const return list; } -bool MediaModel::loadSearch(const QString &category, const QString &term) -{ - SONOS::LockGuard lock(m_lock); - m_searchCategory = category.toUtf8().constData(); - m_searchTerm = term.toUtf8().constData(); - m_searching = true; // enable search state - m_path.clear(); - m_path.push(Path("", SEARCH_TAG, ROOT_DISPLAY_TYPE)); - emit pathChanged(); - return search(); -} - -bool MediaModel::search() -{ - if (!m_smapi) - return false; - - SONOS::SMAPIMetadata meta; - if (!m_smapi->Search(m_searchCategory, m_searchTerm, 0, LOAD_BULKSIZE, meta)) - { - emit totalCountChanged(); - if (m_smapi->AuthTokenExpired()) - emit authStatusChanged(); - return false; - } - clear(); - m_totalCount = meta.TotalCount(); - m_nextIndex = meta.ItemCount(); - emit totalCountChanged(); - SONOS::SMAPIItemList list = meta.GetItems(); - for (SONOS::SMAPIItemList::const_iterator it = list.begin(); it != list.end(); ++it) - { - MediaItem* item = new MediaItem(*it); - if (item->isValid()) - addItem(item); - else - delete item; - } - return m_loaded = true; -} - bool MediaModel::isAuthExpired() const { return (m_smapi ? m_smapi->AuthTokenExpired() : false); @@ -525,7 +423,254 @@ MediaAuth* MediaModel::getDeviceAuth() bool MediaModel::asyncLoad() { if (m_provider) + { m_provider->runModelLoader(this); + return true; + } + return false; +} + +bool MediaModel::loadMoreData() +{ + SONOS::LockGuard lock(m_lock); + if (!m_smapi) + { + emit loadedMore(false); + return false; + } + // At end return false + if (m_nextIndex >= m_totalCount) + { + emit loadedMore(false); + return false; + } + + SONOS::SMAPIMetadata meta; + // browse or search for next items depending of current state + if ((!m_searching && !m_smapi->GetMetadata(pathId().toUtf8().constData(), m_nextIndex, LOAD_BULKSIZE, false, meta)) || + (m_searching && !m_smapi->Search(m_searchCategory, m_searchTerm, m_nextIndex, LOAD_BULKSIZE, meta))) + { + if (m_smapi->AuthTokenExpired()) + emit authStatusChanged(); + emit loaded(false); + return false; + } + if (m_totalCount != meta.TotalCount()) + { + m_totalCount = meta.TotalCount(); + emit totalCountChanged(); + } + m_nextIndex += meta.ItemCount(); + + SONOS::SMAPIItemList list= meta.GetItems(); + for (SONOS::SMAPIItemList::const_iterator it = list.begin(); it != list.end(); ++it) + { + MediaItem* item = new MediaItem(*it); + if (item->isValid()) + m_data << item; + else + { + delete item; + // Also decrease total count + if (m_totalCount) { + --m_totalCount; + emit totalCountChanged(); + } + } + } + m_dataState = ListModel::Loaded; + emit loadedMore(true); + return true; +} + +bool MediaModel::asyncLoadMore() +{ + if (!m_provider) + return false; + m_provider->runCustomizedModelLoader(this, 1); + return true; +} + +bool MediaModel::loadChild(const QString& id, const QString& title, int displayType, int viewIndex /*= 0*/) +{ + if (id.isEmpty()) + return false; + SONOS::LockGuard lock(m_lock); + // save current view index for this path item + if (!m_path.empty()) + m_path.top().viewIndex = viewIndex; + m_path.push(Path(id, title, displayType)); + emit pathChanged(); + return loadData(); +} + +bool MediaModel::asyncLoadChild(const QString &id, const QString &title, int displayType, int viewIndex /*= 0*/) +{ + if (id.isEmpty()) + return false; + { + SONOS::LockGuard lock(m_lock); + // save current view index for this path item + if (!m_path.empty()) + m_path.top().viewIndex = viewIndex; + m_path.push(Path(id, title, displayType)); + emit pathChanged(); + } + return asyncLoad(); +} + +bool MediaModel::loadParent() +{ + SONOS::LockGuard lock(m_lock); + if (!m_path.empty()) + m_path.pop(); + // reload current search else the parent item + if (pathName() == SEARCH_TAG) + { + m_searching = true; // reset state before signal the change + emit pathChanged(); + return search(); + } + else + { + m_searching = false; // reset state before signal the change + emit pathChanged(); + return loadData(); + } +} + +bool MediaModel::asyncLoadParent() +{ + if (!m_provider) + return false; + m_provider->runCustomizedModelLoader(this, 2); + return true; +} + +bool MediaModel::loadSearch(const QString &category, const QString &term) +{ + SONOS::LockGuard lock(m_lock); + m_searchCategory = category.toUtf8().constData(); + m_searchTerm = term.toUtf8().constData(); + m_searching = true; // enable search state + m_path.clear(); + m_path.push(Path("", SEARCH_TAG, ROOT_DISPLAY_TYPE)); + emit pathChanged(); + return search(); +} + +bool MediaModel::asyncLoadSearch(const QString &category, const QString &term) +{ + { + SONOS::LockGuard lock(m_lock); + m_searchCategory = category.toUtf8().constData(); + m_searchTerm = term.toUtf8().constData(); + m_searching = true; // enable search state + m_path.clear(); + m_path.push(Path("", SEARCH_TAG, ROOT_DISPLAY_TYPE)); + emit pathChanged(); + } + if (!m_provider) + return false; + m_provider->runCustomizedModelLoader(this, 3); + return true; +} + +bool MediaModel::search() +{ + if (!m_smapi) + { + emit loaded(false); + return false; + } + + SONOS::SMAPIMetadata meta; + if (!m_smapi->Search(m_searchCategory, m_searchTerm, 0, LOAD_BULKSIZE, meta)) + { + emit totalCountChanged(); + if (m_smapi->AuthTokenExpired()) + emit authStatusChanged(); + emit loaded(false); + return false; + } + clearData(); + m_dataState = ListModel::NoData; + m_totalCount = meta.TotalCount(); + m_nextIndex = meta.ItemCount(); + SONOS::SMAPIItemList list = meta.GetItems(); + for (SONOS::SMAPIItemList::const_iterator it = list.begin(); it != list.end(); ++it) + { + MediaItem* item = new MediaItem(*it); + if (item->isValid()) + m_data << item; + else + { + delete item; + // Also decrease total count + if (m_totalCount > 0) + --m_totalCount; + } + } + emit totalCountChanged(); + m_dataState = ListModel::Loaded; + emit loaded(true); + return true; +} + +void MediaModel::resetModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (MediaItem* item, m_data) + m_items << item; + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + endResetModel(); + } + emit countChanged(); +} + +void MediaModel::appendModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + int cnt = m_items.count(); + beginInsertRows(QModelIndex(), cnt, cnt + m_data.count()-1); + foreach (MediaItem* item, m_data) + m_items << item; + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + } + emit countChanged(); +} + +bool MediaModel::customizedLoad(int id) +{ + switch (id) + { + case 0: + return loadData(); + case 1: + return loadMoreData(); + case 2: + return loadParent(); + case 3: + return search(); + default: + return false; + } } void MediaModel::handleDataUpdate() diff --git a/backend/modules/NosonApp/mediamodel.h b/backend/modules/NosonApp/mediamodel.h index 325ea775..126e2ea7 100644 --- a/backend/modules/NosonApp/mediamodel.h +++ b/backend/modules/NosonApp/mediamodel.h @@ -33,17 +33,20 @@ class MediaType : public QObject Q_OBJECT Q_ENUMS(itemType) - public: - enum itemType - { - unknown = 0, - album = 1, - person = 2, - genre = 3, - playlist = 4, - audioItem = 5, - folder = 6, - }; +public: + enum itemType + { + unknown = 0, + album = 1, + person = 2, + genre = 3, + playlist = 4, + audioItem = 5, + folder = 6, + }; + + MediaType(QObject* parent) + : QObject(parent) {} }; class MediaItem @@ -81,9 +84,9 @@ class MediaItem const QString& objectId() const { return m_objectId; } - const int displayType() const { return m_displayType; } + int displayType() const { return m_displayType; } - const bool isContainer() const { return m_isContainer; } + bool isContainer() const { return m_isContainer; } private: SONOS::DigitalItemPtr m_ptr; @@ -172,20 +175,14 @@ class MediaModel : public QAbstractListModel, public ListModel Q_INVOKABLE bool init(QObject* sonos, const QVariant& service, bool fill = false); - Q_INVOKABLE void clear(); + Q_INVOKABLE void clearData(); - Q_INVOKABLE bool load(); + Q_INVOKABLE bool loadData(); int totalCount() const { return m_totalCount; } bool isRoot() const { return (m_path.empty()); } - Q_INVOKABLE bool loadMore(); - - Q_INVOKABLE bool loadChild(const QString& id, const QString& title, int displayType, int viewIndex = 0); - - Q_INVOKABLE bool loadParent(); - Q_INVOKABLE QString pathName() const; Q_INVOKABLE QString pathId() const; @@ -196,8 +193,6 @@ class MediaModel : public QAbstractListModel, public ListModel Q_INVOKABLE QList listSearchCategories() const; - Q_INVOKABLE bool loadSearch(const QString& category, const QString& term); - bool isAuthExpired() const; int policyAuth() const; @@ -218,6 +213,28 @@ class MediaModel : public QAbstractListModel, public ListModel Q_INVOKABLE bool asyncLoad(); + virtual bool loadMoreData(); + + Q_INVOKABLE bool asyncLoadMore(); + + virtual bool loadChild(const QString& id, const QString& title, int displayType, int viewIndex = 0); + + Q_INVOKABLE bool asyncLoadChild(const QString& id, const QString& title, int displayType, int viewIndex = 0); + + virtual bool loadParent(); + + Q_INVOKABLE bool asyncLoadParent(); + + virtual bool loadSearch(const QString& category, const QString& term); + + Q_INVOKABLE bool asyncLoadSearch(const QString& category, const QString& term); + + Q_INVOKABLE void resetModel(); + + Q_INVOKABLE void appendModel(); + + virtual bool customizedLoad(int id); + virtual void handleDataUpdate(); Q_INVOKABLE int containerUpdateID() { return m_updateID; } @@ -228,12 +245,15 @@ class MediaModel : public QAbstractListModel, public ListModel void totalCountChanged(); void pathChanged(); void authStatusChanged(); + void loaded(bool succeeded); + void loadedMore(bool succeeded); protected: QHash roleNames() const; private: QList m_items; + QList m_data; SONOS::SMAPI* m_smapi; SONOS::SMOAKeyring::Credentials m_auth; diff --git a/backend/modules/NosonApp/player.cpp b/backend/modules/NosonApp/player.cpp index a9176b44..829abc3f 100644 --- a/backend/modules/NosonApp/player.cpp +++ b/backend/modules/NosonApp/player.cpp @@ -21,7 +21,7 @@ #include "player.h" #include "sonos.h" #include "tools.h" -#include "lib/noson/noson/src/private/debug.h" +#include "../../lib/noson/noson/src/private/debug.h" #include // for sscanf #include @@ -72,6 +72,16 @@ bool Player::init(QObject* sonos) return false; } +void Player::beginJob() +{ + m_sonos->beginJob(); +} + +void Player::endJob() +{ + m_sonos->endJob(); +} + void Player::renewSubscriptions() { if (m_player) @@ -111,6 +121,31 @@ int Player::remainingSleepTimerDuration() return 0; } +class playSourceWorker : public SONOS::OS::CWorker +{ +public: + playSourceWorker(Player& player, const QVariant& payload) + : m_player(player) + , m_payload(payload) + { } + + virtual void Process() + { + m_player.beginJob(); + if (!m_player.setSource(m_payload) || !m_player.play()) + emit m_player.jobFailed(); + m_player.endJob(); + } +private: + Player& m_player; + QVariant m_payload; +}; + +bool Player::startPlaySource(const QVariant& payload) +{ + return m_sonos->startJob(new playSourceWorker(*this, payload)); +} + bool Player::play() { return m_player ? m_player->Play() : false; @@ -217,6 +252,33 @@ bool Player::toggleMute(const QString& uuid) return false; } +class playStreamWorker : public SONOS::OS::CWorker +{ +public: + playStreamWorker(Player& player, const QString& url, const QString& title) + : m_player(player) + , m_url(url) + , m_title(title) + { } + + virtual void Process() + { + m_player.beginJob(); + if (!m_player.playStream(m_url, m_title)) + emit m_player.jobFailed(); + m_player.endJob(); + } +private: + Player& m_player; + const QString m_url; + const QString m_title; +}; + +bool Player::startPlayStream(const QString& url, const QString& title) +{ + return m_sonos->startJob(new playStreamWorker(*this, url, title)); +} + bool Player::playStream(const QString& url, const QString& title) { if (m_player) @@ -341,6 +403,31 @@ bool Player::destroyFavorite(const QString& FVid) return m_player ? m_player->DestroyFavorite(FVid.toUtf8().constData()) : false; } +class playFavoriteWorker : public SONOS::OS::CWorker +{ +public: + playFavoriteWorker(Player& player, const QVariant& payload) + : m_player(player) + , m_payload(payload) + { } + + virtual void Process() + { + m_player.beginJob(); + if (!m_player.playFavorite(m_payload)) + emit m_player.jobFailed(); + m_player.endJob(); + } +private: + Player& m_player; + QVariant m_payload; +}; + +bool Player::startPlayFavorite(const QVariant& payload) +{ + return m_sonos->startJob(new playFavoriteWorker(*this, payload)); +} + bool Player::playFavorite(const QVariant& payload) { SONOS::DigitalItemPtr favorite(payload.value()); diff --git a/backend/modules/NosonApp/player.h b/backend/modules/NosonApp/player.h index 1650549a..069246fa 100644 --- a/backend/modules/NosonApp/player.h +++ b/backend/modules/NosonApp/player.h @@ -21,6 +21,7 @@ #ifndef PLAYER_H #define PLAYER_H +#include "../../lib/noson/noson/src/private/os/threads/threadpool.h" #include "../../lib/noson/noson/src/sonosplayer.h" #include @@ -53,6 +54,8 @@ class Player : public QObject Q_INVOKABLE bool init(QObject* sonos); bool connected() const { return m_connected; } + void beginJob(); + void endJob(); Q_INVOKABLE void renewSubscriptions(); Q_INVOKABLE bool ping(); @@ -60,6 +63,7 @@ class Player : public QObject Q_INVOKABLE bool configureSleepTimer(int seconds); Q_INVOKABLE int remainingSleepTimerDuration(); + Q_INVOKABLE bool startPlaySource(const QVariant& payload); // asynchronous Q_INVOKABLE bool play(); Q_INVOKABLE bool stop(); Q_INVOKABLE bool pause(); @@ -71,6 +75,7 @@ class Player : public QObject Q_INVOKABLE bool toggleMute(); Q_INVOKABLE bool toggleMute(const QString& uuid); + Q_INVOKABLE bool startPlayStream(const QString& url, const QString& title); // asynchonous Q_INVOKABLE bool playStream(const QString& url, const QString& title); Q_INVOKABLE bool playLineIN(); Q_INVOKABLE bool playDigitalIN(); @@ -92,6 +97,7 @@ class Player : public QObject Q_INVOKABLE bool addItemToFavorites(const QVariant& payload, const QString& description, const QString& artURI); Q_INVOKABLE bool destroyFavorite(const QString& FVid); + Q_INVOKABLE bool startPlayFavorite(const QVariant& payload); // asynchronous Q_INVOKABLE bool playFavorite(const QVariant& payload); bool muteMaster() const { return m_RCGroup.mute; } @@ -132,6 +138,7 @@ class Player : public QObject QString playMode() const { return QString::fromUtf8(m_AVTProperty.CurrentPlayMode.c_str()); } signals: + void jobFailed(); void connectedChanged(); void renderingChanged(); void renderingGroupChanged(); diff --git a/backend/modules/NosonApp/playlistsmodel.cpp b/backend/modules/NosonApp/playlistsmodel.cpp index 1b378ece..9a0fe22a 100644 --- a/backend/modules/NosonApp/playlistsmodel.cpp +++ b/backend/modules/NosonApp/playlistsmodel.cpp @@ -56,7 +56,9 @@ PlaylistsModel::PlaylistsModel(QObject* parent) PlaylistsModel::~PlaylistsModel() { - clear(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); } void PlaylistsModel::addItem(PlaylistItem* item) @@ -142,29 +144,32 @@ bool PlaylistsModel::init(QObject* sonos, const QString& root, bool fill) return ListModel::init(sonos, _root, fill); } -void PlaylistsModel::clear() +void PlaylistsModel::clearData() { - { - SONOS::LockGuard lock(m_lock); - beginRemoveRows(QModelIndex(), 0, m_items.count()); - qDeleteAll(m_items); - m_items.clear(); - endRemoveRows(); - } - emit countChanged(); + SONOS::LockGuard lock(m_lock); + qDeleteAll(m_data); + m_data.clear(); } -bool PlaylistsModel::load() +bool PlaylistsModel::loadData() { setUpdateSignaled(false); if (!m_provider) + { + emit loaded(false); return false; - clear(); + } const SONOS::PlayerPtr player = m_provider->getPlayer(); if (!player) + { + emit loaded(false); return false; + } + SONOS::LockGuard lock(m_lock); + clearData(); + m_dataState = ListModel::NoData; QString port; port.setNum(player->GetPort()); QString url = "http://"; @@ -176,14 +181,19 @@ bool PlaylistsModel::load() { PlaylistItem* item = new PlaylistItem(*it, url); if (item->isValid()) - addItem(item); + m_data << item; else delete item; } if (cl.failure()) - return m_loaded = false; + { + emit loaded(false); + return false; + } m_updateID = cl.GetUpdateID(); // sync new baseline - return m_loaded = true; + m_dataState = ListModel::Loaded; + emit loaded(true); + return true; } void PlaylistsModel::handleDataUpdate() @@ -195,8 +205,34 @@ void PlaylistsModel::handleDataUpdate() } } +void PlaylistsModel::resetModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (PlaylistItem* item, m_data) + m_items << item; + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + endResetModel(); + } + emit countChanged(); +} + bool PlaylistsModel::asyncLoad() { if (m_provider) + { m_provider->runModelLoader(this); + return true; + } + return false; } diff --git a/backend/modules/NosonApp/playlistsmodel.h b/backend/modules/NosonApp/playlistsmodel.h index 416738d2..68ecfbe4 100644 --- a/backend/modules/NosonApp/playlistsmodel.h +++ b/backend/modules/NosonApp/playlistsmodel.h @@ -42,7 +42,7 @@ class PlaylistItem int artsCount() const { return m_arts.size(); } - QString art(unsigned index) const { return (artsCount() > index ? m_arts[index] : ""); } + QString art(int index) const { return (m_arts.size() > index ? m_arts[index] : ""); } QStringList arts() const { return QStringList(m_arts); } @@ -86,12 +86,16 @@ class PlaylistsModel : public QAbstractListModel, public ListModel Q_INVOKABLE bool init(QObject* sonos, const QString& root, bool fill = false); - Q_INVOKABLE void clear(); + virtual void clearData(); - Q_INVOKABLE bool load(); + virtual bool loadData(); Q_INVOKABLE bool asyncLoad(); + Q_INVOKABLE void resetModel(); + + Q_INVOKABLE void appendModel() { } + virtual void handleDataUpdate(); Q_INVOKABLE int containerUpdateID() { return m_updateID; } @@ -99,12 +103,14 @@ class PlaylistsModel : public QAbstractListModel, public ListModel signals: void dataUpdated(); void countChanged(); + void loaded(bool succeeded); protected: QHash roleNames() const; private: QList m_items; + QList m_data; }; diff --git a/backend/modules/NosonApp/queuemodel.cpp b/backend/modules/NosonApp/queuemodel.cpp index 39f67dd6..9347041f 100644 --- a/backend/modules/NosonApp/queuemodel.cpp +++ b/backend/modules/NosonApp/queuemodel.cpp @@ -29,7 +29,9 @@ QueueModel::QueueModel(QObject* parent) QueueModel::~QueueModel() { - clear(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); } void QueueModel::addItem(TrackItem* item) @@ -80,6 +82,23 @@ QVariant QueueModel::data(const QModelIndex& index, int role) const } } +bool QueueModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + SONOS::LockGuard lock(m_lock); + if (index.row() < 0 || index.row() >= m_items.count()) + return false; + + TrackItem* item = m_items[index.row()]; + switch (role) + { + case ArtRole: + item->setArt(value.toString()); + return true; + default: + return false; + } +} + QHash QueueModel::roleNames() const { QHash roles; @@ -123,29 +142,32 @@ bool QueueModel::init(QObject* sonos, const QString& root, bool fill) return ListModel::init(sonos, _root, fill); } -void QueueModel::clear() +void QueueModel::clearData() { - { - SONOS::LockGuard lock(m_lock); - beginRemoveRows(QModelIndex(), 0, m_items.count()); - qDeleteAll(m_items); - m_items.clear(); - endRemoveRows(); - } - emit countChanged(); + SONOS::LockGuard lock(m_lock); + qDeleteAll(m_data); + m_data.clear(); } -bool QueueModel::load() +bool QueueModel::loadData() { setUpdateSignaled(false); if (!m_provider) + { + emit loaded(false); return false; - clear(); + } const SONOS::PlayerPtr player = m_provider->getPlayer(); if (!player) + { + emit loaded(false); return false; + } + SONOS::LockGuard lock(m_lock); + clearData(); + m_dataState = ListModel::NoData; QString port; port.setNum(player->GetPort()); QString url = "http://"; @@ -156,18 +178,49 @@ bool QueueModel::load() for (SONOS::ContentList::iterator it = cl.begin(); it != cl.end(); ++it) { TrackItem* item = new TrackItem(*it, url); - addItem(item); + m_data << item; } if (cl.failure()) - return m_loaded = false; + { + emit loaded(false); + return false; + } m_updateID = cl.GetUpdateID(); // sync new baseline - return m_loaded = true; + m_dataState = ListModel::Loaded; + emit loaded(true); + return true; } bool QueueModel::asyncLoad() { if (m_provider) + { m_provider->runModelLoader(this); + return true; + } + return false; +} + +void QueueModel::resetModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (TrackItem* item, m_data) + m_items << item; + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + endResetModel(); + } + emit countChanged(); } void QueueModel::handleDataUpdate() diff --git a/backend/modules/NosonApp/queuemodel.h b/backend/modules/NosonApp/queuemodel.h index 68b725c2..58c32362 100644 --- a/backend/modules/NosonApp/queuemodel.h +++ b/backend/modules/NosonApp/queuemodel.h @@ -51,16 +51,22 @@ class QueueModel : public QAbstractListModel, public ListModel QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole); + Q_INVOKABLE QVariantMap get(int row); Q_INVOKABLE bool init(QObject* sonos, const QString& root, bool fill = false); - Q_INVOKABLE void clear(); + virtual void clearData(); - Q_INVOKABLE bool load(); + virtual bool loadData(); Q_INVOKABLE bool asyncLoad(); + Q_INVOKABLE void resetModel(); + + Q_INVOKABLE void appendModel() { } + virtual void handleDataUpdate(); Q_INVOKABLE int containerUpdateID() { return m_updateID; } @@ -68,12 +74,14 @@ class QueueModel : public QAbstractListModel, public ListModel signals: void dataUpdated(); void countChanged(); + void loaded(bool succeeded); protected: QHash roleNames() const; private: QList m_items; + QList m_data; }; #endif /* QUEUEMODEL_H */ diff --git a/backend/modules/NosonApp/radiosmodel.cpp b/backend/modules/NosonApp/radiosmodel.cpp index d9f3f76e..60b91212 100644 --- a/backend/modules/NosonApp/radiosmodel.cpp +++ b/backend/modules/NosonApp/radiosmodel.cpp @@ -30,6 +30,7 @@ RadioItem::RadioItem(const SONOS::DigitalItemPtr& ptr, const QString& baseURL) : m_ptr(ptr) , m_valid(false) { + (void)baseURL; m_id = QString::fromUtf8(ptr->GetObjectID().c_str()); if (ptr->subType() == SONOS::DigitalItem::SubType_audioItem) { @@ -50,6 +51,7 @@ RadioItem::RadioItem(const SONOS::DigitalItemPtr& ptr, const QString& baseURL) char* end = beg; while (isdigit(*(++end))); m_streamId = QString::fromUtf8(beg, end - beg); + m_icon = QString("http://cdn-radiotime-logos.tunein.com/").append(m_streamId).append("q.png"); } } } @@ -68,7 +70,9 @@ RadiosModel::RadiosModel(QObject* parent) RadiosModel::~RadiosModel() { - clear(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); } void RadiosModel::addItem(RadioItem* item) @@ -117,6 +121,23 @@ QVariant RadiosModel::data(const QModelIndex& index, int role) const } } +bool RadiosModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + SONOS::LockGuard lock(m_lock); + if (index.row() < 0 || index.row() >= m_items.count()) + return false; + + RadioItem* item = m_items[index.row()]; + switch (role) + { + case IconRole: + item->setIcon(value.toString()); + return true; + default: + return false; + } +} + QHash RadiosModel::roleNames() const { QHash roles; @@ -158,29 +179,32 @@ bool RadiosModel::init(QObject* sonos, const QString& root, bool fill) return ListModel::init(sonos, _root, fill); } -void RadiosModel::clear() +void RadiosModel::clearData() { - { - SONOS::LockGuard lock(m_lock); - beginRemoveRows(QModelIndex(), 0, m_items.count()); - qDeleteAll(m_items); - m_items.clear(); - endRemoveRows(); - } - emit countChanged(); + SONOS::LockGuard lock(m_lock); + qDeleteAll(m_data); + m_data.clear(); } -bool RadiosModel::load() +bool RadiosModel::loadData() { setUpdateSignaled(false); if (!m_provider) + { + emit loaded(false); return false; - clear(); + } const SONOS::PlayerPtr player = m_provider->getPlayer(); if (!player) + { + emit loaded(false); return false; + } + SONOS::LockGuard lock(m_lock); + clearData(); + m_dataState = ListModel::NoData; QString port; port.setNum(player->GetPort()); QString url = "http://"; @@ -192,20 +216,51 @@ bool RadiosModel::load() { RadioItem* item = new RadioItem(*it, url); if (item->isValid()) - addItem(item); + m_data << item; else delete item; } if (cl.failure()) - return m_loaded = false; + { + emit loaded(false); + return false; + } m_updateID = cl.GetUpdateID(); // sync new baseline - return m_loaded = true; + m_dataState = ListModel::Loaded; + emit loaded(true); + return true; } bool RadiosModel::asyncLoad() { if (m_provider) + { m_provider->runModelLoader(this); + return true; + } + return false; +} + +void RadiosModel::resetModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (RadioItem* item, m_data) + m_items << item; + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + endResetModel(); + } + emit countChanged(); } void RadiosModel::handleDataUpdate() diff --git a/backend/modules/NosonApp/radiosmodel.h b/backend/modules/NosonApp/radiosmodel.h index 9b111cd4..26f32037 100644 --- a/backend/modules/NosonApp/radiosmodel.h +++ b/backend/modules/NosonApp/radiosmodel.h @@ -48,6 +48,8 @@ class RadioItem const QString& normalized() const { return m_normalized; } + void setIcon(const QString& icon) { m_icon = icon; } + private: SONOS::DigitalItemPtr m_ptr; bool m_valid; @@ -85,16 +87,22 @@ class RadiosModel : public QAbstractListModel, public ListModel QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole); + Q_INVOKABLE QVariantMap get(int row); Q_INVOKABLE bool init(QObject* sonos, const QString& root, bool fill = false); - Q_INVOKABLE void clear(); + virtual void clearData(); - Q_INVOKABLE bool load(); + virtual bool loadData(); Q_INVOKABLE bool asyncLoad(); + Q_INVOKABLE void resetModel(); + + Q_INVOKABLE void appendModel() { } + virtual void handleDataUpdate(); Q_INVOKABLE int containerUpdateID() { return m_updateID; } @@ -102,12 +110,14 @@ class RadiosModel : public QAbstractListModel, public ListModel signals: void dataUpdated(); void countChanged(); + void loaded(bool succeeded); protected: QHash roleNames() const; private: QList m_items; + QList m_data; }; #endif /* RADIOSMODEL_H */ diff --git a/backend/modules/NosonApp/renderingmodel.cpp b/backend/modules/NosonApp/renderingmodel.cpp index 44c8b0db..f43d9c8d 100644 --- a/backend/modules/NosonApp/renderingmodel.cpp +++ b/backend/modules/NosonApp/renderingmodel.cpp @@ -37,7 +37,9 @@ RenderingModel::RenderingModel(QObject* parent) RenderingModel::~RenderingModel() { - clear(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); } void RenderingModel::addItem(RenderingItem* item) @@ -106,28 +108,49 @@ QHash RenderingModel::roleNames() const return roles; } -void RenderingModel::clear() +void RenderingModel::clearData() { - beginRemoveRows(QModelIndex(), 0, m_items.count()); qDeleteAll(m_items); m_items.clear(); - endRemoveRows(); - emit countChanged(); } -bool RenderingModel::load(QObject* player) +bool RenderingModel::loadData() { - Player* _player = reinterpret_cast (player); - if (!_player) + if (!m_player) return false; - clear(); - const Player::RCTable& tab = _player->renderingTable(); + clearData(); + const Player::RCTable& tab = m_player->renderingTable(); for (Player::RCTable::const_iterator it = tab.begin(); it != tab.end(); ++it) - addItem(new RenderingItem(*it)); + m_data << new RenderingItem(*it); return true; } +bool RenderingModel::load(QObject* player) +{ + m_player = reinterpret_cast (player); + if (!loadData()) + return false; + resetModel(); + return true; +} + +void RenderingModel::resetModel() +{ + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (RenderingItem* item, m_data) + m_items << item; + m_data.clear(); + endInsertRows(); + endResetModel(); + emit countChanged(); +} + void RenderingModel::setVolume(int index, const QVariant& volume) { setData(QAbstractListModel::index(index), volume, VolumeRole); diff --git a/backend/modules/NosonApp/renderingmodel.h b/backend/modules/NosonApp/renderingmodel.h index da679fab..90c4c6f0 100644 --- a/backend/modules/NosonApp/renderingmodel.h +++ b/backend/modules/NosonApp/renderingmodel.h @@ -76,10 +76,14 @@ class RenderingModel : public QAbstractListModel bool setData(const QModelIndex& index, const QVariant& value, int role); - Q_INVOKABLE void clear(); + virtual void clearData(); + + virtual bool loadData(); Q_INVOKABLE bool load(QObject* player); + virtual void resetModel(); + Q_INVOKABLE void setVolume(int index, const QVariant& volume); Q_INVOKABLE void setMute(int index, const QVariant& mute); @@ -92,6 +96,8 @@ class RenderingModel : public QAbstractListModel private: QList m_items; + QList m_data; + Player* m_player; }; #endif /* RENDERINGMODEL_H */ diff --git a/backend/modules/NosonApp/roomsmodel.cpp b/backend/modules/NosonApp/roomsmodel.cpp index bc6061a6..46187b59 100644 --- a/backend/modules/NosonApp/roomsmodel.cpp +++ b/backend/modules/NosonApp/roomsmodel.cpp @@ -42,23 +42,15 @@ QVariant RoomItem::payload() const RoomsModel::RoomsModel(QObject* parent) : QAbstractListModel(parent) +, m_zoneId("") { } RoomsModel::~RoomsModel() { - clear(); -} - -void RoomsModel::addItem(RoomItem* item) -{ - { - SONOS::LockGuard lock(m_lock); - beginInsertRows(QModelIndex(), rowCount(), rowCount()); - m_items << item; - endInsertRows(); - } - emit countChanged(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); } int RoomsModel::rowCount(const QModelIndex& parent) const @@ -103,7 +95,6 @@ QHash RoomsModel::roleNames() const QVariantMap RoomsModel::get(int row) { - SONOS::LockGuard lock(m_lock); if (row < 0 || row >= m_items.count()) return QVariantMap(); const RoomItem* item = m_items[row]; @@ -117,71 +108,82 @@ QVariantMap RoomsModel::get(int row) return model; } -bool RoomsModel::init(QObject* sonos, bool fill) -{ - return ListModel::init(sonos, "", fill); -} - -void RoomsModel::clear() +void RoomsModel::clearData() { - { - SONOS::LockGuard lock(m_lock); - beginRemoveRows(QModelIndex(), 0, m_items.count()); - qDeleteAll(m_items); - m_items.clear(); - endRemoveRows(); - } - emit countChanged(); + qDeleteAll(m_data); + m_data.clear(); } -bool RoomsModel::load() +bool RoomsModel::loadData() { - setUpdateSignaled(false); - if (!m_provider) return false; - clear(); - SONOS::ZonePlayerList zonePlayers = m_provider->getSystem().GetZonePlayerList(); - for (SONOS::ZonePlayerList::iterator it = zonePlayers.begin(); it != zonePlayers.end(); ++it) - { - RoomItem* item = new RoomItem(it->second); - if (item->isValid()) - addItem(item); - else - delete item; - } - return m_loaded = true; -} - -bool RoomsModel::load(const QString& zoneId) -{ - setUpdateSignaled(false); + clearData(); - if (!m_provider) - return false; - clear(); - SONOS::ZoneList zones = m_provider->getSystem().GetZoneList(); - SONOS::ZoneList::const_iterator itz = zones.find(zoneId.toUtf8().constData()); - if (itz != zones.end()) + if (m_zoneId.isNull()) { - for (std::vector::iterator it = itz->second->begin(); it != itz->second->end(); ++it) + SONOS::ZonePlayerList zonePlayers = m_provider->getSystem().GetZonePlayerList(); + for (SONOS::ZonePlayerList::iterator it = zonePlayers.begin(); it != zonePlayers.end(); ++it) { - RoomItem* item = new RoomItem(*it); + RoomItem* item = new RoomItem(it->second); if (item->isValid()) - addItem(item); + m_data << item; else delete item; } } - return m_loaded = true; + else + { + SONOS::ZoneList zones = m_provider->getSystem().GetZoneList(); + SONOS::ZoneList::const_iterator itz = zones.find(m_zoneId.toUtf8().constData()); + if (itz != zones.end()) + { + for (std::vector::iterator it = itz->second->begin(); it != itz->second->end(); ++it) + { + RoomItem* item = new RoomItem(*it); + if (item->isValid()) + m_data << item; + else + delete item; + } + } + } + return true; } -void RoomsModel::handleDataUpdate() +bool RoomsModel::load(QObject* sonos) { - if (!updateSignaled()) - { - setUpdateSignaled(true); - dataUpdated(); - } + m_provider = reinterpret_cast (sonos); + m_zoneId = QString(); + if (!loadData()) + return false; + resetModel(); + return true; +} + +bool RoomsModel::load(QObject* sonos, const QString& zoneId) +{ + m_provider = reinterpret_cast (sonos); + m_zoneId = zoneId; + if (!loadData()) + return false; + resetModel(); + return true; +} + +void RoomsModel::resetModel() +{ + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (RoomItem* item, m_data) + m_items << item; + m_data.clear(); + endInsertRows(); + endResetModel(); + emit countChanged(); } diff --git a/backend/modules/NosonApp/roomsmodel.h b/backend/modules/NosonApp/roomsmodel.h index ab0e872c..275c3cca 100644 --- a/backend/modules/NosonApp/roomsmodel.h +++ b/backend/modules/NosonApp/roomsmodel.h @@ -55,7 +55,7 @@ class RoomItem }; -class RoomsModel : public QAbstractListModel, public ListModel +class RoomsModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(int count READ rowCount NOTIFY countChanged) @@ -81,18 +81,17 @@ class RoomsModel : public QAbstractListModel, public ListModel Q_INVOKABLE QVariantMap get(int row); - Q_INVOKABLE bool init(QObject* sonos, bool fill = false); + virtual void clearData(); - Q_INVOKABLE void clear(); + virtual bool loadData(); - Q_INVOKABLE bool load(); + Q_INVOKABLE bool load(QObject* sonos); - Q_INVOKABLE bool load(const QString& zoneId); + Q_INVOKABLE bool load(QObject* sonos, const QString& zoneId); - virtual void handleDataUpdate(); + virtual void resetModel(); signals: - void dataUpdated(); void countChanged(); protected: @@ -100,6 +99,9 @@ class RoomsModel : public QAbstractListModel, public ListModel private: QList m_items; + QList m_data; + Sonos* m_provider; + QString m_zoneId; }; #endif /* ROOMSMODEL_H */ diff --git a/backend/modules/NosonApp/servicesmodel.cpp b/backend/modules/NosonApp/servicesmodel.cpp index fe49d739..f8d21de0 100644 --- a/backend/modules/NosonApp/servicesmodel.cpp +++ b/backend/modules/NosonApp/servicesmodel.cpp @@ -51,7 +51,9 @@ ServicesModel::ServicesModel(QObject* parent) ServicesModel::~ServicesModel() { - clear(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); } void ServicesModel::addItem(ServiceItem* item) @@ -127,49 +129,76 @@ QVariantMap ServicesModel::get(int row) return model; } -bool ServicesModel::init(QObject* sonos, bool fill) +void ServicesModel::clearData() { - return ListModel::init(sonos, "", fill); -} - -void ServicesModel::clear() -{ - { - SONOS::LockGuard lock(m_lock); - beginRemoveRows(QModelIndex(), 0, m_items.count()); - qDeleteAll(m_items); - m_items.clear(); - endRemoveRows(); - } - emit countChanged(); + SONOS::LockGuard lock(m_lock); + qDeleteAll(m_data); + m_data.clear(); } -bool ServicesModel::load() +bool ServicesModel::loadData() { setUpdateSignaled(false); if (!m_provider) + { + emit loaded(false); return false; - clear(); + } const SONOS::PlayerPtr player = m_provider->getPlayer(); if (!player) + { + emit loaded(false); return false; + } + + SONOS::LockGuard lock(m_lock); + clearData(); + m_dataState = ListModel::NoData; SONOS::SMServiceList list = player->GetAvailableServices(); for (SONOS::SMServiceList::const_iterator it = list.begin(); it != list.end(); ++it) { ServiceItem* item = new ServiceItem(*it); if (item->isValid()) - addItem(item); + m_data << item; else delete item; } - return m_loaded = true; + m_dataState = ListModel::Loaded; + emit loaded(true); + return true; } bool ServicesModel::asyncLoad() { if (m_provider) + { m_provider->runModelLoader(this); + return true; + } + return false; +} + +void ServicesModel::resetModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (ServiceItem* item, m_data) + m_items << item; + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + endResetModel(); + } + emit countChanged(); } void ServicesModel::handleDataUpdate() diff --git a/backend/modules/NosonApp/servicesmodel.h b/backend/modules/NosonApp/servicesmodel.h index 722e7ea5..6aae5d16 100644 --- a/backend/modules/NosonApp/servicesmodel.h +++ b/backend/modules/NosonApp/servicesmodel.h @@ -84,25 +84,29 @@ class ServicesModel : public QAbstractListModel, public ListModel Q_INVOKABLE QVariantMap get(int row); - Q_INVOKABLE bool init(QObject* sonos, bool fill = false); + Q_INVOKABLE bool init(QObject* sonos, bool fill = false) { return ListModel::init(sonos, fill); } - Q_INVOKABLE void clear(); + virtual void clearData(); - Q_INVOKABLE bool load(); + virtual bool loadData(); Q_INVOKABLE bool asyncLoad(); + Q_INVOKABLE void resetModel(); + virtual void handleDataUpdate(); signals: void dataUpdated(); void countChanged(); + void loaded(bool succeeded); protected: QHash roleNames() const; private: QList m_items; + QList m_data; }; #endif /* SERVICESMODEL_H */ diff --git a/backend/modules/NosonApp/sonos.cpp b/backend/modules/NosonApp/sonos.cpp index 445d738b..10c45c3e 100644 --- a/backend/modules/NosonApp/sonos.cpp +++ b/backend/modules/NosonApp/sonos.cpp @@ -25,6 +25,8 @@ #include +#define JOB_THREADPOOL_SIZE 16 + class ContentLoader : public SONOS::OS::CWorker { public: @@ -36,14 +38,37 @@ class ContentLoader : public SONOS::OS::CWorker virtual void Process() { + m_sonos.beginJob(); if (m_payload) m_sonos.loadModel(m_payload); else m_sonos.loadEmptyModels(); + m_sonos.endJob(); + } +private: + Sonos& m_sonos; + ListModel* m_payload; +}; + +class CustomizedContentLoader : public SONOS::OS::CWorker +{ +public: + CustomizedContentLoader(Sonos& sonos, ListModel* payload, int id) + : m_sonos(sonos) + , m_payload(payload) + , m_id(id) { } + + virtual void Process() + { + m_sonos.beginJob(); + if (m_payload) + m_sonos.customizedLoadModel(m_payload, m_id); + m_sonos.endJob(); } private: Sonos& m_sonos; ListModel* m_payload; + int m_id; }; Sonos::Sonos(QObject* parent) @@ -51,7 +76,8 @@ Sonos::Sonos(QObject* parent) , m_library(ManagedContents()) , m_shareUpdateID(0) , m_system(this, topologyEventCB) -, m_threadpool(5) +, m_threadpool(JOB_THREADPOOL_SIZE) +, m_jobCount(SONOS::LockedNumber(0)) , m_locale("en_US") { SONOS::DBGLevel(2); @@ -66,6 +92,27 @@ Sonos::~Sonos() } } +class InitWorker : public SONOS::OS::CWorker +{ +public: + InitWorker(Sonos& sonos, int debug) : m_sonos(sonos), m_debug(debug) { } + + virtual void Process() + { + m_sonos.beginJob(); + emit m_sonos.initDone(m_sonos.init(m_debug)); + m_sonos.endJob(); + } +private: + Sonos& m_sonos; + int m_debug; +}; + +bool Sonos::startInit(int debug) +{ + return m_threadpool.Enqueue(new InitWorker(*this, debug)); +} + bool Sonos::init(int debug) { SONOS::DBGLevel(debug > DBG_INFO ? debug : DBG_INFO); @@ -157,6 +204,55 @@ bool Sonos::joinZone(const QVariant& zonePayload, const QVariant& toZonePayload) } +bool Sonos::joinZones(const QVariantList& zonePayloads, const QVariant& toZonePayload) +{ + std::vector zones; + SONOS::ZonePtr toZone = toZonePayload.value(); + for (QVariantList::const_iterator it = zonePayloads.begin(); it != zonePayloads.end(); ++it) + zones.push_back(it->value()); + if (toZone && toZone->GetCoordinator()) + { + for (std::vector::const_iterator it = zones.begin(); it != zones.end(); ++it) + { + if ((*it)->GetZoneName() == toZone->GetZoneName()) + continue; + for (std::vector::iterator itr = (*it)->begin(); itr != (*it)->end(); ++itr) + { + SONOS::Player player(*itr); + player.JoinToGroup(toZone->GetCoordinator()->GetUUID()); + } + } + return true; + } + return false; +} + +class JoinZonesWorker : public SONOS::OS::CWorker +{ +public: + JoinZonesWorker(Sonos& sonos, const QVariantList& zonePayloads, const QVariant& toZonePayload) + : m_sonos(sonos) + , m_zonePayloads(zonePayloads) + , m_toZonePayload(toZonePayload) + { } + + virtual void Process() + { + m_sonos.beginJob(); + m_sonos.joinZones(m_zonePayloads, m_toZonePayload); + m_sonos.endJob(); + } +private: + Sonos& m_sonos; + QVariantList m_zonePayloads; + QVariant m_toZonePayload; +}; + +bool Sonos::startJoinZones(const QVariantList& zonePayloads, const QVariant& toZonePayload) +{ + return m_threadpool.Enqueue(new JoinZonesWorker(*this, zonePayloads, toZonePayload)); +} + bool Sonos::unjoinRoom(const QVariant& roomPayload) { SONOS::ZonePlayerPtr room = roomPayload.value(); @@ -168,6 +264,45 @@ bool Sonos::unjoinRoom(const QVariant& roomPayload) return false; } +bool Sonos::unjoinRooms(const QVariantList& roomPayloads) +{ + for (QVariantList::const_iterator it = roomPayloads.begin(); it != roomPayloads.end(); ++it) { + SONOS::ZonePlayerPtr room = it->value(); + if (room && room->IsValid()) + { + SONOS::Player player(room); + return player.BecomeStandalone(); + } + else + return false; + } + return true; +} + +class UnjoinRoomsWorker : public SONOS::OS::CWorker +{ +public: + UnjoinRoomsWorker(Sonos& sonos, const QVariantList& roomPayloads) + : m_sonos(sonos) + , m_roomPayloads(roomPayloads) + { } + + virtual void Process() + { + m_sonos.beginJob(); + m_sonos.unjoinRooms(m_roomPayloads); + m_sonos.endJob(); + } +private: + Sonos& m_sonos; + QVariantList m_roomPayloads; +}; + +bool Sonos::startUnjoinRooms(const QVariantList& roomPayloads) +{ + return m_threadpool.Enqueue(new UnjoinRoomsWorker(*this, roomPayloads)); +} + bool Sonos::unjoinZone(const QVariant& zonePayload) { SONOS::ZonePtr zone = zonePayload.value(); @@ -184,7 +319,31 @@ bool Sonos::unjoinZone(const QVariant& zonePayload) } -const SONOS::System& Sonos::getSystem() const +class UnjoinZoneWorker : public SONOS::OS::CWorker +{ +public: + UnjoinZoneWorker(Sonos& sonos, const QVariant& zonePayload) + : m_sonos(sonos) + , m_zonePayload(zonePayload) + { } + + virtual void Process() + { + m_sonos.beginJob(); + m_sonos.unjoinZone(m_zonePayload); + m_sonos.endJob(); + } +private: + Sonos& m_sonos; + QVariant m_zonePayload; +}; + +bool Sonos::startUnjoinZone(const QVariant& zonePayload) +{ + return m_threadpool.Enqueue(new UnjoinZoneWorker(*this, zonePayload)); +} + +const SONOS::System &Sonos::getSystem() const { return m_system; } @@ -205,7 +364,7 @@ void Sonos::loadEmptyModels() { SONOS::Locked::pointer mc = m_library.Get(); for (ManagedContents::iterator it = mc->begin(); it != mc->end(); ++it) - if (!it->model->m_loaded) + if (it->model->m_dataState == ListModel::NoData) left.push_back(qMakePair(it->model, SONOS::LockGuard(it->model->m_lock))); } emit loadingStarted(); @@ -214,7 +373,7 @@ void Sonos::loadEmptyModels() while (!left.isEmpty()) { QPair item = left.front(); - item.first->load(); + item.first->loadData(); left.pop_front(); } } @@ -228,6 +387,8 @@ void Sonos::runModelLoader(ListModel* model) model->m_pending = true; // decline next request m_threadpool.Enqueue(new ContentLoader(*this, model)); } + else + SONOS::DBG(DBG_ERROR, "%s: request has been declined (%p)\n", __FUNCTION__, model); } void Sonos::loadModel(ListModel* model) @@ -248,11 +409,29 @@ void Sonos::loadModel(ListModel* model) SONOS::DBG(DBG_INFO, "%s: %p (%s)\n", __FUNCTION__, item.first, item.first->m_root.toUtf8().constData()); emit loadingStarted(); item.first->m_pending = false; // accept add next request in queue - item.first->load(); + item.first->loadData(); emit loadingFinished(); } } +void Sonos::runCustomizedModelLoader(ListModel* model, int id) +{ + if (model && !model->m_pending) + { + model->m_pending = true; // decline next request + m_threadpool.Enqueue(new CustomizedContentLoader(*this, model, id)); + } + else + SONOS::DBG(DBG_ERROR, "%s: request id %d has been declined (%p)\n", __FUNCTION__, id, model); +} + +void Sonos::customizedLoadModel(ListModel *model, int id) +{ + SONOS::LockGuard guard(model->m_lock); + model->m_pending = false; // accept add next request in queue + model->customizedLoad(id); +} + void Sonos::registerModel(ListModel* model, const QString& root) { if (model) @@ -291,6 +470,23 @@ void Sonos::unregisterModel(ListModel* model) } } +bool Sonos::startJob(SONOS::OS::CWorker* worker) +{ + return m_threadpool.Enqueue(worker); +} + +void Sonos::beginJob() +{ + m_jobCount.Add(1); + emit jobCountChanged(); +} + +void Sonos::endJob() +{ + m_jobCount.Add(-1); + emit jobCountChanged(); +} + void Sonos::playerEventCB(void* handle) { Sonos* sonos = static_cast(handle); diff --git a/backend/modules/NosonApp/sonos.h b/backend/modules/NosonApp/sonos.h index aa6c59da..8b7c7138 100644 --- a/backend/modules/NosonApp/sonos.h +++ b/backend/modules/NosonApp/sonos.h @@ -46,11 +46,13 @@ class Sonos : public QObject { Q_OBJECT + Q_PROPERTY(int jobCount READ jobCount NOTIFY jobCountChanged) public: explicit Sonos(QObject *parent = 0); ~Sonos(); + Q_INVOKABLE bool startInit(int debug = 0); // asynchronous Q_INVOKABLE bool init(int debug = 0); Q_INVOKABLE void setLocale(const QString& locale); @@ -72,10 +74,15 @@ class Sonos : public QObject Q_INVOKABLE bool joinRoom(const QVariant& roomPayload, const QVariant& toZonePayload); Q_INVOKABLE bool joinZone(const QVariant& zonePayload, const QVariant& toZonePayload); + Q_INVOKABLE bool joinZones(const QVariantList& zonePayloads, const QVariant& toZonePayload); + Q_INVOKABLE bool startJoinZones(const QVariantList& zonePayloads, const QVariant& toZonePayload); Q_INVOKABLE bool unjoinRoom(const QVariant& roomPayload); + Q_INVOKABLE bool unjoinRooms(const QVariantList& roomPayloads); + Q_INVOKABLE bool startUnjoinRooms(const QVariantList& roomPayloads); Q_INVOKABLE bool unjoinZone(const QVariant& zonePayload); + Q_INVOKABLE bool startUnjoinZone(const QVariant& zonePayload); const SONOS::System& getSystem() const; const SONOS::PlayerPtr& getPlayer() const; @@ -86,9 +93,17 @@ class Sonos : public QObject void runModelLoader(ListModel* model); void loadModel(ListModel* model); + void runCustomizedModelLoader(ListModel* model, int id); + void customizedLoadModel(ListModel* model, int id); + void registerModel(ListModel* model, const QString& root); void unregisterModel(ListModel* model); + bool startJob(SONOS::OS::CWorker* worker); + int jobCount() { return *(m_jobCount.Get()); } + void beginJob(); + void endJob(); + // Define singleton provider functions static QObject* sonos_provider(QQmlEngine *engine, QJSEngine *scriptEngine) { @@ -160,12 +175,15 @@ class Sonos : public QObject } signals: + void initDone(bool succeeded); void loadingStarted(); void loadingFinished(); void transportChanged(); void renderingControlChanged(); void topologyChanged(); + void jobCountChanged(); + private: struct RegisteredContent { @@ -181,6 +199,7 @@ class Sonos : public QObject SONOS::System m_system; SONOS::OS::CThreadPool m_threadpool; + SONOS::LockedNumber m_jobCount; SONOS::Locked m_locale; // language_COUNTRY diff --git a/backend/modules/NosonApp/tracksmodel.cpp b/backend/modules/NosonApp/tracksmodel.cpp index 63068bda..0cf69842 100644 --- a/backend/modules/NosonApp/tracksmodel.cpp +++ b/backend/modules/NosonApp/tracksmodel.cpp @@ -65,7 +65,9 @@ TracksModel::TracksModel(QObject* parent) TracksModel::~TracksModel() { - clear(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); SAFE_DELETE(m_contentList) SAFE_DELETE(m_contentDirectory); } @@ -118,6 +120,23 @@ QVariant TracksModel::data(const QModelIndex& index, int role) const } } +bool TracksModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + SONOS::LockGuard lock(m_lock); + if (index.row() < 0 || index.row() >= m_items.count()) + return false; + + TrackItem* item = m_items[index.row()]; + switch (role) + { + case ArtRole: + item->setArt(value.toString()); + return true; + default: + return false; + } +} + QHash TracksModel::roleNames() const { QHash roles; @@ -161,86 +180,104 @@ bool TracksModel::init(QObject* sonos, const QString& root, bool fill) return ListModel::init(sonos, _root, fill); } -void TracksModel::clear() +void TracksModel::clearData() { - { - SONOS::LockGuard lock(m_lock); - beginRemoveRows(QModelIndex(), 0, m_items.count()); - qDeleteAll(m_items); - m_items.clear(); - m_totalCount = 0; - endRemoveRows(); - } - emit countChanged(); + SONOS::LockGuard lock(m_lock); + qDeleteAll(m_data); + m_data.clear(); } -bool TracksModel::load() +bool TracksModel::loadData() { setUpdateSignaled(false); if (!m_provider) - return false; - clear(); { - SONOS::LockGuard lock(m_lock); - SAFE_DELETE(m_contentList); - SAFE_DELETE(m_contentDirectory); + emit loaded(false); + return false; } const SONOS::PlayerPtr player = m_provider->getPlayer(); if (!player) + { + emit loaded(false); return false; + } + + SONOS::LockGuard lock(m_lock); + SAFE_DELETE(m_contentList); + SAFE_DELETE(m_contentDirectory); + m_contentDirectory = new SONOS::ContentDirectory(player->GetHost(), player->GetPort()); + if (m_contentDirectory) + m_contentList = new SONOS::ContentList(*m_contentDirectory, m_root.isEmpty() ? SONOS::ContentSearch(SONOS::SearchTrack,"").Root() : m_root.toUtf8().constData()); + if (!m_contentList) { - SONOS::LockGuard lock(m_lock); - m_contentDirectory = new SONOS::ContentDirectory(player->GetHost(), player->GetPort()); - if (m_contentDirectory) - m_contentList = new SONOS::ContentList(*m_contentDirectory, m_root.isEmpty() ? SONOS::ContentSearch(SONOS::SearchTrack,"").Root() : m_root.toUtf8().constData()); - if (!m_contentList) - return false; - m_totalCount = m_contentList->size(); - m_iterator = m_contentList->begin(); + emit loaded(false); + return false; } - emit totalCountChanged(); + m_totalCount = m_contentList->size(); + m_iterator = m_contentList->begin(); QString port; port.setNum(m_contentDirectory->GetPort()); QString url = "http://"; url.append(m_contentDirectory->GetHost().c_str()).append(":").append(port); + clearData(); + m_dataState = ListModel::NoData; unsigned cnt = 0; while (cnt < LOAD_BULKSIZE && m_iterator != m_contentList->end()) { TrackItem* item = new TrackItem(*m_iterator, url); if (item->isValid()) { - addItem(item); + m_data << item; ++cnt; } else { delete item; // Also decrease total count - if (m_totalCount) - { + if (m_totalCount > 0) --m_totalCount; - emit totalCountChanged(); - } } ++m_iterator; } if (m_contentList->failure()) - return m_loaded = false; + { + emit loaded(false); + return false; + } m_updateID = m_contentList->GetUpdateID(); // sync new baseline - return m_loaded = true; + emit totalCountChanged(); + m_dataState = ListModel::Loaded; + emit loaded(true); + return true; } -bool TracksModel::loadMore() +bool TracksModel::asyncLoad() +{ + if (m_provider) + { + m_provider->runModelLoader(this); + return true; + } + return false; +} + +bool TracksModel::loadMoreData() { SONOS::LockGuard lock(m_lock); if (!m_contentDirectory || !m_contentList) + { + emit loadedMore(false); return false; + } // At end return false if (m_iterator == m_contentList->end()) + { + emit loadedMore(false); return false; + } QString port; port.setNum(m_contentDirectory->GetPort()); @@ -253,15 +290,14 @@ bool TracksModel::loadMore() TrackItem* item = new TrackItem(*m_iterator, url); if (item->isValid()) { - addItem(item); + m_data << item; ++cnt; } else { delete item; // Also decrease total count - if (m_totalCount) - { + if (m_totalCount) { --m_totalCount; emit totalCountChanged(); } @@ -269,14 +305,73 @@ bool TracksModel::loadMore() ++m_iterator; } if (m_contentList->failure()) + { + emit loadedMore(false); return false; + } + m_dataState = ListModel::Loaded; + emit loadedMore(true); return true; } -bool TracksModel::asyncLoad() +bool TracksModel::asyncLoadMore() { - if (m_provider) - m_provider->runModelLoader(this); + if (!m_provider) + return false; + m_provider->runCustomizedModelLoader(this, 1); + return true; +} + +void TracksModel::resetModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (TrackItem* item, m_data) + m_items << item; + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + endResetModel(); + } + emit countChanged(); +} + +void TracksModel::appendModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + int cnt = m_items.count(); + beginInsertRows(QModelIndex(), cnt, cnt + m_data.count()-1); + foreach (TrackItem* item, m_data) + m_items << item; + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + } + emit countChanged(); +} + +bool TracksModel::customizedLoad(int id) +{ + switch (id) + { + case 0: + return loadData(); + case 1: + return loadMoreData(); + default: + return false; + } } void TracksModel::handleDataUpdate() diff --git a/backend/modules/NosonApp/tracksmodel.h b/backend/modules/NosonApp/tracksmodel.h index 79fd8f68..59a3d7c2 100644 --- a/backend/modules/NosonApp/tracksmodel.h +++ b/backend/modules/NosonApp/tracksmodel.h @@ -51,6 +51,8 @@ class TrackItem bool isService() const { return m_isService; } + void setArt(const QString& art) { m_art = art; } + private: SONOS::DigitalItemPtr m_ptr; bool m_valid; @@ -91,20 +93,30 @@ class TracksModel : public QAbstractListModel, public ListModel QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole); + Q_INVOKABLE QVariantMap get(int row); Q_INVOKABLE bool init(QObject* sonos, const QString& root, bool fill = false); - Q_INVOKABLE void clear(); + virtual void clearData(); - Q_INVOKABLE bool load(); + virtual bool loadData(); int totalCount() const { return m_totalCount; } - Q_INVOKABLE bool loadMore(); - Q_INVOKABLE bool asyncLoad(); + virtual bool loadMoreData(); + + Q_INVOKABLE bool asyncLoadMore(); + + Q_INVOKABLE void resetModel(); + + Q_INVOKABLE void appendModel(); + + virtual bool customizedLoad(int id); + virtual void handleDataUpdate(); Q_INVOKABLE int containerUpdateID() { return m_updateID; } @@ -113,12 +125,15 @@ class TracksModel : public QAbstractListModel, public ListModel void dataUpdated(); void countChanged(); void totalCountChanged(); + void loaded(bool succeeded); + void loadedMore(bool succeeded); protected: QHash roleNames() const; private: QList m_items; + QList m_data; SONOS::ContentDirectory* m_contentDirectory; SONOS::ContentList* m_contentList; diff --git a/backend/modules/NosonApp/zonesmodel.cpp b/backend/modules/NosonApp/zonesmodel.cpp index e1740a42..852e47a9 100644 --- a/backend/modules/NosonApp/zonesmodel.cpp +++ b/backend/modules/NosonApp/zonesmodel.cpp @@ -53,7 +53,9 @@ ZonesModel::ZonesModel(QObject* parent) ZonesModel::~ZonesModel() { - clear(); + clearData(); + qDeleteAll(m_items); + m_items.clear(); } void ZonesModel::addItem(ZoneItem* item) @@ -75,6 +77,7 @@ int ZonesModel::rowCount(const QModelIndex& parent) const QVariant ZonesModel::data(const QModelIndex& index, int role) const { + SONOS::LockGuard lock(m_lock); if (index.row() < 0 || index.row() >= m_items.count()) return QVariant(); @@ -127,41 +130,70 @@ QVariantMap ZonesModel::get(int row) return model; } -bool ZonesModel::init(QObject* sonos, bool fill) +void ZonesModel::clearData() { - return ListModel::init(sonos, "", fill); -} - -void ZonesModel::clear() -{ - { - SONOS::LockGuard lock(m_lock); - beginRemoveRows(QModelIndex(), 0, m_items.count()); - qDeleteAll(m_items); - m_items.clear(); - endRemoveRows(); - } - emit countChanged(); + SONOS::LockGuard lock(m_lock); + qDeleteAll(m_data); + m_data.clear(); } -bool ZonesModel::load() +bool ZonesModel::loadData() { setUpdateSignaled(false); if (!m_provider) + { + emit loaded(false); return false; - clear(); - SONOS::ZoneList zones = m_provider->getSystem().GetZoneList(); + } + SONOS::LockGuard lock(m_lock); + clearData(); + m_dataState = ListModel::NoData; + SONOS::ZoneList zones = m_provider->getSystem().GetZoneList(); for (SONOS::ZoneList::iterator it = zones.begin(); it != zones.end(); ++it) { ZoneItem* item = new ZoneItem(it->second); if (item->isValid()) - addItem(item); + m_data << item; else delete item; } - return m_loaded = true; + m_dataState = ListModel::Loaded; + emit loaded(true); + return true; +} + +bool ZonesModel::asyncLoad() +{ + if (m_provider) + { + m_provider->runModelLoader(this); + return true; + } + return false; +} + +void ZonesModel::resetModel() +{ + { + SONOS::LockGuard lock(m_lock); + if (m_dataState != ListModel::Loaded) + return; + beginResetModel(); + beginRemoveRows(QModelIndex(), 0, m_items.count()-1); + qDeleteAll(m_items); + m_items.clear(); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, m_data.count()-1); + foreach (ZoneItem* item, m_data) + m_items << item; + m_data.clear(); + m_dataState = ListModel::Synced; + endInsertRows(); + endResetModel(); + } + emit countChanged(); } void ZonesModel::handleDataUpdate() diff --git a/backend/modules/NosonApp/zonesmodel.h b/backend/modules/NosonApp/zonesmodel.h index 303302f4..a2494723 100644 --- a/backend/modules/NosonApp/zonesmodel.h +++ b/backend/modules/NosonApp/zonesmodel.h @@ -84,23 +84,31 @@ class ZonesModel : public QAbstractListModel, public ListModel Q_INVOKABLE QVariantMap get(int row); - Q_INVOKABLE bool init(QObject* sonos, bool fill = false); + Q_INVOKABLE bool init(QObject* sonos, bool fill = false) { return ListModel::init(sonos, fill); } - Q_INVOKABLE void clear(); + virtual void clearData(); - Q_INVOKABLE bool load(); + virtual bool loadData(); + + Q_INVOKABLE bool asyncLoad(); + + Q_INVOKABLE void resetModel(); + + Q_INVOKABLE void appendModel() { } virtual void handleDataUpdate(); signals: void dataUpdated(); void countChanged(); + void loaded(bool succeeded); protected: QHash roleNames() const; private: QList m_items; + QList m_data; }; #endif /* ZONESMODEL_H */ diff --git a/debian/changelog b/debian/changelog index fc0b9ff1..5bc900b8 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,8 @@ -noson-app (2.4) UNRELEASED; urgency=low +noson-app (2.5) UNRELEASED; urgency=low + + [ janbar ] + * Release 2.5 + * Refactor for latest backend [ janbar ] * Release 2.4