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