summaryrefslogtreecommitdiff
path: root/rockwork
diff options
context:
space:
mode:
authorAndrew Branson <andrew.branson@cern.ch>2016-02-11 23:55:16 +0100
committerAndrew Branson <andrew.branson@cern.ch>2016-02-11 23:55:16 +0100
commit29aaea2d80a9eb1715b6cddfac2d2aacf76358bd (patch)
tree012795b6bec16c72f38d33cff46324c9a0225868 /rockwork
launchpad ~mzanetti/rockwork/trunk r87
Diffstat (limited to 'rockwork')
-rw-r--r--rockwork/AppSettingsPage.qml78
-rw-r--r--rockwork/AppStoreDetailsPage.qml278
-rw-r--r--rockwork/AppStorePage.qml266
-rw-r--r--rockwork/ContentPeerPickerPage.qml41
-rw-r--r--rockwork/DeveloperToolsPage.qml157
-rw-r--r--rockwork/FirmwareUpgradePage.qml58
-rw-r--r--rockwork/HealthSettingsDialog.qml115
-rw-r--r--rockwork/ImportPackagePage.qml32
-rw-r--r--rockwork/InfoPage.qml86
-rw-r--r--rockwork/InstalledAppDelegate.qml88
-rw-r--r--rockwork/InstalledAppsPage.qml201
-rw-r--r--rockwork/Main.qml53
-rw-r--r--rockwork/MainMenuPage.qml317
-rw-r--r--rockwork/NotificationsPage.qml88
-rw-r--r--rockwork/PebbleModels.qml28
-rw-r--r--rockwork/PebblesPage.qml69
-rw-r--r--rockwork/ScreenshotsPage.qml107
-rw-r--r--rockwork/SettingsPage.qml80
-rw-r--r--rockwork/SystemAppIcon.qml67
-rw-r--r--rockwork/applicationsfiltermodel.cpp102
-rw-r--r--rockwork/applicationsfiltermodel.h54
-rw-r--r--rockwork/applicationsmodel.cpp365
-rw-r--r--rockwork/applicationsmodel.h160
-rw-r--r--rockwork/appstoreclient.cpp323
-rw-r--r--rockwork/appstoreclient.h62
-rw-r--r--rockwork/artwork/bianca-black.pngbin0 -> 9165 bytes
-rw-r--r--rockwork/artwork/bianca-silver.pngbin0 -> 10766 bytes
-rw-r--r--rockwork/artwork/black-20mm-hole.pngbin0 -> 44522 bytes
-rw-r--r--rockwork/artwork/bobby-black.pngbin0 -> 16390 bytes
-rw-r--r--rockwork/artwork/bobby-gold.pngbin0 -> 35026 bytes
-rw-r--r--rockwork/artwork/bobby-silver.pngbin0 -> 16599 bytes
-rw-r--r--rockwork/artwork/rockwork.svg275
-rw-r--r--rockwork/artwork/snowy-black.pngbin0 -> 28360 bytes
-rw-r--r--rockwork/artwork/snowy-red.pngbin0 -> 29962 bytes
-rw-r--r--rockwork/artwork/snowy-white.pngbin0 -> 25610 bytes
-rw-r--r--rockwork/artwork/spalding-14mm-black.pngbin0 -> 45124 bytes
-rw-r--r--rockwork/artwork/spalding-14mm-rose-gold.pngbin0 -> 52798 bytes
-rw-r--r--rockwork/artwork/spalding-14mm-silver.pngbin0 -> 36782 bytes
-rw-r--r--rockwork/artwork/spalding-20mm-black.pngbin0 -> 44592 bytes
-rw-r--r--rockwork/artwork/spalding-20mm-silver.pngbin0 -> 38164 bytes
-rw-r--r--rockwork/artwork/tintin-black.pngbin0 -> 5497 bytes
-rw-r--r--rockwork/artwork/tintin-blue.pngbin0 -> 8409 bytes
-rw-r--r--rockwork/artwork/tintin-green.pngbin0 -> 8338 bytes
-rw-r--r--rockwork/artwork/tintin-grey.pngbin0 -> 5493 bytes
-rw-r--r--rockwork/artwork/tintin-orange.pngbin0 -> 6384 bytes
-rw-r--r--rockwork/artwork/tintin-pink.pngbin0 -> 8897 bytes
-rw-r--r--rockwork/artwork/tintin-red.pngbin0 -> 6160 bytes
-rw-r--r--rockwork/artwork/tintin-white.pngbin0 -> 6089 bytes
-rw-r--r--rockwork/main.cpp37
-rw-r--r--rockwork/notificationsourcemodel.cpp117
-rw-r--r--rockwork/notificationsourcemodel.h48
-rw-r--r--rockwork/org.freedesktop.Notifications.xml45
-rw-r--r--rockwork/pebble.cpp432
-rw-r--r--rockwork/pebble.h131
-rw-r--r--rockwork/pebbles.cpp180
-rw-r--r--rockwork/pebbles.h56
-rw-r--r--rockwork/rockwork.apparmor7
-rw-r--r--rockwork/rockwork.desktop8
-rw-r--r--rockwork/rockwork.pro72
-rw-r--r--rockwork/rockwork.qrc48
-rw-r--r--rockwork/rockwork.svg275
-rw-r--r--rockwork/rockwork.url-dispatcher5
-rw-r--r--rockwork/screenshotmodel.cpp71
-rw-r--r--rockwork/screenshotmodel.h38
-rw-r--r--rockwork/servicecontrol.cpp118
-rw-r--r--rockwork/servicecontrol.h38
-rw-r--r--rockwork/snowywhite.pngbin0 -> 14213 bytes
-rw-r--r--rockwork/snowywhite.svg241
68 files changed, 5517 insertions, 0 deletions
diff --git a/rockwork/AppSettingsPage.qml b/rockwork/AppSettingsPage.qml
new file mode 100644
index 0000000..d8d865b
--- /dev/null
+++ b/rockwork/AppSettingsPage.qml
@@ -0,0 +1,78 @@
+import QtQuick 2.4
+import Ubuntu.Web 0.2
+import Ubuntu.Components 1.3
+import com.canonical.Oxide 1.0 as Oxide
+
+Page {
+ id: settings
+
+ property string uuid;
+ property string url;
+ property var pebble;
+
+ title: i18n.tr("App Settings")
+
+ WebContext {
+ id: webcontext
+ userAgent: "Mozilla/5.0 (Linux; Android 5.0; Nexus 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.102 Mobile Safari/537.36 Ubuntu Touch (RockWork)"
+ }
+
+ WebView {
+ id: webview
+ anchors {
+ fill: parent
+ bottom: parent.bottom
+ }
+ width: parent.width
+ height: parent.height
+
+ context: webcontext
+ url: settings.url
+ preferences.localStorageEnabled: true
+ preferences.appCacheEnabled: true
+ preferences.javascriptCanAccessClipboard: true
+
+ function navigationRequestedDelegate(request) {
+ //The pebblejs:// protocol is handeled by the urihandler, as it appears we can't intercept it
+
+ var url = request.url.toString();
+ console.log(url, url.substring(0, 16));
+ if (url.substring(0, 16) == 'pebblejs://close') {
+ pebble.configurationClosed(settings.uuid, url);
+ request.action = Oxide.NavigationRequest.ActionReject;
+ pageStack.pop();
+ }
+ }
+
+ Component.onCompleted: {
+ preferences.localStorageEnabled = true;
+ }
+ }
+
+ ProgressBar {
+ height: units.dp(3)
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: parent.top
+ }
+
+ showProgressPercentage: false
+ value: (webview.loadProgress / 100)
+ visible: (webview.loading && !webview.lastLoadStopped)
+ }
+
+ Connections {
+ target: UriHandler
+ onOpened: {
+ if (uris && uris[0] && uris[0].length) {
+ var url = uris[0];
+
+ if (url.substring(0, 16) == 'pebblejs://close') {
+ pebble.configurationClosed(settings.uuid, url);
+ pageStack.pop();
+ }
+ }
+ }
+ }
+}
diff --git a/rockwork/AppStoreDetailsPage.qml b/rockwork/AppStoreDetailsPage.qml
new file mode 100644
index 0000000..696e3c6
--- /dev/null
+++ b/rockwork/AppStoreDetailsPage.qml
@@ -0,0 +1,278 @@
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.3
+import Ubuntu.Components.ListItems 1.3
+import QtGraphicalEffects 1.0
+
+Page {
+ id: root
+ title: i18n.tr("App details")
+
+ property var pebble: null
+ property var app: null
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: units.gu(1)
+
+ Item {
+ Layout.fillWidth: true
+ height: headerColumn.height + units.gu(1)
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: units.gu(1)
+ spacing: units.gu(1)
+ height: headerColumn.height
+
+ UbuntuShape {
+ id: iconShape
+ Layout.fillHeight: true
+ Layout.preferredWidth: height
+
+ source: Image {
+ height: iconShape.height
+ width: iconShape.width
+ source: root.app.icon
+ }
+ }
+
+ ColumnLayout {
+ id: headerColumn
+ Layout.fillWidth: true
+ Label {
+ text: root.app.name
+ fontSize: "large"
+ Layout.fillWidth: true
+ elide: Text.ElideRight
+ }
+ Label {
+ text: root.app.vendor
+ Layout.fillWidth: true
+ }
+ }
+
+ Button {
+ id: installButton
+ text: enabled ? i18n.tr("Install") : (installing && !installed ? i18n.tr("Installing...") : i18n.tr("Installed"))
+ color: UbuntuColors.green
+ enabled: !installed && !installing
+ property bool installing: false
+ property bool installed: root.pebble.installedApps.contains(root.app.storeId) || root.pebble.installedWatchfaces.contains(root.app.storeId)
+ Connections {
+ target: root.pebble.installedApps
+ onChanged: {
+ installButton.installed = root.pebble.installedApps.contains(root.app.storeId) || root.pebble.installedWatchfaces.contains(root.app.storeId)
+ }
+ }
+
+ Connections {
+ target: root.pebble.installedWatchfaces
+ onChanged: {
+ installButton.installed = root.pebble.installedApps.contains(root.app.storeId) || root.pebble.installedWatchfaces.contains(root.app.storeId)
+ }
+ }
+
+ onClicked: {
+ root.pebble.installApp(root.app.storeId)
+ installButton.installing = true
+ }
+ }
+ }
+ }
+
+ Flickable {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ contentHeight: contentColumn.height
+ bottomMargin: units.gu(1)
+ clip: true
+
+ Column {
+ id: contentColumn
+ width: parent.width
+ height: childrenRect.height
+
+ Image {
+ width: parent.width
+ // ss.w : ss.h = w : h
+ height: sourceSize.height * width / sourceSize.width
+ fillMode: Image.PreserveAspectFit
+ source: root.app.headerImage
+ }
+
+ RowLayout {
+ anchors {
+ left: parent.left
+ right: parent.right
+ }
+ height: units.gu(6)
+
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Row {
+ anchors.centerIn: parent
+ spacing: units.gu(1)
+ Icon {
+ name: "like"
+ height: parent.height
+ width: height
+ }
+ Label {
+ text: root.app.hearts
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.preferredHeight: parent.height - units.gu(2)
+ Layout.preferredWidth: units.dp(1)
+ color: UbuntuColors.lightGrey
+ }
+
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Row {
+ anchors.centerIn: parent
+ spacing: units.gu(1)
+ Icon {
+ name: root.app.isWatchFace ? "clock-app-symbolic" : "stock_application"
+ height: parent.height
+ width: height
+ }
+ Label {
+ text: root.app.isWatchFace ? "Watchface" : "Watchapp"
+ }
+ }
+ }
+ }
+
+ ColumnLayout {
+ anchors { left: parent.left; right: parent.right; margins: units.gu(1) }
+ spacing: units.gu(1)
+
+ PebbleModels {
+ id: modelModel
+ }
+
+
+ Item {
+ id: screenshotsItem
+ Layout.preferredHeight: units.gu(20)
+ Layout.fillWidth: true
+
+ property bool isRound: modelModel.get(root.pebble.model).shape === "round"
+
+ ListView {
+ id: screenshotsListView
+ anchors.centerIn: parent
+ width: parent.width
+ height: screenshotsItem.isRound ? units.gu(10) : units.gu(9.5)
+ orientation: ListView.Horizontal
+ spacing: units.gu(1)
+ snapMode: ListView.SnapToItem
+ preferredHighlightBegin: (screenshotsListView.width - height * .95) / 2
+ preferredHighlightEnd: (screenshotsListView.width + height * .95) / 2
+ highlightRangeMode: ListView.StrictlyEnforceRange
+
+ model: root.app.screenshotImages
+ delegate: AnimatedImage {
+ height: screenshotsListView.height
+ width: height * 0.95
+ fillMode: Image.PreserveAspectFit
+ source: modelData
+ }
+ }
+ Image {
+ id: watchImage
+ // ssw : ssh = w : h
+ height: parent.height
+ width: height * sourceSize.width / sourceSize.height
+ fillMode: Image.PreserveAspectFit
+ anchors.centerIn: parent
+ source: modelModel.get(root.pebble.model).image
+ Rectangle {
+ anchors.centerIn: parent
+ height: units.gu(10)
+ width: height
+ color: "black"
+ radius: screenshotsItem.isRound ? height / 2 : 0
+ }
+ }
+
+ OpacityMask {
+ anchors.fill: screenshotsListView
+ source: screenshotsListView
+ maskSource: maskRect
+ }
+
+ Rectangle {
+ id: maskRect
+ anchors.fill: screenshotsListView
+ color: "transparent"
+ visible: false
+
+ Rectangle {
+ color: "blue"
+ anchors.centerIn: parent
+ height: screenshotsListView.height
+ width: screenshotsItem.isRound ? height : height * 0.9
+ radius: screenshotsItem.isRound ? height / 2 : units.gu(.5)
+// anchors.fill: watchImage
+// anchors.margins: units.gu(5)
+// radius: modelModel.get(root.pebble.model).shape === "rectangle" ? units.gu(.5) : height / 2
+// visible: false
+ }
+ }
+
+ }
+
+ Label {
+ Layout.fillWidth: true
+ font.bold: true
+ text: i18n.tr("Description")
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: units.dp(1)
+ color: UbuntuColors.lightGrey
+ }
+
+ Label {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ wrapMode: Text.WordWrap
+ text: root.app.description
+ }
+
+ GridLayout {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ columns: 2
+ columnSpacing: units.gu(1)
+ rowSpacing: units.gu(1)
+ Label {
+ text: i18n.tr("Developer")
+ font.bold: true
+ }
+ Label {
+ text: root.app.vendor
+ Layout.fillWidth: true
+ }
+ Label {
+ text: i18n.tr("Version")
+ font.bold: true
+ }
+ Label {
+ text: root.app.version
+ Layout.fillWidth: true
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/rockwork/AppStorePage.qml b/rockwork/AppStorePage.qml
new file mode 100644
index 0000000..bb8712b
--- /dev/null
+++ b/rockwork/AppStorePage.qml
@@ -0,0 +1,266 @@
+import QtQuick 2.4
+import Ubuntu.Components 1.3
+import QtQuick.Layouts 1.1
+import RockWork 1.0
+
+Page {
+ id: root
+ title: showWatchApps ? i18n.tr("Add new watchapp") : i18n.tr("Add new watchface")
+
+ property var pebble: null
+ property bool showWatchApps: false
+ property bool showWatchFaces: false
+
+ property string link: ""
+
+ function fetchHome() {
+ if (showWatchApps) {
+ client.fetchHome(AppStoreClient.TypeWatchapp)
+ } else {
+ client.fetchHome(AppStoreClient.TypeWatchface)
+ }
+ }
+
+ head {
+ actions: [
+ Action {
+ iconName: "search"
+ onTriggered: {
+ if (searchField.shown) {
+ searchField.shown = false;
+ root.fetchHome();
+ } else {
+ searchField.shown = true;
+ }
+ }
+ }
+ ]
+ }
+
+ Component.onCompleted: {
+ if (root.link) {
+ client.fetchLink(link)
+ } else {
+ root.fetchHome()
+ }
+ }
+
+ AppStoreClient {
+ id: client
+ hardwarePlatform: pebble.hardwarePlatform
+ }
+
+ Item {
+ id: searchField
+ anchors { left: parent.left; right: parent.right; top: parent.top }
+ anchors.topMargin: shown ? 0 : -height
+ Behavior on anchors.topMargin { UbuntuNumberAnimation {} }
+ opacity: shown ? 1 : 0
+ Behavior on opacity { UbuntuNumberAnimation {} }
+ height: units.gu(6)
+
+ property bool shown: false
+ onShownChanged: {
+ if (shown) {
+ searchTextField.focus = true;
+ }
+ }
+
+ TextField {
+ id: searchTextField
+ anchors.centerIn: parent
+ width: parent.width - units.gu(2)
+ onDisplayTextChanged: {
+ searchTimer.restart()
+ }
+
+ Timer {
+ id: searchTimer
+ interval: 300
+ onTriggered: {
+ client.search(searchTextField.displayText, root.showWatchApps ? AppStoreClient.TypeWatchapp : AppStoreClient.TypeWatchface);
+ }
+ }
+ }
+ }
+
+ Item {
+ anchors { left: parent.left; top: searchField.bottom; right: parent.right; bottom: parent.bottom }
+ ListView {
+ anchors.fill: parent
+ model: ApplicationsFilterModel {
+ id: appsFilterModel
+ model: client.model
+ }
+ clip: true
+ section.property: "groupId"
+ section.labelPositioning: ViewSection.CurrentLabelAtStart |
+ ViewSection.InlineLabels
+ section.delegate: ListItem {
+ height: section ? label.implicitHeight + units.gu(3) : 0
+
+ Rectangle {
+ anchors.fill: parent
+ color: "white"
+ }
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: units.gu(1)
+ Label {
+ id: label
+ text: client.model.groupName(section)
+ fontSize: "large"
+// font.weight: Font.DemiBold
+ elide: Text.ElideRight
+ Layout.fillWidth: true
+ }
+ AbstractButton {
+ Layout.fillHeight: true
+ implicitWidth: seeAllLabel.implicitWidth + height
+ Row {
+ anchors.verticalCenter: parent.verticalCenter
+ Label {
+ id: seeAllLabel
+ text: i18n.tr("See all")
+ }
+ Icon {
+ implicitHeight: parent.height
+ implicitWidth: height
+ name: "go-next"
+ }
+ }
+ onClicked: {
+ pageStack.push(Qt.resolvedUrl("AppStorePage.qml"), {pebble: root.pebble, link: client.model.groupLink(section), title: client.model.groupName(section)});
+ }
+ }
+ }
+ }
+
+ footer: Item {
+ height: client.model.links.length > 0 ? units.gu(6) : 0
+ width: parent.width
+
+ RowLayout {
+ anchors {
+ fill: parent
+ margins: units.gu(1)
+ }
+ spacing: units.gu(1)
+
+ Repeater {
+ model: client.model.links
+ Button {
+ text: client.model.linkName(client.model.links[index])
+ onClicked: client.fetchLink(client.model.links[index]);
+ color: UbuntuColors.orange
+ Layout.fillWidth: true
+ }
+ }
+ }
+ }
+
+ delegate: ListItem {
+ height: delegateColumn.height + units.gu(2)
+
+ RowLayout {
+ id: delegateRow
+ anchors.fill: parent
+ anchors.margins: units.gu(1)
+ spacing: units.gu(1)
+
+ AnimatedImage {
+ Layout.fillHeight: true
+ Layout.preferredWidth: height
+ source: model.icon
+ asynchronous: true
+// sourceSize.width: width
+// sourceSize.height: height
+ }
+
+ ColumnLayout {
+ id: delegateColumn
+ Layout.fillWidth: true;
+ Layout.fillHeight: true;
+ Label {
+ Layout.fillWidth: true
+ text: model.name
+ font.weight: Font.DemiBold
+ elide: Text.ElideRight
+ }
+ Label {
+ Layout.fillWidth: true
+ text: model.category
+ }
+ RowLayout {
+ Icon {
+ name: "like"
+ Layout.preferredHeight: parent.height
+ Layout.preferredWidth: height
+ implicitHeight: parent.height
+ }
+ Label {
+ Layout.fillWidth: true
+ text: model.hearts
+ }
+ Icon {
+ id: tickIcon
+ name: "tick"
+ implicitHeight: parent.height
+ Layout.preferredWidth: height
+ visible: root.pebble.installedApps.contains(model.storeId) || root.pebble.installedWatchfaces.contains(model.storeId)
+ Connections {
+ target: root.pebble.installedApps
+ onChanged: {
+ tickIcon.visible = root.pebble.installedApps.contains(model.storeId) || root.pebble.installedWatchfaces.contains(model.storeId)
+ }
+ }
+
+ Connections {
+ target: root.pebble.installedWatchfaces
+ onChanged: {
+ tickIcon.visible = root.pebble.installedApps.contains(model.storeId) || root.pebble.installedWatchfaces.contains(model.storeId)
+ }
+ }
+
+ }
+ }
+ }
+
+ }
+
+ onClicked: {
+ client.fetchAppDetails(model.storeId);
+ pageStack.push(Qt.resolvedUrl("AppStoreDetailsPage.qml"), {app: appsFilterModel.get(index), pebble: root.pebble})
+ }
+ }
+ }
+
+// RowLayout {
+// id: buttonRow
+// anchors { left: parent.left; bottom: parent.bottom; right: parent.right; margins: units.gu(1) }
+// spacing: units.gu(1)
+// Button {
+// text: i18n.tr("Previous")
+// Layout.fillWidth: true
+// enabled: client.offset > 0
+// onClicked: {
+// client.previous()
+// }
+// }
+// Button {
+// text: i18n.tr("Next")
+// Layout.fillWidth: true
+// onClicked: {
+// client.next()
+// }
+// }
+// }
+ }
+
+ ActivityIndicator {
+ anchors.centerIn: parent
+ running: client.busy
+ }
+}
+
diff --git a/rockwork/ContentPeerPickerPage.qml b/rockwork/ContentPeerPickerPage.qml
new file mode 100644
index 0000000..7ee9702
--- /dev/null
+++ b/rockwork/ContentPeerPickerPage.qml
@@ -0,0 +1,41 @@
+import QtQuick 2.4
+import Ubuntu.Components 1.3
+import Ubuntu.Content 1.3
+import RockWork 1.0
+
+Page {
+ id: pickerPage
+ head {
+ locked: true
+ visible: false
+ }
+
+ property alias contentType: contentPeerPicker.contentType
+ property string itemName
+ property alias handler: contentPeerPicker.handler
+ property string filename
+
+ Component {
+ id: exportItemComponent
+ ContentItem {
+ name: pickerPage.itemName
+ }
+ }
+ ContentPeerPicker {
+ id: contentPeerPicker
+ anchors.fill: parent
+
+ onCancelPressed: pageStack.pop()
+
+ onPeerSelected: {
+ var transfer = peer.request();
+ var items = [];
+ var item = exportItemComponent.createObject();
+ item.url = "file://" + pickerPage.filename;
+ items.push(item)
+ transfer.items = items;
+ transfer.state = ContentTransfer.Charged;
+ pageStack.pop();
+ }
+ }
+}
diff --git a/rockwork/DeveloperToolsPage.qml b/rockwork/DeveloperToolsPage.qml
new file mode 100644
index 0000000..2f77254
--- /dev/null
+++ b/rockwork/DeveloperToolsPage.qml
@@ -0,0 +1,157 @@
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.3
+import Ubuntu.Components.Popups 1.3
+import Ubuntu.Content 1.3
+
+Page {
+ id: root
+ title: i18n.tr("Developer Tools")
+
+ property var pebble: null
+
+ //Creating the menu list this way to allow the text field to be translatable (http://askubuntu.com/a/476331)
+ ListModel {
+ id: devMenuModel
+ dynamicRoles: true
+ }
+
+ Component.onCompleted: {
+ populateDevMenu();
+ }
+
+ function populateDevMenu() {
+ devMenuModel.clear();
+
+ devMenuModel.append({
+ icon: "camera-app-symbolic",
+ text: i18n.tr("Screenshots"),
+ page: "ScreenshotsPage.qml",
+ dialog: "",
+ color: "gold"
+ });
+ devMenuModel.append({
+ icon: "dialog-warning-symbolic",
+ text: i18n.tr("Report problem"),
+ page: "",
+ dialog: sendLogsComponent,
+ color: UbuntuColors.red
+ });
+ devMenuModel.append({
+ icon: "stock_application",
+ text: i18n.tr("Install app or watchface from file"),
+ page: "ImportPackagePage.qml",
+ dialog: null,
+ color: UbuntuColors.blue
+ });
+
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+
+ Repeater {
+ id: menuRepeater
+ model: devMenuModel
+ delegate: ListItem {
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: units.gu(1)
+
+ UbuntuShape {
+ Layout.fillHeight: true
+ Layout.preferredWidth: height
+ backgroundColor: model.color
+ Icon {
+ anchors.fill: parent
+ anchors.margins: units.gu(.5)
+ name: model.icon
+ color: "white"
+ }
+ }
+
+
+ Label {
+ text: model.text
+ Layout.fillWidth: true
+ }
+ }
+
+ onClicked: {
+ if (model.page) {
+ pageStack.push(Qt.resolvedUrl(model.page), {pebble: root.pebble})
+ }
+ if (model.dialog) {
+ PopupUtils.open(model.dialog)
+ }
+ }
+ }
+ }
+
+ Item {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ }
+ }
+
+ Component {
+ id: sendLogsComponent
+ Dialog {
+ id: sendLogsDialog
+ title: i18n.tr("Report problem")
+ ActivityIndicator {
+ id: busyIndicator
+ visible: false
+ running: visible
+ }
+ Label {
+ text: i18n.tr("Preparing logs package...")
+ visible: busyIndicator.visible
+ horizontalAlignment: Text.AlignHCenter
+ fontSize: "large"
+ }
+
+ Connections {
+ target: root.pebble
+ onLogsDumped: {
+ if (success) {
+ var filename = "/tmp/pebble.log"
+ pageStack.push(Qt.resolvedUrl("ContentPeerPickerPage.qml"), {itemName: i18n.tr("pebble.log"),handler: ContentHandler.Share, contentType: ContentType.All, filename: filename })
+ }
+ PopupUtils.close(sendLogsDialog)
+ }
+ }
+
+ Button {
+ text: i18n.tr("Send rockworkd.log")
+ color: UbuntuColors.blue
+ visible: !busyIndicator.visible
+ onClicked: {
+ var filename = homePath + "/.cache/upstart/rockworkd.log"
+ pageStack.push(Qt.resolvedUrl("ContentPeerPickerPage.qml"), {itemName: i18n.tr("rockworkd.log"),handler: ContentHandler.Share, contentType: ContentType.All, filename: filename })
+ PopupUtils.close(sendLogsDialog)
+ }
+ }
+ Button {
+ text: i18n.tr("Send watch logs")
+ color: UbuntuColors.blue
+ visible: !busyIndicator.visible
+ onClicked: {
+ busyIndicator.visible = true
+ root.pebble.dumpLogs("/tmp/pebble.log")
+ }
+ }
+ Button {
+ text: i18n.tr("Cancel")
+ color: UbuntuColors.red
+ visible: !busyIndicator.visible
+ onClicked: {
+ PopupUtils.close(sendLogsDialog)
+ }
+ }
+ }
+ }
+
+}
+
diff --git a/rockwork/FirmwareUpgradePage.qml b/rockwork/FirmwareUpgradePage.qml
new file mode 100644
index 0000000..3281a12
--- /dev/null
+++ b/rockwork/FirmwareUpgradePage.qml
@@ -0,0 +1,58 @@
+import QtQuick 2.4
+import Ubuntu.Components 1.3
+
+Page {
+ id: root
+ title: i18n.tr("Firmware upgrade")
+
+ property var pebble: null
+
+ Column {
+ anchors.fill: parent
+ anchors.margins: units.gu(1)
+ spacing: units.gu(2)
+
+ Label {
+ text: i18n.tr("A new firmware upgrade is available for your Pebble smartwatch.")
+ fontSize: "large"
+ width: parent.width
+ wrapMode: Text.WordWrap
+ }
+
+ Label {
+ text: i18n.tr("Currently installed firmware: %1").arg("<b>" + root.pebble.softwareVersion + "</b>")
+ width: parent.width
+ wrapMode: Text.WordWrap
+ }
+
+ Label {
+ text: i18n.tr("Candidate firmware version: %1").arg("<b>" + root.pebble.candidateVersion + "</b>")
+ width: parent.width
+ wrapMode: Text.WordWrap
+ }
+
+ Label {
+ text: "<b>" + i18n.tr("Release Notes: %1").arg("</b><br>" + root.pebble.firmwareReleaseNotes)
+ width: parent.width
+ wrapMode: Text.WordWrap
+ }
+
+ Label {
+ text: "<b>" + i18n.tr("Important:") + "</b> " + i18n.tr("This update will also upgrade recovery data. Make sure your Pebble smartwarch is connected to a power adapter.")
+ width: parent.width
+ wrapMode: Text.WordWrap
+ visible: root.pebble.candidateVersion.indexOf("mig") > 0
+ }
+
+ Button {
+ text: "Upgrade now"
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: UbuntuColors.blue
+ onClicked: {
+ root.pebble.performFirmwareUpgrade();
+ pageStack.pop();
+ }
+ }
+ }
+}
+
diff --git a/rockwork/HealthSettingsDialog.qml b/rockwork/HealthSettingsDialog.qml
new file mode 100644
index 0000000..94e5d22
--- /dev/null
+++ b/rockwork/HealthSettingsDialog.qml
@@ -0,0 +1,115 @@
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.3
+import Ubuntu.Components.Popups 1.3
+import Ubuntu.Components.ListItems 1.3
+
+Dialog {
+ id: root
+ title: i18n.tr("Health settings")
+
+ property var healthParams: null
+
+ signal accepted();
+
+ RowLayout {
+ Label {
+ text: i18n.tr("Health app enabled")
+ Layout.fillWidth: true
+ }
+ Switch {
+ id: enabledSwitch
+ checked: healthParams["enabled"]
+ }
+ }
+
+ ItemSelector {
+ id: genderSelector
+ model: [i18n.tr("Female"), i18n.tr("Male")]
+ selectedIndex: root.healthParams["gender"] === "female" ? 0 : 1
+ }
+
+ RowLayout {
+ Label {
+ text: i18n.tr("Age")
+ Layout.fillWidth: true
+ }
+ TextField {
+ id: ageField
+ inputMethodHints: Qt.ImhDigitsOnly
+ text: healthParams["age"]
+ Layout.preferredWidth: units.gu(10)
+ }
+ }
+
+ RowLayout {
+ Label {
+ text: i18n.tr("Height (cm)")
+ Layout.fillWidth: true
+ }
+ TextField {
+ id: heightField
+ inputMethodHints: Qt.ImhDigitsOnly
+ text: healthParams["height"]
+ Layout.preferredWidth: units.gu(10)
+ }
+ }
+
+ RowLayout {
+ Label {
+ text: i18n.tr("Weight")
+ Layout.fillWidth: true
+ }
+ TextField {
+ id: weightField
+ inputMethodHints: Qt.ImhDigitsOnly
+ text: healthParams["weight"]
+ Layout.preferredWidth: units.gu(10)
+ }
+ }
+
+ RowLayout {
+ Label {
+ text: i18n.tr("I want to be more active")
+ Layout.fillWidth: true
+ }
+ Switch {
+ id: moreActiveSwitch
+ checked: healthParams["moreActive"]
+ }
+ }
+
+ RowLayout {
+ Label {
+ text: i18n.tr("I want to sleep more")
+ Layout.fillWidth: true
+ }
+ Switch {
+ id: sleepMoreSwitch
+ checked: healthParams["sleepMore"]
+ }
+ }
+
+
+ Button {
+ text: i18n.tr("OK")
+ color: UbuntuColors.green
+ onClicked: {
+ root.healthParams["enabled"] = enabledSwitch.checked;
+ root.healthParams["gender"] = genderSelector.selectedIndex == 0 ? "female" : "male"
+ root.healthParams["age"] = ageField.text;
+ root.healthParams["height"] = heightField.text;
+ root.healthParams["weight"] = weightField.text;
+ root.healthParams["moreActive"] = moreActiveSwitch.checked;
+ root.healthParams["sleepMore"] = sleepMoreSwitch.checked;
+ root.accepted();
+ PopupUtils.close(root);
+ }
+ }
+ Button {
+ text: i18n.tr("Cancel")
+ color: UbuntuColors.red
+ onClicked: PopupUtils.close(root)
+ }
+}
+
diff --git a/rockwork/ImportPackagePage.qml b/rockwork/ImportPackagePage.qml
new file mode 100644
index 0000000..4f86f78
--- /dev/null
+++ b/rockwork/ImportPackagePage.qml
@@ -0,0 +1,32 @@
+import QtQuick 2.4
+import Ubuntu.Components 1.3
+import Ubuntu.Content 1.3
+
+Page {
+ id: root
+ title: i18n.tr("Import watchapp or watchface")
+
+ property var pebble: null
+
+ ContentPeerPicker {
+ anchors.fill: parent
+ handler: ContentHandler.Source
+ contentType: ContentType.All
+ showTitle: false
+
+ onPeerSelected: {
+ var transfer = peer.request();
+
+ transfer.stateChanged.connect(function() {
+ if (transfer.state == ContentTransfer.Charged) {
+ for (var i = 0; i < transfer.items.length; i++) {
+ print("sideloading package", transfer.items[i].url)
+ root.pebble.sideloadApp(transfer.items[i].url)
+ }
+ pageStack.pop();
+ }
+ })
+ }
+ }
+}
+
diff --git a/rockwork/InfoPage.qml b/rockwork/InfoPage.qml
new file mode 100644
index 0000000..3eec387
--- /dev/null
+++ b/rockwork/InfoPage.qml
@@ -0,0 +1,86 @@
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.3
+import Ubuntu.Components.ListItems 1.3
+
+Page {
+ title: "About RockWork"
+
+ Flickable {
+ anchors.fill: parent
+ contentHeight: contentColumn.height + units.gu(4)
+
+ ColumnLayout {
+ id: contentColumn
+ anchors { left: parent.left; top: parent.top; right: parent.right; margins: units.gu(2) }
+ spacing: units.gu(2)
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: units.gu(2)
+ UbuntuShape {
+ source: Image {
+ anchors.fill: parent
+ source: "artwork/rockwork.svg"
+ }
+ height: units.gu(6)
+ width: height
+ }
+
+ Label {
+ text: i18n.tr("Version %1").arg(version)
+ Layout.fillWidth: true
+ fontSize: "large"
+ }
+ }
+
+ ThinDivider {}
+
+ Label {
+ text: i18n.tr("Contributors")
+ Layout.fillWidth: true
+ font.bold: true
+ }
+ Label {
+ text: "Michael Zanetti<br>Brian Douglas<br>Katharine Berry"
+ Layout.fillWidth: true
+ }
+
+ ThinDivider {}
+
+ Label {
+ text: i18n.tr("Legal")
+ Layout.fillWidth: true
+ font.bold: true
+ }
+
+ Label {
+ text: "This program is free software: you can redistribute it and/or modify" +
+ "it under the terms of the GNU General Public License as published by" +
+ "the Free Software Foundation, version 3 of the License.<br>" +
+
+ "This program is distributed in the hope that it will be useful," +
+ "but WITHOUT ANY WARRANTY; without even the implied warranty of" +
+ "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the" +
+ "GNU General Public License for more details.<br>" +
+
+ "You should have received a copy of the GNU General Public License" +
+ "along with this program. If not, see <http://www.gnu.org/licenses/>."
+ Layout.fillWidth: true
+ wrapMode: Text.WordWrap
+ }
+
+ Label {
+ text: i18n.tr("This application is neither affiliated with nor endorsed by Pebble Technology Corp.")
+ Layout.fillWidth: true
+ wrapMode: Text.WordWrap
+ }
+ Label {
+ text: i18n.tr("Pebble is a trademark of Pebble Technology Corp.")
+ Layout.fillWidth: true
+ wrapMode: Text.WordWrap
+ }
+ }
+ }
+}
+
diff --git a/rockwork/InstalledAppDelegate.qml b/rockwork/InstalledAppDelegate.qml
new file mode 100644
index 0000000..89f6ba8
--- /dev/null
+++ b/rockwork/InstalledAppDelegate.qml
@@ -0,0 +1,88 @@
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.3
+import RockWork 1.0
+
+ListItem {
+ id: root
+
+ property string uuid: ""
+ property string name: ""
+ property string iconSource: ""
+ property string vendor: ""
+ property bool hasSettings: false
+ property alias hasGrip: grip.visible
+ property bool isSystemApp: false
+
+ signal deleteApp();
+ signal configureApp();
+
+ leadingActions: ListItemActions {
+ actions: [
+ Action {
+ visible: !root.isSystemApp
+ iconName: "delete"
+ onTriggered: {
+ root.deleteApp();
+ }
+ }
+ ]
+ }
+
+ trailingActions: ListItemActions {
+ actions: [
+ Action {
+ visible: root.hasSettings
+ iconName: "settings"
+ onTriggered: {
+ print("settings triggered")
+ root.configureApp();
+ }
+ }
+ ]
+ }
+
+ RowLayout {
+ anchors {
+ fill: parent
+ margins: units.gu(1)
+ }
+ spacing: units.gu(1)
+
+ SystemAppIcon {
+ Layout.fillHeight: true
+ Layout.preferredWidth: height
+ isSystemApp: root.isSystemApp
+ uuid: root.uuid
+ iconSource: root.iconSource
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ Label {
+ text: root.name
+ Layout.fillWidth: true
+ }
+
+ Label {
+ text: root.vendor
+ Layout.fillWidth: true
+ fontSize: "small"
+ }
+ }
+
+ Item {
+ id: grip
+ Layout.fillHeight: true
+ Layout.preferredWidth: height
+ opacity: (root.contentMoving || root.swiped || root.dragging) ? 0 : 1
+ Behavior on opacity { UbuntuNumberAnimation {} }
+ Icon {
+ width: units.gu(3)
+ height: width
+ anchors.centerIn: parent
+ name: "grip-large"
+ }
+ }
+ }
+}
diff --git a/rockwork/InstalledAppsPage.qml b/rockwork/InstalledAppsPage.qml
new file mode 100644
index 0000000..a18cd3f
--- /dev/null
+++ b/rockwork/InstalledAppsPage.qml
@@ -0,0 +1,201 @@
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.3
+import Ubuntu.Components.Popups 1.3
+import RockWork 1.0
+
+Page {
+ id: root
+ title: showWatchApps ? (showWatchFaces ? i18n.tr("Apps & Watchfaces") : i18n.tr("Apps")) : i18n.tr("Watchfaces")
+
+ property var pebble: null
+ property bool showWatchApps: false
+ property bool showWatchFaces: false
+
+ head {
+ actions: [
+ Action {
+ iconName: "add"
+ onTriggered: pageStack.push(Qt.resolvedUrl("AppStorePage.qml"), {pebble: root.pebble, showWatchApps: root.showWatchApps, showWatchFaces: root.showWatchFaces})
+ }
+ ]
+ }
+
+ function configureApp(uuid) {
+ // The health app is special :/
+ if (uuid == "{36d8c6ed-4c83-4fa1-a9e2-8f12dc941f8c}") {
+ var popup = PopupUtils.open(Qt.resolvedUrl("HealthSettingsDialog.qml"), root, {healthParams: pebble.healthParams});
+ popup.accepted.connect(function() {
+ pebble.healthParams = popup.healthParams
+ })
+ } else {
+ pebble.requestConfigurationURL(uuid);
+ }
+ }
+
+ Item {
+ anchors.fill: parent
+ ListView {
+ id: listView
+ anchors.fill: parent
+ model: root.showWatchApps ? root.pebble.installedApps : root.pebble.installedWatchfaces
+ clip: true
+ property real realContentY: contentY + originY
+
+ delegate: InstalledAppDelegate {
+ id: delegate
+ uuid: model.uuid
+ name: model.name
+ iconSource: model.icon
+ vendor: model.vendor
+ visible: dndArea.draggedIndex !== index
+ hasGrip: index > 0
+ isSystemApp: model.isSystemApp
+ hasSettings: model.hasSettings
+
+ onDeleteApp: {
+ pebble.removeApp(model.uuid)
+ }
+ onConfigureApp: {
+ root.configureApp(model.uuid)
+ }
+ onClicked: {
+ PopupUtils.open(dialogComponent, root, {app: listView.model.get(index)})
+ }
+ }
+ }
+ MouseArea {
+ id: dndArea
+ anchors {
+ top: parent.top
+ bottom: parent.bottom
+ right: parent.right
+ }
+ drag.axis: Drag.YAxis
+ propagateComposedEvents: true
+ width: units.gu(5)
+
+ property int startY: 0
+ property int draggedIndex: -1
+
+
+ onPressAndHold: {
+ startY = mouseY;
+ draggedIndex = Math.floor((listView.realContentY + mouseY) / fakeDragItem.height)
+ if (draggedIndex == 0) {
+ print("cannot drag settings app");
+ return;
+ }
+
+ var draggedItem = listView.model.get(draggedIndex);
+ fakeDragItem.uuid = draggedItem.uuid;
+ fakeDragItem.name = draggedItem.name;
+ fakeDragItem.vendor = draggedItem.vendor;
+ fakeDragItem.iconSource = draggedItem.icon;
+ fakeDragItem.isSystemApp = draggedItem.isSystemApp;
+ fakeDragItem.y = (fakeDragItem.height * draggedIndex) - listView.realContentY
+ drag.target = fakeDragItem;
+ }
+
+ onMouseYChanged: {
+ var newIndex = Math.floor((listView.realContentY + mouseY) / fakeDragItem.height)
+
+ if (newIndex > draggedIndex) {
+ newIndex = draggedIndex + 1;
+ } else if (newIndex < draggedIndex) {
+ newIndex = draggedIndex - 1;
+ } else {
+ return;
+ }
+
+ if (newIndex >= 1 && newIndex < listView.count) {
+ listView.model.move(draggedIndex, newIndex);
+ draggedIndex = newIndex;
+ }
+ }
+
+ onReleased: {
+ if (draggedIndex > -1) {
+ listView.model.commitMove();
+ draggedIndex = -1;
+ drag.target = null;
+ }
+ }
+ }
+ }
+
+
+
+ InstalledAppDelegate {
+ id: fakeDragItem
+ visible: dndArea.draggedIndex != -1
+
+ }
+
+ Component {
+ id: dialogComponent
+ Dialog {
+ id: dialog
+ property var app: null
+
+ RowLayout {
+ SystemAppIcon {
+ height: titleCol.height
+ width: height
+ isSystemApp: app.isSystemApp
+ uuid: app.uuid
+ iconSource: app.icon
+ }
+
+ ColumnLayout {
+ id: titleCol
+ Layout.fillWidth: true
+
+ Label {
+ Layout.fillWidth: true
+ text: app.name
+ fontSize: "large"
+ }
+ Label {
+ Layout.fillWidth: true
+ text: app.vendor
+ }
+ }
+ }
+
+ Button {
+ text: i18n.tr("Launch")
+ color: UbuntuColors.green
+ onClicked: {
+ pebble.launchApp(app.uuid);
+ PopupUtils.close(dialog);
+ }
+ }
+
+ Button {
+ text: i18n.tr("Configure")
+ color: UbuntuColors.blue
+ visible: app.hasSettings
+ onClicked: {
+ root.configureApp(app.uuid);
+ PopupUtils.close(dialog);
+ }
+ }
+
+ Button {
+ text: i18n.tr("Delete")
+ color: UbuntuColors.red
+ visible: !app.isSystemApp
+ onClicked: {
+ pebble.removeApp(app.uuid);
+ PopupUtils.close(dialog);
+ }
+ }
+
+ Button {
+ text: i18n.tr("Close")
+ onClicked: PopupUtils.close(dialog)
+ }
+ }
+ }
+}
diff --git a/rockwork/Main.qml b/rockwork/Main.qml
new file mode 100644
index 0000000..2bdece3
--- /dev/null
+++ b/rockwork/Main.qml
@@ -0,0 +1,53 @@
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.3
+import RockWork 1.0
+
+/*!
+ \brief MainView with a Label and Button elements.
+*/
+
+MainView {
+ applicationName: "rockwork.mzanetti"
+
+ width: units.gu(40)
+ height: units.gu(70)
+
+ ServiceController {
+ id: serviceController
+ serviceName: "rockworkd"
+ Component.onCompleted: {
+ if (!serviceController.serviceFileInstalled) {
+ print("Service file not installed. Installing now.")
+ serviceController.installServiceFile();
+ }
+ if (!serviceController.serviceRunning) {
+ print("Service not running. Starting now.")
+ serviceController.startService();
+ }
+ if (pebbles.version !== version) {
+ print("Service file version (", version, ") is not equal running service version (", pebbles.version, "). Restarting service.")
+ serviceController.restartService();
+ }
+ }
+ }
+
+ Pebbles {
+ id: pebbles
+ onCountChanged: loadStack()
+ }
+
+ function loadStack() {
+ pageStack.clear()
+ if (pebbles.count == 1) {
+ pageStack.push(Qt.resolvedUrl("MainMenuPage.qml"), {pebble: pebbles.get(0)})
+ } else {
+ pageStack.push(Qt.resolvedUrl("PebblesPage.qml"))
+ }
+ }
+
+ PageStack {
+ id: pageStack
+ Component.onCompleted: loadStack();
+ }
+}
diff --git a/rockwork/MainMenuPage.qml b/rockwork/MainMenuPage.qml
new file mode 100644
index 0000000..32c7b96
--- /dev/null
+++ b/rockwork/MainMenuPage.qml
@@ -0,0 +1,317 @@
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.3
+
+Page {
+ id: root
+ title: pebble.name
+
+ property var pebble: null
+
+ head {
+ actions: [
+ Action {
+ iconName: "info"
+ text: i18n.tr("About")
+ onTriggered: {
+ pageStack.push(Qt.resolvedUrl("InfoPage.qml"))
+ }
+ },
+ Action {
+ iconName: "ubuntu-sdk-symbolic"
+ text: i18n.tr("Developer tools")
+ onTriggered: {
+ pageStack.push(Qt.resolvedUrl("DeveloperToolsPage.qml"), {pebble: root.pebble})
+ }
+ }
+ ]
+ }
+
+ //Creating the menu list this way to allow the text field to be translatable (http://askubuntu.com/a/476331)
+ ListModel {
+ id: mainMenuModel
+ dynamicRoles: true
+ }
+
+ Component.onCompleted: {
+ populateMainMenu();
+ }
+
+ Connections {
+ target: root.pebble
+ onFirmwareUpgradeAvailableChanged: {
+ populateMainMenu();
+ }
+ }
+
+ function populateMainMenu() {
+ mainMenuModel.clear();
+
+ mainMenuModel.append({
+ icon: "stock_notification",
+ text: i18n.tr("Manage notifications"),
+ page: "NotificationsPage.qml",
+ color: "blue"
+ });
+
+ mainMenuModel.append({
+ icon: "stock_application",
+ text: i18n.tr("Manage Apps"),
+ page: "InstalledAppsPage.qml",
+ showWatchApps: true,
+ color: UbuntuColors.green
+ });
+
+ mainMenuModel.append({
+ icon: "clock-app-symbolic",
+ text: i18n.tr("Manage Watchfaces"),
+ page: "InstalledAppsPage.qml",
+ showWatchFaces: true,
+ color: "black"
+ });
+
+ mainMenuModel.append({
+ icon: "settings",
+ text: i18n.tr("Settings"),
+ page: "SettingsPage.qml",
+ showWatchFaces: true,
+ color: "gold"
+ });
+
+ if (root.pebble.firmwareUpgradeAvailable) {
+ mainMenuModel.append({
+ icon: "preferences-system-updates-symbolic",
+ text: i18n.tr("Firmware upgrade"),
+ page: "FirmwareUpgradePage.qml",
+ color: "red"
+ });
+ }
+
+ }
+
+ PebbleModels {
+ id: modelModel
+ }
+
+ GridLayout {
+ anchors.fill: parent
+ columns: parent.width > parent.height ? 2 : 1
+
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Layout.maximumHeight: units.gu(30)
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: units.gu(1)
+ spacing: units.gu(1)
+
+ Item {
+ Layout.alignment: Qt.AlignHCenter
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ Layout.minimumWidth: watchImage.width
+ Image {
+ id: watchImage
+ width: implicitWidth * height / implicitHeight
+ height: parent.height
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ source: modelModel.get(root.pebble.model).image
+ fillMode: Image.PreserveAspectFit
+
+ Item {
+ id: watchFace
+ height: parent.height * (modelModel.get(root.pebble.model - 1).shape === "rectangle" ? .5 : .515)
+ width: height * (modelModel.get(root.pebble.model - 1).shape === "rectangle" ? .85 : 1)
+ anchors.centerIn: parent
+ anchors.horizontalCenterOffset: units.dp(1)
+ anchors.verticalCenterOffset: units.dp(modelModel.get(root.pebble.model - 1).shape === "rectangle" ? 0 : 1)
+
+ Image {
+ id: image
+ anchors.fill: parent
+ source: "file://" + root.pebble.screenshots.latestScreenshot
+ visible: false
+ }
+
+ Component.onCompleted: {
+ if (!root.pebble.screenshots.latestScreenshot) {
+ root.pebble.requestScreenshot();
+ }
+ }
+
+ Rectangle {
+ id: textItem
+ anchors.fill: parent
+ layer.enabled: true
+ radius: modelModel.get(root.pebble.model - 1).shape === "rectangle" ? units.gu(.5) : height / 2
+ // This item should be used as the 'mask'
+ layer.samplerName: "maskSource"
+ layer.effect: ShaderEffect {
+ property var colorSource: image;
+ fragmentShader: "
+ uniform lowp sampler2D colorSource;
+ uniform lowp sampler2D maskSource;
+ uniform lowp float qt_Opacity;
+ varying highp vec2 qt_TexCoord0;
+ void main() {
+ gl_FragColor =
+ texture2D(colorSource, qt_TexCoord0)
+ * texture2D(maskSource, qt_TexCoord0).a
+ * qt_Opacity;
+ }
+ "
+ }
+ }
+ }
+ }
+ }
+ ColumnLayout {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ spacing: units.gu(2)
+ Rectangle {
+ height: units.gu(10)
+ width: height
+ radius: height / 2
+ color: root.pebble.connected ? UbuntuColors.green : UbuntuColors.red
+
+ Icon {
+ anchors.fill: parent
+ anchors.margins: units.gu(2)
+ color: "white"
+ name: root.pebble.connected ? "tick" : "dialog-error-symbolic"
+ }
+ }
+
+ Label {
+ text: root.pebble.connected ? i18n.tr("Connected") : i18n.tr("Disconnected")
+ Layout.fillWidth: true
+ }
+ }
+ }
+ }
+
+
+ Column {
+ Layout.fillWidth: true
+ Layout.preferredHeight: childrenRect.height
+ spacing: menuRepeater.count > 0 ? 0 : units.gu(2)
+ Label {
+ text: i18n.tr("Your Pebble smartwatch is disconnected. Please make sure it is powered on, within range and it is paired properly in the Bluetooth System Settings.")
+ width: parent.width - units.gu(4)
+ anchors.horizontalCenter: parent.horizontalCenter
+ wrapMode: Text.WordWrap
+ visible: !root.pebble.connected
+ fontSize: "large"
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ Button {
+ text: i18n.tr("Open System Settings")
+ visible: !root.pebble.connected
+ onClicked: Qt.openUrlExternally("settings://system/bluetooth")
+ color: UbuntuColors.orange
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ Label {
+ text: i18n.tr("Your Pebble smartwatch is in factory mode and needs to be initialized.")
+ width: parent.width - units.gu(4)
+ anchors.horizontalCenter: parent.horizontalCenter
+ wrapMode: Text.WordWrap
+ visible: root.pebble.connected && root.pebble.recovery && !root.pebble.upgradingFirmware
+ fontSize: "large"
+ horizontalAlignment: Text.AlignHCenter
+ }
+ Button {
+ text: i18n.tr("Initialize Pebble")
+ onClicked: root.pebble.performFirmwareUpgrade();
+ visible: root.pebble.connected && root.pebble.recovery && !root.pebble.upgradingFirmware
+ color: UbuntuColors.orange
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ Rectangle {
+ id: upgradeIcon
+ height: units.gu(10)
+ width: height
+ radius: width / 2
+ color: UbuntuColors.orange
+ anchors.horizontalCenter: parent.horizontalCenter
+ Icon {
+ anchors.fill: parent
+ anchors.margins: units.gu(1)
+ name: "preferences-system-updates-symbolic"
+ color: "white"
+ }
+
+ RotationAnimation on rotation {
+ duration: 2000
+ loops: Animation.Infinite
+ from: 0
+ to: 360
+ running: upgradeIcon.visible
+ }
+ visible: root.pebble.connected && root.pebble.upgradingFirmware
+ }
+
+ Label {
+ text: i18n.tr("Upgrading...")
+ fontSize: "large"
+ anchors.horizontalCenter: parent.horizontalCenter
+ visible: root.pebble.connected && root.pebble.upgradingFirmware
+ }
+
+ Repeater {
+ id: menuRepeater
+ model: root.pebble.connected && !root.pebble.recovery && !root.pebble.upgradingFirmware ? mainMenuModel : null
+ delegate: ListItem {
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: units.gu(1)
+
+ UbuntuShape {
+ Layout.fillHeight: true
+ Layout.preferredWidth: height
+ backgroundColor: model.color
+ Icon {
+ anchors.fill: parent
+ anchors.margins: units.gu(.5)
+ name: model.icon
+ color: "white"
+ }
+ }
+
+
+ Label {
+ text: model.text
+ Layout.fillWidth: true
+ }
+ }
+
+ onClicked: {
+ var options = {};
+ options["pebble"] = root.pebble
+ var modelItem = mainMenuModel.get(index)
+ options["showWatchApps"] = modelItem.showWatchApps
+ options["showWatchFaces"] = modelItem.showWatchFaces
+ pageStack.push(Qt.resolvedUrl(model.page), options)
+ }
+ }
+ }
+ }
+ }
+
+ Connections {
+ target: pebble
+ onOpenURL: {
+ if (url) {
+ pageStack.push(Qt.resolvedUrl("AppSettingsPage.qml"), {uuid: uuid, url: url, pebble: pebble})
+ }
+ }
+ }
+}
diff --git a/rockwork/NotificationsPage.qml b/rockwork/NotificationsPage.qml
new file mode 100644
index 0000000..9802b05
--- /dev/null
+++ b/rockwork/NotificationsPage.qml
@@ -0,0 +1,88 @@
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.3
+import RockWork 1.0
+
+Page {
+ id: root
+ title: i18n.tr("Notifications")
+
+ property var pebble: null
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.topMargin: units.gu(1)
+
+ Item {
+ Layout.fillWidth: true
+ implicitHeight: infoLabel.height
+
+ Label {
+ id: infoLabel
+ anchors {
+ left: parent.left
+ right: parent.right
+ margins: units.gu(2)
+ }
+
+ wrapMode: Text.WordWrap
+ text: i18n.tr("Entries here will be added as notifications appear on the phone. Selected notifications will be shown on your Pebble smartwatch.")
+ }
+ }
+
+
+ ListView {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ clip: true
+ model: root.pebble.notifications
+
+ delegate: ListItem {
+ ListItemLayout {
+ title.text: model.name
+
+ UbuntuShape {
+ SlotsLayout.position: SlotsLayout.Leading;
+ height: units.gu(5)
+ width: height
+ backgroundColor: {
+ // Add some hacks for known icons
+ switch (model.icon) {
+ case "calendar":
+ return UbuntuColors.orange;
+ case "settings":
+ return "grey";
+ case "dialog-question-symbolic":
+ return UbuntuColors.red;
+ case "alarm-clock":
+ return UbuntuColors.purple;
+ case "gpm-battery-050":
+ return UbuntuColors.green;
+ }
+ return "black"
+ }
+ source: Image {
+ height: parent.height
+ width: parent.width
+ source: model.icon.indexOf("/") === 0 ? "file://" + model.icon : ""
+ }
+ Icon {
+ anchors.fill: parent
+ anchors.margins: units.gu(.5)
+ name: model.icon.indexOf("/") !== 0 ? model.icon : ""
+ color: "white"
+ }
+ }
+
+ Switch {
+ checked: model.enabled
+ SlotsLayout.position: SlotsLayout.Trailing;
+ onClicked: {
+ root.pebble.setNotificationFilter(model.name, checked)
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/rockwork/PebbleModels.qml b/rockwork/PebbleModels.qml
new file mode 100644
index 0000000..103064a
--- /dev/null
+++ b/rockwork/PebbleModels.qml
@@ -0,0 +1,28 @@
+import QtQuick 2.4
+
+ListModel {
+ id: modelModel
+ ListElement { image: 'artwork/tintin-black.png'; shape: "rectangle" } // Fallback for Unknown
+ ListElement { image: 'artwork/tintin-black.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/tintin-white.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/tintin-red.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/tintin-orange.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/tintin-grey.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/bianca-silver.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/bianca-black.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/tintin-blue.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/tintin-green.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/tintin-pink.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/snowy-white.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/snowy-black.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/snowy-red.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/bobby-silver.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/bobby-black.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/bobby-gold.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/spalding-14mm-silver.png'; shape: "round" }
+ ListElement { image: 'artwork/spalding-14mm-black.png'; shape: "round" }
+ ListElement { image: 'artwork/spalding-20mm-silver.png'; shape: "round" }
+ ListElement { image: 'artwork/spalding-20mm-black.png'; shape: "round" }
+ ListElement { image: 'artwork/spalding-14mm-rose-gold.png'; shape: "round" }
+}
+
diff --git a/rockwork/PebblesPage.qml b/rockwork/PebblesPage.qml
new file mode 100644
index 0000000..a973b0a
--- /dev/null
+++ b/rockwork/PebblesPage.qml
@@ -0,0 +1,69 @@
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.3
+
+Page {
+ title: i18n.tr("Manage Pebble Watches")
+
+ head {
+ actions: [
+ Action {
+ iconName: "settings"
+ onTriggered: {
+ onClicked: Qt.openUrlExternally("settings://system/bluetooth")
+ }
+ }
+ ]
+ }
+
+ ListView {
+ anchors.fill: parent
+ model: pebbles
+ delegate: ListItem {
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: units.gu(1)
+
+ ColumnLayout {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+
+ Label {
+ text: model.name
+ }
+
+ Label {
+ text: model.connected ? i18n.tr("Connected") : i18n.tr("Disconnected")
+ fontSize: "small"
+ }
+ }
+ }
+
+ onClicked: {
+ var p = pebbles.get(index);
+ print("opening pebble:", p.name, p.hardwarePlatform)
+ pageStack.push(Qt.resolvedUrl("MainMenuPage.qml"), {pebble: pebbles.get(index)})
+ }
+ }
+ }
+
+ Column {
+ anchors.centerIn: parent
+ width: parent.width - units.gu(4)
+ spacing: units.gu(4)
+ visible: pebbles.count === 0
+
+ Label {
+ text: i18n.tr("No Pebble smartwatches configured yet. Please connect your Pebble smartwatch using System Settings.")
+ fontSize: "large"
+ width: parent.width
+ wrapMode: Text.WordWrap
+ }
+
+ Button {
+ text: i18n.tr("Open System Settings")
+ anchors.horizontalCenter: parent.horizontalCenter
+ onClicked: Qt.openUrlExternally("settings://system/bluetooth")
+ }
+ }
+}
diff --git a/rockwork/ScreenshotsPage.qml b/rockwork/ScreenshotsPage.qml
new file mode 100644
index 0000000..fdbeb9a
--- /dev/null
+++ b/rockwork/ScreenshotsPage.qml
@@ -0,0 +1,107 @@
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.3
+import Ubuntu.Components.Popups 1.3
+import Ubuntu.Content 1.3
+import RockWork 1.0
+
+Page {
+ id: root
+
+ title: i18n.tr("Screenshots")
+
+ property var pebble: null
+
+ head {
+ actions: [
+ Action {
+ iconName: "camera-app-symbolic"
+ onTriggered: root.pebble.requestScreenshot()
+ }
+ ]
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: units.gu(1)
+ spacing: units.gu(1)
+
+ GridView {
+ id: grid
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ clip: true
+
+ property int columns: 2
+
+ cellWidth: width / columns
+ cellHeight: cellWidth
+
+ model: root.pebble.screenshots
+
+ displaced: Transition {
+ UbuntuNumberAnimation { properties: "x,y" }
+ }
+
+ delegate: Item {
+ width: grid.cellWidth
+ height: grid.cellHeight
+ Image {
+ anchors.fill: parent
+ anchors.margins: units.gu(.5)
+ fillMode: Image.PreserveAspectFit
+ source: "file://" + model.filename
+ }
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ PopupUtils.open(dialogComponent, root, {filename: model.filename})
+ }
+ }
+ }
+ }
+ }
+
+ Component {
+ id: dialogComponent
+ Dialog {
+ id: dialog
+ title: i18n.tr("Screenshot options")
+
+ property string filename
+
+ Button {
+ text: i18n.tr("Share")
+ color: UbuntuColors.blue
+ onClicked: {
+ pageStack.push(Qt.resolvedUrl("ContentPeerPickerPage.qml"), {itemName: i18n.tr("Pebble screenshot"), handler: ContentHandler.Share, contentType: ContentType.Pictures, filename: filename })
+ PopupUtils.close(dialog)
+ }
+ }
+ Button {
+ text: i18n.tr("Save")
+ color: UbuntuColors.green
+ onClicked: {
+ pageStack.push(Qt.resolvedUrl("ContentPeerPickerPage.qml"), {itemName: i18n.tr("Pebble screenshot"),handler: ContentHandler.Destination, contentType: ContentType.Pictures, filename: filename })
+ PopupUtils.close(dialog)
+ }
+ }
+
+ Button {
+ text: i18n.tr("Delete")
+ color: UbuntuColors.red
+ onClicked: {
+ root.pebble.removeScreenshot(filename)
+ PopupUtils.close(dialog)
+ }
+ }
+ Button {
+ text: i18n.tr("Cancel")
+ onClicked: {
+ PopupUtils.close(dialog)
+ }
+ }
+ }
+ }
+}
+
diff --git a/rockwork/SettingsPage.qml b/rockwork/SettingsPage.qml
new file mode 100644
index 0000000..153aaf4
--- /dev/null
+++ b/rockwork/SettingsPage.qml
@@ -0,0 +1,80 @@
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.3
+import Ubuntu.Components.ListItems 1.3
+
+Page {
+ id: root
+ title: i18n.tr("Settings")
+
+ property var pebble: null
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: units.gu(1)
+ spacing: units.gu(1)
+
+ Label {
+ Layout.fillWidth: true
+ text: i18n.tr("Distance Units")
+ font.bold: true
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ CheckBox {
+ id: metricUnitsCheckbox
+ checked: !root.pebble.imperialUnits
+ onClicked: {
+ checked = true
+ root.pebble.imperialUnits = false;
+ imperialUnitsCheckBox.checked = false;
+ }
+ }
+ Label {
+ text: i18n.tr("Metric")
+ Layout.fillWidth: true
+ }
+ CheckBox {
+ id: imperialUnitsCheckBox
+ checked: root.pebble.imperialUnits
+ onClicked: {
+ checked = true
+ root.pebble.imperialUnits = true;
+ metricUnitsCheckbox.checked = false;
+ }
+ }
+ Label {
+ text: i18n.tr("Imperial")
+ Layout.fillWidth: true
+ }
+ }
+ ThinDivider {}
+
+ Label {
+ text: i18n.tr("Calendar")
+ Layout.fillWidth: true
+ font.bold: true
+ }
+ RowLayout {
+ Layout.fillWidth: true
+ Label {
+ text: i18n.tr("Sync calendar to timeline")
+ Layout.fillWidth: true
+ }
+ Switch {
+ checked: root.pebble.calendarSyncEnabled
+ onClicked: {
+ root.pebble.calendarSyncEnabled = checked;
+ }
+ }
+ }
+ ThinDivider {}
+
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+ }
+}
+
diff --git a/rockwork/SystemAppIcon.qml b/rockwork/SystemAppIcon.qml
new file mode 100644
index 0000000..88e37bc
--- /dev/null
+++ b/rockwork/SystemAppIcon.qml
@@ -0,0 +1,67 @@
+import QtQuick 2.4
+import Ubuntu.Components 1.3
+
+Item {
+ id: root
+
+ property bool isSystemApp: false
+ property string uuid: ""
+ property string iconSource: ""
+
+ UbuntuShape {
+ anchors.fill: parent
+ visible: root.isSystemApp
+ backgroundColor: {
+ switch (root.uuid) {
+ case "{07e0d9cb-8957-4bf7-9d42-35bf47caadfe}":
+ return "gray";
+ case "{18e443ce-38fd-47c8-84d5-6d0c775fbe55}":
+ return "blue";
+ case "{36d8c6ed-4c83-4fa1-a9e2-8f12dc941f8c}":
+ return UbuntuColors.red;
+ case "{1f03293d-47af-4f28-b960-f2b02a6dd757}":
+ return "gold"
+ case "{b2cae818-10f8-46df-ad2b-98ad2254a3c1}":
+ return "darkviolet"
+ case "{67a32d95-ef69-46d4-a0b9-854cc62f97f9}":
+ return "green";
+ case "{8f3c8686-31a1-4f5f-91f5-01600c9bdc59}":
+ return "black"
+ }
+
+ return "";
+ }
+ }
+ Icon {
+ anchors.fill: parent
+ implicitHeight: height
+ anchors.margins: units.gu(1)
+ visible: root.isSystemApp
+ color: "white"
+ name: {
+ switch (root.uuid) {
+ case "{07e0d9cb-8957-4bf7-9d42-35bf47caadfe}":
+ return "settings";
+ case "{18e443ce-38fd-47c8-84d5-6d0c775fbe55}":
+ return "clock-app-symbolic";
+ case "{36d8c6ed-4c83-4fa1-a9e2-8f12dc941f8c}":
+ return "like";
+ case "{1f03293d-47af-4f28-b960-f2b02a6dd757}":
+ return "stock_music";
+ case "{b2cae818-10f8-46df-ad2b-98ad2254a3c1}":
+ return "stock_notification";
+ case "{67a32d95-ef69-46d4-a0b9-854cc62f97f9}":
+ return "stock_alarm-clock";
+ case "{8f3c8686-31a1-4f5f-91f5-01600c9bdc59}":
+ return "clock-app-symbolic";
+ }
+ return "";
+ }
+ }
+
+ Image {
+ source: root.isSystemApp ? "" : "file://" + root.iconSource;
+ anchors.fill: parent
+ visible: !root.isSystemApp
+ }
+}
diff --git a/rockwork/applicationsfiltermodel.cpp b/rockwork/applicationsfiltermodel.cpp
new file mode 100644
index 0000000..d3eb10d
--- /dev/null
+++ b/rockwork/applicationsfiltermodel.cpp
@@ -0,0 +1,102 @@
+#include "applicationsfiltermodel.h"
+#include "applicationsmodel.h"
+
+ApplicationsFilterModel::ApplicationsFilterModel(QObject *parent):
+ QSortFilterProxyModel(parent)
+{
+ sort(0);
+}
+
+ApplicationsModel *ApplicationsFilterModel::appsModel() const
+{
+ return m_appsModel;
+}
+
+void ApplicationsFilterModel::setAppsModel(ApplicationsModel *model)
+{
+ if (m_appsModel != model) {
+ m_appsModel = model;
+ setSourceModel(m_appsModel);
+ emit appsModelChanged();
+ }
+}
+
+bool ApplicationsFilterModel::showWatchApps() const
+{
+ return m_showWatchApps;
+}
+
+void ApplicationsFilterModel::setShowWatchApps(bool showWatchApps)
+{
+ if (m_showWatchApps != showWatchApps) {
+ m_showWatchApps = showWatchApps;
+ emit showWatchAppsChanged();
+ invalidateFilter();
+ }
+}
+
+bool ApplicationsFilterModel::showWatchFaces() const
+{
+ return m_showWatchFaces;
+}
+
+void ApplicationsFilterModel::setShowWatchFaces(bool showWatchFaces)
+{
+ if (m_showWatchFaces != showWatchFaces) {
+ m_showWatchFaces = showWatchFaces;
+ emit showWatchFacesChanged();
+ invalidateFilter();
+ }
+}
+
+bool ApplicationsFilterModel::sortByGroupId() const
+{
+ return m_sortByGroupId;
+}
+
+void ApplicationsFilterModel::setSortByGroupId(bool sortByGroupId)
+{
+ if (m_sortByGroupId != sortByGroupId) {
+ m_sortByGroupId = sortByGroupId;
+ emit sortByGroupIdChanged();
+ sort(0);
+ }
+}
+
+bool ApplicationsFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
+{
+ Q_UNUSED(source_parent)
+ AppItem *item = m_appsModel->get(source_row);
+ if (m_showWatchApps && !item->isWatchFace()) {
+ return true;
+ }
+ if (m_showWatchFaces && item->isWatchFace()) {
+ return true;
+ }
+ return false;
+}
+
+bool ApplicationsFilterModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
+{
+ AppItem *leftItem = m_appsModel->get(source_left.row());
+ AppItem *rightItem = m_appsModel->get(source_right.row());
+
+ if (m_sortByGroupId && leftItem->groupId() != rightItem->groupId()) {
+ return leftItem->groupId() < rightItem->groupId();
+ }
+
+ return QSortFilterProxyModel::lessThan(source_left, source_right);
+}
+
+AppItem* ApplicationsFilterModel::get(int index) const
+{
+ return m_appsModel->get(mapToSource(this->index(index, 0)).row());
+}
+
+void ApplicationsFilterModel::move(int from, int to)
+{
+ QModelIndex sourceFrom = mapToSource(index(from, 0));
+ QModelIndex sourceTo = mapToSource(index(to, 0));
+ m_appsModel->move(sourceFrom.row(), sourceTo.row());
+}
+
diff --git a/rockwork/applicationsfiltermodel.h b/rockwork/applicationsfiltermodel.h
new file mode 100644
index 0000000..96c3b5c
--- /dev/null
+++ b/rockwork/applicationsfiltermodel.h
@@ -0,0 +1,54 @@
+#ifndef APPLICATIONSFILTERMODEL_H
+#define APPLICATIONSFILTERMODEL_H
+
+#include <QSortFilterProxyModel>
+
+class ApplicationsModel;
+class AppItem;
+
+class ApplicationsFilterModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+ Q_PROPERTY(ApplicationsModel* model READ appsModel WRITE setAppsModel NOTIFY appsModelChanged)
+ Q_PROPERTY(bool showWatchApps READ showWatchApps WRITE setShowWatchApps NOTIFY showWatchAppsChanged)
+ Q_PROPERTY(bool showWatchFaces READ showWatchFaces WRITE setShowWatchFaces NOTIFY showWatchFacesChanged)
+ Q_PROPERTY(bool sortByGroupId READ sortByGroupId WRITE setSortByGroupId NOTIFY sortByGroupIdChanged)
+
+public:
+ ApplicationsFilterModel(QObject *parent = nullptr);
+
+ ApplicationsModel *appsModel() const;
+ void setAppsModel(ApplicationsModel *model);
+
+ bool showWatchApps() const;
+ void setShowWatchApps(bool showWatchApps);
+
+ bool showWatchFaces() const;
+ void setShowWatchFaces(bool showWatchFaces);
+
+ bool sortByGroupId() const;
+ void setSortByGroupId(bool sortByGroupId);
+
+ bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
+ bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override;
+
+ Q_INVOKABLE AppItem *get(int index) const;
+
+ Q_INVOKABLE void move(int from, int to);
+signals:
+ void appsModelChanged();
+ void showWatchAppsChanged();
+ void showWatchFacesChanged();
+ void sortByGroupIdChanged();
+
+public slots:
+
+private:
+ ApplicationsModel *m_appsModel;
+
+ bool m_showWatchApps = true;
+ bool m_showWatchFaces = true;
+ bool m_sortByGroupId = true;
+};
+
+#endif // APPLICATIONSFILTERMODEL_H
diff --git a/rockwork/applicationsmodel.cpp b/rockwork/applicationsmodel.cpp
new file mode 100644
index 0000000..27e2c3e
--- /dev/null
+++ b/rockwork/applicationsmodel.cpp
@@ -0,0 +1,365 @@
+#include "applicationsmodel.h"
+
+#include <QDebug>
+
+
+ApplicationsModel::ApplicationsModel(QObject *parent):
+ QAbstractListModel(parent)
+{
+}
+
+int ApplicationsModel::rowCount(const QModelIndex &parent) const
+{
+ Q_UNUSED(parent)
+ return m_apps.count();
+}
+
+QVariant ApplicationsModel::data(const QModelIndex &index, int role) const
+{
+ switch (role) {
+ case RoleStoreId:
+ return m_apps.at(index.row())->storeId();
+ case RoleUuid:
+ return m_apps.at(index.row())->uuid();
+ case RoleName:
+ return m_apps.at(index.row())->name();
+ case RoleIcon:
+ return m_apps.at(index.row())->icon();
+ case RoleVendor:
+ return m_apps.at(index.row())->vendor();
+ case RoleVersion:
+ return m_apps.at(index.row())->version();
+ case RoleIsWatchFace:
+ return m_apps.at(index.row())->isWatchFace();
+ case RoleIsSystemApp:
+ return m_apps.at(index.row())->isSystemApp();
+ case RoleHasSettings:
+ return m_apps.at(index.row())->hasSettings();
+ case RoleDescription:
+ return m_apps.at(index.row())->description();
+ case RoleHearts:
+ return m_apps.at(index.row())->hearts();
+ case RoleCategory:
+ return m_apps.at(index.row())->category();
+ case RoleGroupId:
+ return m_apps.at(index.row())->groupId();
+ }
+
+ return QVariant();
+}
+
+QHash<int, QByteArray> ApplicationsModel::roleNames() const
+{
+ QHash<int, QByteArray> roles;
+ roles.insert(RoleStoreId, "storeId");
+ roles.insert(RoleUuid, "uuid");
+ roles.insert(RoleName, "name");
+ roles.insert(RoleIcon, "icon");
+ roles.insert(RoleVendor, "vendor");
+ roles.insert(RoleVersion, "version");
+ roles.insert(RoleIsWatchFace, "isWatchFace");
+ roles.insert(RoleIsSystemApp, "isSystemApp");
+ roles.insert(RoleHasSettings, "hasSettings");
+ roles.insert(RoleDescription, "description");
+ roles.insert(RoleHearts, "hearts");
+ roles.insert(RoleCategory, "category");
+ roles.insert(RoleGroupId, "groupId");
+ return roles;
+}
+
+void ApplicationsModel::clear()
+{
+ beginResetModel();
+ qDeleteAll(m_apps);
+ m_apps.clear();
+ endResetModel();
+ m_groupNames.clear();
+ m_groupLinks.clear();
+ m_links.clear();
+ m_linkNames.clear();
+ emit linksChanged();
+ emit changed();
+}
+
+void ApplicationsModel::insert(AppItem *item)
+{
+ item->setParent(this);
+ beginInsertRows(QModelIndex(), rowCount(), rowCount());
+ m_apps.append(item);
+ endInsertRows();
+ emit changed();
+}
+
+void ApplicationsModel::insertGroup(const QString &id, const QString &name, const QString &link)
+{
+ m_groupNames[id] = name;
+ m_groupLinks[id] = link;
+}
+
+AppItem *ApplicationsModel::get(int index) const
+{
+ if (index >= 0 && index < m_apps.count()) {
+ return m_apps.at(index);
+ }
+ return nullptr;
+}
+
+AppItem *ApplicationsModel::findByStoreId(const QString &storeId) const
+{
+ foreach (AppItem *item, m_apps) {
+ if (item->storeId() == storeId) {
+ return item;
+ }
+ }
+ return nullptr;
+}
+
+AppItem *ApplicationsModel::findByUuid(const QString &uuid) const
+{
+ foreach (AppItem *item, m_apps) {
+ if (item->uuid() == uuid) {
+ return item;
+ }
+ }
+ return nullptr;
+}
+
+bool ApplicationsModel::contains(const QString &storeId) const
+{
+ foreach (AppItem* item, m_apps) {
+ if (item->storeId() == storeId) {
+ return true;
+ }
+ }
+ return false;
+}
+
+int ApplicationsModel::indexOf(AppItem *item) const
+{
+ return m_apps.indexOf(item);
+}
+
+QString ApplicationsModel::groupName(const QString &groupId) const
+{
+ return m_groupNames.value(groupId);
+}
+
+QString ApplicationsModel::groupLink(const QString &groupId) const
+{
+ return m_groupLinks.value(groupId);
+}
+
+QString ApplicationsModel::linkName(const QString &link) const
+{
+ return m_linkNames.value(link);
+}
+
+QStringList ApplicationsModel::links() const
+{
+ return m_links;
+}
+
+void ApplicationsModel::addLink(const QString &link, const QString &name)
+{
+ m_links.append(link);
+ m_linkNames[link] = name;
+ emit linksChanged();
+}
+
+void ApplicationsModel::move(int from, int to)
+{
+ if (from < 0 || to < 0) {
+ return;
+ }
+ if (from >= m_apps.count() || to >= m_apps.count()) {
+ return;
+ }
+ if (from == to) {
+ return;
+ }
+ int newModelIndex = to > from ? to + 1 : to;
+ beginMoveRows(QModelIndex(), from, from, QModelIndex(), newModelIndex);
+
+ m_apps.move(from, to);
+ QStringList appList;
+ foreach (const AppItem *item, m_apps) {
+ appList << item->name();
+ }
+ endMoveRows();
+}
+
+void ApplicationsModel::commitMove()
+{
+ emit appsSorted();
+}
+
+AppItem::AppItem(QObject *parent):
+ QObject(parent)
+{
+
+}
+
+QString AppItem::storeId() const
+{
+ return m_storeId;
+}
+
+QString AppItem::uuid() const
+{
+ return m_uuid;
+}
+
+QString AppItem::name() const
+{
+ return m_name;
+}
+
+QString AppItem::icon() const
+{
+ return m_icon;
+}
+
+QString AppItem::vendor() const
+{
+ return m_vendor;
+}
+
+QString AppItem::version() const
+{
+ return m_version;
+}
+
+QString AppItem::description() const
+{
+ return m_description;
+}
+
+int AppItem::hearts() const
+{
+ return m_hearts;
+}
+
+QStringList AppItem::screenshotImages() const
+{
+ return m_screenshotImages;
+}
+
+bool AppItem::isWatchFace() const
+{
+ return m_isWatchFace;
+}
+
+bool AppItem::isSystemApp() const
+{
+ return m_isSystemApp;
+}
+
+bool AppItem::hasSettings() const
+{
+ return m_hasSettings;
+}
+
+bool AppItem::companion() const
+{
+ return m_companion;
+}
+
+QString AppItem::category() const
+{
+ return m_category;
+}
+
+QString AppItem::groupId() const
+{
+ return m_groupId;
+}
+
+void AppItem::setStoreId(const QString &storeId)
+{
+ m_storeId = storeId;
+}
+
+void AppItem::setUuid(const QString &uuid)
+{
+ m_uuid = uuid;
+}
+
+void AppItem::setName(const QString &name)
+{
+ m_name = name;
+}
+
+void AppItem::setIcon(const QString &icon)
+{
+ m_icon = icon;
+}
+
+void AppItem::setVendor(const QString &vendor)
+{
+ m_vendor = vendor;
+ emit vendorChanged();
+}
+
+void AppItem::setVersion(const QString &version)
+{
+ m_version = version;
+ emit versionChanged();
+}
+
+void AppItem::setDescription(const QString &description)
+{
+ m_description = description;
+}
+
+void AppItem::setHearts(int hearts)
+{
+ m_hearts = hearts;
+}
+
+void AppItem::setIsWatchFace(bool isWatchFace)
+{
+ m_isWatchFace = isWatchFace;
+ emit isWatchFaceChanged();
+}
+
+void AppItem::setIsSystemApp(bool isSystemApp)
+{
+ m_isSystemApp = isSystemApp;
+}
+
+void AppItem::setHasSettings(bool hasSettings)
+{
+ m_hasSettings = hasSettings;
+}
+
+void AppItem::setCompanion(bool companion)
+{
+ m_companion = companion;
+}
+
+void AppItem::setCategory(const QString &category)
+{
+ m_category = category;
+}
+
+void AppItem::setScreenshotImages(const QStringList &screenshotImages)
+{
+ m_screenshotImages = screenshotImages;
+}
+
+void AppItem::setHeaderImage(const QString &headerImage)
+{
+ m_headerImage = headerImage;
+ emit headerImageChanged();
+}
+
+void AppItem::setGroupId(const QString &groupId)
+{
+ m_groupId = groupId;
+}
+
+QString AppItem::headerImage() const
+{
+ return m_headerImage;
+}
+
diff --git a/rockwork/applicationsmodel.h b/rockwork/applicationsmodel.h
new file mode 100644
index 0000000..91539bc
--- /dev/null
+++ b/rockwork/applicationsmodel.h
@@ -0,0 +1,160 @@
+#ifndef APPLICATIONSMODEL_H
+#define APPLICATIONSMODEL_H
+
+#include <QAbstractListModel>
+#include <QDBusObjectPath>
+
+class QDBusInterface;
+
+class AppItem: public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QString storeId MEMBER m_storeId CONSTANT)
+ Q_PROPERTY(QString uuid MEMBER m_uuid CONSTANT)
+ Q_PROPERTY(QString name MEMBER m_name CONSTANT)
+ Q_PROPERTY(QString icon MEMBER m_icon CONSTANT)
+ Q_PROPERTY(QString vendor MEMBER m_vendor NOTIFY vendorChanged)
+ Q_PROPERTY(QString version MEMBER m_version NOTIFY versionChanged)
+ Q_PROPERTY(QString description MEMBER m_description CONSTANT)
+ Q_PROPERTY(int hearts MEMBER m_hearts CONSTANT)
+ Q_PROPERTY(QStringList screenshotImages MEMBER m_screenshotImages CONSTANT)
+ Q_PROPERTY(QString headerImage READ headerImage NOTIFY headerImageChanged)
+ Q_PROPERTY(QString category MEMBER m_category CONSTANT)
+ Q_PROPERTY(bool isWatchFace MEMBER m_isWatchFace NOTIFY isWatchFaceChanged)
+ Q_PROPERTY(bool isSystemApp MEMBER m_isSystemApp CONSTANT)
+ Q_PROPERTY(bool hasSettings MEMBER m_hasSettings CONSTANT)
+ Q_PROPERTY(bool companion MEMBER m_companion CONSTANT)
+
+ Q_PROPERTY(QString groupId MEMBER m_groupId CONSTANT)
+
+
+public:
+ AppItem(QObject *parent = 0);
+
+ QString storeId() const;
+ QString uuid() const;
+ QString name() const;
+ QString icon() const;
+ QString vendor() const;
+ QString version() const;
+ QString description() const;
+ int hearts() const;
+ QStringList screenshotImages() const;
+ QString headerImage() const;
+ bool isWatchFace() const;
+ bool isSystemApp() const;
+ bool hasSettings() const;
+ bool companion() const;
+ QString category() const;
+
+ QString groupId() const;
+
+ void setStoreId(const QString &storeId);
+ void setUuid(const QString &uuid);
+ void setName(const QString &name);
+ void setIcon(const QString &icon);
+ void setVendor(const QString &vendor);
+ void setVersion(const QString &version);
+ void setDescription(const QString &description);
+ void setHearts(int hearts);
+ void setCategory(const QString &category);
+ void setScreenshotImages(const QStringList &screenshotImages);
+ void setHeaderImage(const QString &headerImage);
+ void setIsWatchFace(bool isWatchFace);
+ void setIsSystemApp(bool isSystemApp);
+ void setHasSettings(bool hasSettings);
+ void setCompanion(bool companion);
+
+ // For grouping in lists, e.g. by collection
+ void setGroupId(const QString &groupId);
+
+
+signals:
+ void versionChanged();
+ void vendorChanged();
+ void headerImageChanged();
+ void isWatchFaceChanged();
+
+private:
+ QString m_storeId;
+ QString m_uuid;
+ QString m_name;
+ QString m_icon;
+ QString m_vendor;
+ QString m_version;
+ QString m_description;
+ int m_hearts = 0;
+ QString m_category;
+ QStringList m_screenshotImages;
+ bool m_isWatchFace = false;
+ bool m_isSystemApp = false;
+ bool m_hasSettings = false;
+ bool m_companion = false;
+
+ QString m_groupId;
+
+ QString m_headerImage;
+};
+
+class ApplicationsModel : public QAbstractListModel
+{
+ Q_OBJECT
+ Q_PROPERTY(QStringList links READ links NOTIFY linksChanged)
+
+public:
+ enum Roles {
+ RoleStoreId,
+ RoleUuid,
+ RoleName,
+ RoleIcon,
+ RoleVendor,
+ RoleVersion,
+ RoleIsWatchFace,
+ RoleIsSystemApp,
+ RoleHasSettings,
+ RoleDescription,
+ RoleHearts,
+ RoleCategory,
+ RoleGroupId
+ };
+
+ ApplicationsModel(QObject *parent = nullptr);
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ QHash<int, QByteArray> roleNames() const override;
+
+ void clear();
+ void insert(AppItem *item);
+ void insertGroup(const QString &id, const QString &name, const QString &link);
+
+ Q_INVOKABLE AppItem* get(int index) const;
+ AppItem* findByStoreId(const QString &storeId) const;
+ AppItem* findByUuid(const QString &uuid) const;
+ Q_INVOKABLE bool contains(const QString &storeId) const;
+ int indexOf(AppItem *item) const;
+
+ Q_INVOKABLE QString groupName(const QString &groupId) const;
+ Q_INVOKABLE QString groupLink(const QString &groupId) const;
+
+ QStringList links() const;
+ Q_INVOKABLE QString linkName(const QString &link) const;
+ void addLink(const QString &link, const QString &name);
+
+ Q_INVOKABLE void move(int from, int to);
+ Q_INVOKABLE void commitMove();
+
+signals:
+ void linksChanged();
+ void appsSorted();
+ void changed();
+
+private:
+ QList<AppItem*> m_apps;
+ QHash<QString, QString> m_groupNames;
+ QHash<QString, QString> m_groupLinks;
+ QStringList m_links;
+ QHash<QString, QString> m_linkNames;
+};
+
+#endif // APPLICATIONSMODEL_H
diff --git a/rockwork/appstoreclient.cpp b/rockwork/appstoreclient.cpp
new file mode 100644
index 0000000..ac87510
--- /dev/null
+++ b/rockwork/appstoreclient.cpp
@@ -0,0 +1,323 @@
+#include "appstoreclient.h"
+#include "applicationsmodel.h"
+
+#include <QNetworkAccessManager>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QUrlQuery>
+#include <QJsonDocument>
+#include <QJsonParseError>
+
+#include <libintl.h>
+
+/* Known params for pebble api
+ query.addQueryItem("offset", QString::number(offset));
+ query.addQueryItem("limit", QString::number(limit));
+ query.addQueryItem("image_ratio", "1"); // Not sure yet what this does
+ query.addQueryItem("filter_hardware", "true");
+ query.addQueryItem("firmware_version", "3");
+ query.addQueryItem("hardware", hardwarePlatform);
+ query.addQueryItem("platform", "all");
+*/
+
+AppStoreClient::AppStoreClient(QObject *parent):
+ QObject(parent),
+ m_nam(new QNetworkAccessManager(this)),
+ m_model(new ApplicationsModel(this))
+{
+}
+
+ApplicationsModel *AppStoreClient::model() const
+{
+ return m_model;
+}
+
+int AppStoreClient::limit() const
+{
+ return m_limit;
+}
+
+void AppStoreClient::setLimit(int limit)
+{
+ m_limit = limit;
+ emit limitChanged();
+}
+
+QString AppStoreClient::hardwarePlatform() const
+{
+ return m_hardwarePlatform;
+}
+
+void AppStoreClient::setHardwarePlatform(const QString &hardwarePlatform)
+{
+ m_hardwarePlatform = hardwarePlatform;
+ emit hardwarePlatformChanged();
+}
+
+bool AppStoreClient::busy() const
+{
+ return m_busy;
+}
+
+void AppStoreClient::fetchHome(Type type)
+{
+ m_model->clear();
+ setBusy(true);
+
+ QUrlQuery query;
+ query.addQueryItem("firmware_version", "3");
+ if (!m_hardwarePlatform.isEmpty()) {
+ query.addQueryItem("hardware", m_hardwarePlatform);
+ query.addQueryItem("filter_hardware", "true");
+ }
+
+ QString url;
+ if (type == TypeWatchapp) {
+ url = "https://api2.getpebble.com/v2/home/apps";
+ } else {
+ url = "https://api2.getpebble.com/v2/home/watchfaces";
+ }
+ QUrl storeUrl(url);
+ storeUrl.setQuery(query);
+ QNetworkRequest request(storeUrl);
+
+ qDebug() << "fetching home" << storeUrl.toString();
+ QNetworkReply *reply = m_nam->get(request);
+ connect(reply, &QNetworkReply::finished, [this, reply]() {
+ QByteArray data = reply->readAll();
+ reply->deleteLater();
+
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data);
+ QVariantMap resultMap = jsonDoc.toVariant().toMap();
+
+ QHash<QString, QStringList> collections;
+ foreach (const QVariant &entry, resultMap.value("collections").toList()) {
+ QStringList appIds;
+ foreach (const QVariant &appId, entry.toMap().value("application_ids").toList()) {
+ appIds << appId.toString();
+ }
+ QString slug = entry.toMap().value("slug").toString();
+ collections[slug] = appIds;
+ m_model->insertGroup(slug, entry.toMap().value("name").toString(), entry.toMap().value("links").toMap().value("apps").toString());
+ }
+
+ QHash<QString, QString> categoryNames;
+ foreach (const QVariant &entry, resultMap.value("categories").toList()) {
+ categoryNames[entry.toMap().value("id").toString()] = entry.toMap().value("name").toString();
+ }
+
+ foreach (const QVariant &entry, jsonDoc.toVariant().toMap().value("applications").toList()) {
+ AppItem* item = parseAppItem(entry.toMap());
+ foreach (const QString &collection, collections.keys()) {
+ if (collections.value(collection).contains(item->storeId())) {
+ item->setGroupId(collection);
+ break;
+ }
+ }
+ item->setCategory(categoryNames.value(entry.toMap().value("category_id").toString()));
+
+ qDebug() << "have entry" << item->name() << item->groupId() << item->companion();
+
+ if (item->groupId().isEmpty() || item->companion()) {
+ // Skip items that we couldn't match to a collection
+ // Also skip apps that need a companion
+ delete item;
+ continue;
+ }
+ m_model->insert(item);
+ }
+ setBusy(false);
+ });
+
+
+}
+
+void AppStoreClient::fetchLink(const QString &link)
+{
+ m_model->clear();
+ setBusy(true);
+
+ QUrl storeUrl(link);
+ QUrlQuery query(storeUrl);
+ query.removeQueryItem("limit");
+ // We fetch one more than we actually want so we can see if we need to display
+ // a next button
+ query.addQueryItem("limit", QString::number(m_limit + 1));
+ int currentOffset = query.queryItemValue("offset").toInt();
+ query.removeQueryItem("offset");
+ query.addQueryItem("offset", QString::number(qMax(0, currentOffset - 1)));
+ if (!query.hasQueryItem("hardware")) {
+ query.addQueryItem("hardware", m_hardwarePlatform);
+ query.addQueryItem("filter_hardware", "true");
+ }
+ storeUrl.setQuery(query);
+ QNetworkRequest request(storeUrl);
+ qDebug() << "fetching link" << request.url();
+
+ QNetworkReply *reply = m_nam->get(request);
+ connect(reply, &QNetworkReply::finished, [this, reply]() {
+ qDebug() << "fetch reply";
+ QByteArray data = reply->readAll();
+ reply->deleteLater();
+
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data);
+ QVariantMap resultMap = jsonDoc.toVariant().toMap();
+
+ bool haveMore = false;
+ foreach (const QVariant &entry, resultMap.value("data").toList()) {
+ if (model()->rowCount() >= m_limit) {
+ haveMore = true;
+ break;
+ }
+ AppItem *item = parseAppItem(entry.toMap());
+ if (item->companion()) {
+ // For now just skip items with companions
+ delete item;
+ } else {
+ m_model->insert(item);
+ }
+ }
+
+ if (resultMap.contains("links") && resultMap.value("links").toMap().contains("nextPage") &&
+ !resultMap.value("links").toMap().value("nextPage").isNull()) {
+ int currentOffset = resultMap.value("offset").toInt();
+ QString nextLink = resultMap.value("links").toMap().value("nextPage").toString();
+
+ if (currentOffset > 0) {
+ QUrl previousLink(nextLink);
+ QUrlQuery query(previousLink);
+ query.removeQueryItem("limit");
+ query.addQueryItem("limit", QString::number(m_limit + 1));
+ query.removeQueryItem("offset");
+ query.addQueryItem("offset", QString::number(qMax(0, currentOffset - m_limit + 1)));
+ previousLink.setQuery(query);
+ m_model->addLink(previousLink.toString(), gettext("Previous"));
+ }
+ if (haveMore) {
+ m_model->addLink(nextLink, gettext("Next"));
+ }
+ }
+ setBusy(false);
+ });
+
+}
+
+void AppStoreClient::fetchAppDetails(const QString &appId)
+{
+ QUrl url("https://api2.getpebble.com/v2/apps/id/" + appId);
+ QUrlQuery query;
+ if (!m_hardwarePlatform.isEmpty()) {
+ query.addQueryItem("hardware", m_hardwarePlatform);
+ }
+ url.setQuery(query);
+
+ QNetworkRequest request(url);
+ QNetworkReply * reply = m_nam->get(request);
+ connect(reply, &QNetworkReply::finished, [this, reply, appId]() {
+ reply->deleteLater();
+ AppItem *item = m_model->findByStoreId(appId);
+ if (!item) {
+ qWarning() << "Can't find item with id" << appId;
+ return;
+ }
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll());
+ QVariantMap replyMap = jsonDoc.toVariant().toMap().value("data").toList().first().toMap();
+ if (replyMap.contains("header_images") && replyMap.value("header_images").toList().count() > 0) {
+ item->setHeaderImage(replyMap.value("header_images").toList().first().toMap().value("orig").toString());
+ }
+ item->setVendor(replyMap.value("author").toString());
+ item->setVersion(replyMap.value("latest_release").toMap().value("version").toString());
+ item->setIsWatchFace(replyMap.value("type").toString() == "watchface");
+ });
+}
+
+void AppStoreClient::search(const QString &searchString, Type type)
+{
+ m_model->clear();
+ setBusy(true);
+
+ QUrl url("https://bujatnzd81-dsn.algolia.io/1/indexes/pebble-appstore-production");
+ QUrlQuery query;
+ query.addQueryItem("x-algolia-api-key", "8dbb11cdde0f4f9d7bf787e83ac955ed");
+ query.addQueryItem("x-algolia-application-id", "BUJATNZD81");
+ query.addQueryItem("query", searchString);
+ QStringList filters;
+ if (type == TypeWatchapp) {
+ filters.append("watchapp");
+ } else if (type == TypeWatchface) {
+ filters.append("watchface");
+ }
+ filters.append(m_hardwarePlatform);
+ query.addQueryItem("tagFilters", filters.join(","));
+ url.setQuery(query);
+
+ QNetworkRequest request(url);
+ qDebug() << "Search query:" << url;
+ QNetworkReply *reply = m_nam->get(request);
+ connect(reply, &QNetworkReply::finished, [this, reply]() {
+ m_model->clear();
+ setBusy(false);
+
+ reply->deleteLater();
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll());
+
+ QVariantMap resultMap = jsonDoc.toVariant().toMap();
+ foreach (const QVariant &entry, resultMap.value("hits").toList()) {
+ AppItem *item = parseAppItem(entry.toMap());
+ m_model->insert(item);
+// qDebug() << "have item" << item->name() << item->icon();
+ }
+ qDebug() << "Found" << m_model->rowCount() << "items";
+ });
+}
+
+AppItem* AppStoreClient::parseAppItem(const QVariantMap &map)
+{
+ AppItem *item = new AppItem();
+ item->setStoreId(map.value("id").toString());
+ item->setName(map.value("title").toString());
+ if (!map.value("list_image").toString().isEmpty()) {
+ item->setIcon(map.value("list_image").toString());
+ } else {
+ item->setIcon(map.value("list_image").toMap().value("144x144").toString());
+ }
+ item->setDescription(map.value("description").toString());
+ item->setHearts(map.value("hearts").toInt());
+ item->setCategory(map.value("category_name").toString());
+ item->setCompanion(!map.value("companions").toMap().value("android").isNull() || !map.value("companions").toMap().value("ios").isNull());
+
+ QVariantList screenshotsList = map.value("screenshot_images").toList();
+ // try to get more hardware specific screenshots. The store search keeps them in a subgroup.
+ if (map.contains("asset_collections")) {
+ foreach (const QVariant &assetCollection, map.value("asset_collections").toList()) {
+ if (assetCollection.toMap().value("hardware_platform").toString() == m_hardwarePlatform) {
+ screenshotsList = assetCollection.toMap().value("screenshots").toList();
+ break;
+ }
+ }
+ }
+ QStringList screenshotImages;
+ foreach (const QVariant &screenshotItem, screenshotsList) {
+ if (!screenshotItem.toString().isEmpty()) {
+ screenshotImages << screenshotItem.toString();
+ } else if (screenshotItem.toMap().count() > 0) {
+ screenshotImages << screenshotItem.toMap().first().toString();
+ }
+ }
+ item->setScreenshotImages(screenshotImages);
+// qDebug() << "setting screenshot images" << item->screenshotImages();
+
+ // The search seems to return references to invalid icon images. if we detect that, we'll replace it with a screenshot
+ if (item->icon().contains("aOUhkV1R1uCqCVkKY5Dv") && !item->screenshotImages().isEmpty()) {
+ item->setIcon(item->screenshotImages().first());
+ }
+
+ return item;
+}
+
+void AppStoreClient::setBusy(bool busy)
+{
+ m_busy = busy;
+ emit busyChanged();
+}
+
diff --git a/rockwork/appstoreclient.h b/rockwork/appstoreclient.h
new file mode 100644
index 0000000..5a31d60
--- /dev/null
+++ b/rockwork/appstoreclient.h
@@ -0,0 +1,62 @@
+#ifndef APPSTORECLIENT_H
+#define APPSTORECLIENT_H
+
+#include <QObject>
+
+class QNetworkAccessManager;
+class ApplicationsModel;
+class AppItem;
+
+class AppStoreClient : public QObject
+{
+ Q_OBJECT
+ Q_ENUMS(Type)
+ Q_PROPERTY(ApplicationsModel* model READ model CONSTANT)
+ Q_PROPERTY(int limit READ limit WRITE setLimit NOTIFY limitChanged)
+ Q_PROPERTY(QString hardwarePlatform READ hardwarePlatform WRITE setHardwarePlatform NOTIFY hardwarePlatformChanged)
+ Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
+
+public:
+ enum Type {
+ TypeWatchapp,
+ TypeWatchface
+ };
+
+ explicit AppStoreClient(QObject *parent = 0);
+
+ ApplicationsModel *model() const;
+
+ int limit() const;
+ void setLimit(int limit);
+
+ QString hardwarePlatform() const;
+ void setHardwarePlatform(const QString &hardwarePlatform);
+
+ bool busy() const;
+
+signals:
+ void limitChanged();
+ void hardwarePlatformChanged();
+ void busyChanged();
+
+public slots:
+ void fetchHome(Type type);
+ void fetchLink(const QString &link);
+
+ void fetchAppDetails(const QString &appId);
+
+ void search(const QString &searchString, Type type);
+
+private:
+ AppItem *parseAppItem(const QVariantMap &map);
+ void setBusy(bool busy);
+
+private:
+ QNetworkAccessManager *m_nam;
+ ApplicationsModel *m_model;
+ int m_limit = 20;
+ QString m_hardwarePlatform;
+ bool m_busy = false;
+};
+
+#endif // APPSTORECLIENT_H
diff --git a/rockwork/artwork/bianca-black.png b/rockwork/artwork/bianca-black.png
new file mode 100644
index 0000000..d1207c9
--- /dev/null
+++ b/rockwork/artwork/bianca-black.png
Binary files differ
diff --git a/rockwork/artwork/bianca-silver.png b/rockwork/artwork/bianca-silver.png
new file mode 100644
index 0000000..2d003e8
--- /dev/null
+++ b/rockwork/artwork/bianca-silver.png
Binary files differ
diff --git a/rockwork/artwork/black-20mm-hole.png b/rockwork/artwork/black-20mm-hole.png
new file mode 100644
index 0000000..ff61e66
--- /dev/null
+++ b/rockwork/artwork/black-20mm-hole.png
Binary files differ
diff --git a/rockwork/artwork/bobby-black.png b/rockwork/artwork/bobby-black.png
new file mode 100644
index 0000000..83177b5
--- /dev/null
+++ b/rockwork/artwork/bobby-black.png
Binary files differ
diff --git a/rockwork/artwork/bobby-gold.png b/rockwork/artwork/bobby-gold.png
new file mode 100644
index 0000000..d97f2f4
--- /dev/null
+++ b/rockwork/artwork/bobby-gold.png
Binary files differ
diff --git a/rockwork/artwork/bobby-silver.png b/rockwork/artwork/bobby-silver.png
new file mode 100644
index 0000000..44efdf8
--- /dev/null
+++ b/rockwork/artwork/bobby-silver.png
Binary files differ
diff --git a/rockwork/artwork/rockwork.svg b/rockwork/artwork/rockwork.svg
new file mode 100644
index 0000000..e4e92c0
--- /dev/null
+++ b/rockwork/artwork/rockwork.svg
@@ -0,0 +1,275 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="72.248886mm"
+ height="72.248886mm"
+ viewBox="0 0 255.99999 255.99999"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="upebble.svg">
+ <defs
+ id="defs4">
+ <filter
+ inkscape:collect="always"
+ style="color-interpolation-filters:sRGB"
+ id="filter4248"
+ x="-0.025328101"
+ width="1.0506562"
+ y="-0.013960773"
+ height="1.0279215">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="2.3907822"
+ id="feGaussianBlur4250" />
+ </filter>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="3.959798"
+ inkscape:cx="89.121544"
+ inkscape:cy="77.044911"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="2880"
+ inkscape:window-height="1752"
+ inkscape:window-x="0"
+ inkscape:window-y="48"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136"
+ originx="-40.000001"
+ originy="-539"
+ snapvisiblegridlinesonly="true"
+ enabled="false" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-40,-257.36221)">
+ <rect
+ style="opacity:1;fill:#78d3fc;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect4200"
+ width="256"
+ height="256"
+ x="40"
+ y="257.36221" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:0.0479798;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 40,257.36221 256,0 -256,256 z"
+ id="rect4252"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ <g
+ id="g4300">
+ <g
+ style="fill:#000000;fill-opacity:1;opacity:0.291;filter:url(#filter4248)"
+ id="g4202"
+ transform="matrix(0.60632857,0,0,0.60632857,-37.462675,74.399202)">
+ <path
+ sodipodi:nodetypes="czccc"
+ inkscape:connector-curvature="0"
+ id="path4204"
+ d="m 437.97969,445.08937 c 0,0 11.49464,-4.59544 12.27285,0.25253 0.77821,4.84797 2.06459,45.23266 2.06459,45.23266 -8.36034,0.32794 -13.15013,-0.0886 -13.15013,-0.0886 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path4206"
+ d="m 439.49492,491.6046 12.68287,0.70015 0.54937,42.27954 -13.73731,0.25254 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <rect
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect4208"
+ width="120"
+ height="95"
+ x="280"
+ y="623.36218" />
+ <rect
+ y="307.36221"
+ x="280"
+ height="95"
+ width="120"
+ id="rect4210"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="zcczz"
+ inkscape:connector-curvature="0"
+ id="path4212"
+ d="M 228.0862,442.4309 C 228.58744,435.98794 240,437.36221 240,437.36221 l 0,42.02031 c 0,0 -14.31567,-1.22669 -13.80125,-2.84014 0.51442,-1.61345 1.3862,-27.66851 1.88745,-34.11148 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 420,643.715 337.60905,658.36221 255,643.715 l 0,-20 165,0 z"
+ id="path4214"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="path4216"
+ d="M 255,382.36221 337.39095,367.715 420,382.36221 l 0,20 -165,0 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="cczcc"
+ inkscape:connector-curvature="0"
+ id="path4218"
+ d="m 438.52906,535.82255 c 0,0 5.15979,0.84007 13.83236,0.44761 0.13423,13.76866 -1.20901,37.74804 -1.85634,42.35471 -0.64733,4.60667 -11.01016,-0.50508 -11.01016,-0.50508 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="zzzzzzzzz"
+ inkscape:connector-curvature="0"
+ id="path4220"
+ d="m 244.85206,406.56127 c 16.07143,-13.92858 66.12644,-13.34299 97.65304,-13.30725 31.5266,0.0357 73.56632,-0.53467 90.70918,15.17961 17.14286,15.71428 12.91706,70.98675 13.01566,106.0726 0.0986,35.08586 5.19864,81.42741 -13.01565,99.64169 C 415,632.3622 371.12033,628.47664 339.15317,628.18658 307.186,627.89652 263.91063,632.7014 245.3392,615.91569 226.76777,599.12997 231.43107,540.45867 231.61582,505.9352 c 0.18475,-34.52347 -2.83519,-85.44536 13.23624,-99.37393 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <rect
+ ry="20"
+ rx="20"
+ y="422.36221"
+ x="260"
+ height="174.99998"
+ width="155"
+ id="rect4222"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <rect
+ y="442.36221"
+ x="280"
+ height="135"
+ width="120"
+ id="rect4224"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="cccccccc"
+ inkscape:connector-curvature="0"
+ id="path4226"
+ d="m 375,442.36221 25,0 0,135 -25,0 0,-109.75206 -7.32361,-5.3033 7.32361,-4.9245 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ </g>
+ <path
+ sodipodi:nodetypes="czccc"
+ inkscape:connector-curvature="0"
+ id="rect4177"
+ d="m 228.09692,344.2696 c 0,0 6.96953,-2.78634 7.44138,0.15312 0.47185,2.93946 1.25182,27.42585 1.25182,27.42585 -5.06911,0.19884 -7.9733,-0.0537 -7.9733,-0.0537 z"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="rect4179"
+ d="m 229.01565,372.47312 7.68999,0.42452 0.3331,25.63529 -8.32933,0.15312 z"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <rect
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect4167"
+ width="72.75943"
+ height="57.601215"
+ x="132.30933"
+ y="452.36151" />
+ <rect
+ y="260.76169"
+ x="132.30933"
+ height="57.601215"
+ width="72.75943"
+ id="rect4165"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="zcczz"
+ inkscape:connector-curvature="0"
+ id="rect4169"
+ d="m 100.8325,342.6577 c 0.30392,-3.90655 7.22368,-3.07329 7.22368,-3.07329 l 0,25.47811 c 0,0 -8.679998,-0.74378 -8.36809,-1.72206 0.311907,-0.97828 0.84049,-16.77621 1.14441,-20.68276 z"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 217.19532,464.702 -49.95598,8.88102 -50.08823,-8.88102 0,-12.12657 100.04421,0 z"
+ id="path4175"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="rect4172"
+ d="m 117.15111,306.23633 49.95599,-8.88102 50.08822,8.88102 0,12.12658 -100.04421,0 z"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="cczcc"
+ inkscape:connector-curvature="0"
+ id="rect4181"
+ d="m 228.43002,399.28372 c 0,0 3.12853,0.50936 8.38696,0.2714 0.0814,8.34833 -0.73306,22.88772 -1.12555,25.68087 -0.3925,2.79316 -6.67578,-0.30624 -6.67578,-0.30624 z"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="zzzzzzzzz"
+ inkscape:connector-curvature="0"
+ id="rect4149"
+ d="m 110.99812,320.90892 c 9.74457,-8.4453 40.09435,-8.09024 59.20983,-8.06857 19.11548,0.0217 44.60536,-0.32419 54.99957,9.20383 10.39421,9.52802 7.83198,43.0413 7.89177,64.31485 0.0598,21.27356 3.15208,49.37176 -7.89176,60.4156 -11.04385,11.04384 -37.64935,8.68791 -57.03195,8.51204 -19.38261,-0.17587 -45.6217,2.73747 -56.88209,-7.44019 -11.26039,-10.17766 -8.4329,-45.75175 -8.32088,-66.68431 0.11202,-20.93257 -1.71905,-51.80796 8.02551,-60.25325 z"
+ style="opacity:1;fill:#cbcbcb;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 133.22587,330.48948 69.72779,0 c 6.71812,0 11.83367,5.41484 12.12657,12.12657 1.19073,27.28478 1.19022,54.56956 0,81.85435 -0.29278,6.71174 -5.40845,12.12657 -12.12657,12.12657 l -69.72779,0 c -6.71812,0 -11.82926,-5.41504 -12.12657,-12.12657 -1.18246,-26.69356 -1.65764,-53.74075 0,-81.85435 0.39543,-6.70647 5.40845,-12.12657 12.12657,-12.12657 z"
+ id="rect4152"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="sssssssss" />
+ <rect
+ y="342.61606"
+ x="131.62029"
+ height="81.854355"
+ width="72.75943"
+ id="rect4154"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="cccccccc"
+ inkscape:connector-curvature="0"
+ id="rect4156"
+ d="m 189.2215,342.61605 15.15821,0 0,81.85436 -15.15821,0 0,-66.54581 -4.44052,-3.21555 4.44052,-2.98586 z"
+ style="opacity:1;fill:#78d3fc;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ </g>
+ <path
+ style="fill:#000000"
+ d="m 143.63128,393.17695 c -0.82884,-0.82884 0.32908,-1.46136 2.67524,-1.46136 3.02637,0 3.07586,-0.7116 0.31911,-4.58902 l -2.10477,-2.96041 2.0287,-2.94922 c 1.15063,-1.67272 2.20678,-4.13671 2.44014,-5.69282 0.40696,-2.71383 0.44252,-2.74828 3.27625,-3.17322 1.57564,-0.23629 4.03472,-1.23453 5.46462,-2.21832 1.42988,-0.98379 2.85437,-1.78871 3.16551,-1.78871 0.31114,0 1.68177,0.80847 3.04585,1.7966 1.40341,1.01663 3.70432,1.98019 5.29992,2.21946 2.92933,0.43928 2.99923,0.52347 3.81465,4.59457 0.23714,1.18397 1.22649,3.16119 2.19857,4.39382 0.97208,1.23265 1.76741,2.42825 1.76741,2.6569 0,0.22864 -0.83031,1.62253 -1.84514,3.09753 -2.44659,3.556 -2.38325,4.49115 0.32038,4.72947 1.19102,0.10499 2.22891,0.46766 2.30641,0.80594 0.15778,0.68864 -33.49245,1.21919 -34.17285,0.53879 z m 27.8729,-2.84521 c 0.009,-0.76112 0.83738,-2.4679 1.83992,-3.79283 l 1.82279,-2.40898 -1.58789,-2.08183 c -0.87334,-1.14501 -1.90888,-3.37431 -2.30119,-4.954 -0.68475,-2.75721 -0.82407,-2.89364 -3.48101,-3.40876 -1.52223,-0.29514 -3.68339,-1.22898 -4.80255,-2.0752 l -2.03486,-1.53859 -2.27046,1.53481 c -2.19134,1.48132 -2.85113,1.74296 -6.34208,2.51493 -1.32103,0.29212 -1.68348,0.80695 -2.01126,2.85674 -0.2199,1.37522 -1.11243,3.57721 -1.98338,4.8933 l -1.58355,2.39291 1.54803,2.0343 c 0.85141,1.11887 1.73796,2.79542 1.97011,3.72567 l 0.42209,1.69138 10.38909,0 c 10.35877,0 10.38914,-0.004 10.4062,-1.38385 z m -16.16204,-1.17442 c -3.00082,-0.83158 -3.29439,-2.70542 -0.30753,-1.96294 1.0994,0.27329 2.9676,0.49689 4.15156,0.49689 1.18396,0 3.05216,-0.2236 4.15155,-0.49689 1.34833,-0.33517 1.9989,-0.27586 1.9989,0.18225 0,1.61808 -6.44052,2.76557 -9.99448,1.78069 z m -1.87921,-5.21512 c -0.23497,-0.23497 -0.42721,-1.51123 -0.42721,-2.83614 0,-2.76084 1.40937,-2.98541 1.73071,-0.27578 0.21201,1.78766 -0.62633,3.78908 -1.3035,3.11192 z m 10.02856,-2.68238 c 0,-1.7768 0.25627,-2.46018 0.92256,-2.46018 0.6663,0 0.92257,0.68338 0.92257,2.46018 0,1.7768 -0.25627,2.46018 -0.92257,2.46018 -0.66629,0 -0.92256,-0.68338 -0.92256,-2.46018 z m -22.75668,2.7677 c 0,-0.69366 0.43166,-0.94682 1.38386,-0.81158 0.76111,0.10809 1.38385,0.47331 1.38385,0.81158 0,0.33828 -0.62274,0.70349 -1.38385,0.81159 -0.9522,0.13523 -1.38386,-0.11793 -1.38386,-0.81159 z m 37.8226,0.30318 c -0.46699,-0.7556 1.22568,-1.49279 2.13592,-0.93022 0.35999,0.22248 0.4953,0.66216 0.30068,0.97707 -0.46943,0.75955 -1.95589,0.73096 -2.4366,-0.0469 z m -32.49939,-13.42132 c -1.23387,-1.36341 -1.29125,-1.95046 -0.19065,-1.95046 1.05106,0 3.00139,2.11681 2.45435,2.66385 -0.65422,0.65422 -1.17438,0.49029 -2.2637,-0.71339 z m 27.38286,0.11435 c 0.23773,-1.20797 2.11252,-2.50883 2.72827,-1.89307 0.48678,0.48678 -1.47947,2.90348 -2.36232,2.90348 -0.31065,0 -0.47533,-0.45468 -0.36595,-1.01041 z m -13.38512,-4.7488 c -0.52662,-1.37234 -0.0519,-3.6431 0.82178,-3.93083 0.51752,-0.17043 0.76881,0.45533 0.76881,1.91443 0,2.29458 -0.99824,3.56005 -1.59059,2.0164 z"
+ id="path4285"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ssscsssssssssssssssscsssscsssscsscsssssssssssssssssssssscssccssssssssssss" />
+ <text
+ xml:space="preserve"
+ style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ x="136.46957"
+ y="405.82156"
+ id="text4342"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan4344"
+ x="136.46957"
+ y="405.82156"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10px;font-family:Ubuntu;-inkscape-font-specification:Ubuntu">Tomorrow</tspan></text>
+ </g>
+</svg>
diff --git a/rockwork/artwork/snowy-black.png b/rockwork/artwork/snowy-black.png
new file mode 100644
index 0000000..acf3439
--- /dev/null
+++ b/rockwork/artwork/snowy-black.png
Binary files differ
diff --git a/rockwork/artwork/snowy-red.png b/rockwork/artwork/snowy-red.png
new file mode 100644
index 0000000..b0bdc8e
--- /dev/null
+++ b/rockwork/artwork/snowy-red.png
Binary files differ
diff --git a/rockwork/artwork/snowy-white.png b/rockwork/artwork/snowy-white.png
new file mode 100644
index 0000000..3bfe6d1
--- /dev/null
+++ b/rockwork/artwork/snowy-white.png
Binary files differ
diff --git a/rockwork/artwork/spalding-14mm-black.png b/rockwork/artwork/spalding-14mm-black.png
new file mode 100644
index 0000000..47b5b03
--- /dev/null
+++ b/rockwork/artwork/spalding-14mm-black.png
Binary files differ
diff --git a/rockwork/artwork/spalding-14mm-rose-gold.png b/rockwork/artwork/spalding-14mm-rose-gold.png
new file mode 100644
index 0000000..8775cf1
--- /dev/null
+++ b/rockwork/artwork/spalding-14mm-rose-gold.png
Binary files differ
diff --git a/rockwork/artwork/spalding-14mm-silver.png b/rockwork/artwork/spalding-14mm-silver.png
new file mode 100644
index 0000000..bcc5f16
--- /dev/null
+++ b/rockwork/artwork/spalding-14mm-silver.png
Binary files differ
diff --git a/rockwork/artwork/spalding-20mm-black.png b/rockwork/artwork/spalding-20mm-black.png
new file mode 100644
index 0000000..d00a1f7
--- /dev/null
+++ b/rockwork/artwork/spalding-20mm-black.png
Binary files differ
diff --git a/rockwork/artwork/spalding-20mm-silver.png b/rockwork/artwork/spalding-20mm-silver.png
new file mode 100644
index 0000000..18b0e02
--- /dev/null
+++ b/rockwork/artwork/spalding-20mm-silver.png
Binary files differ
diff --git a/rockwork/artwork/tintin-black.png b/rockwork/artwork/tintin-black.png
new file mode 100644
index 0000000..dcf2c31
--- /dev/null
+++ b/rockwork/artwork/tintin-black.png
Binary files differ
diff --git a/rockwork/artwork/tintin-blue.png b/rockwork/artwork/tintin-blue.png
new file mode 100644
index 0000000..eca2d3b
--- /dev/null
+++ b/rockwork/artwork/tintin-blue.png
Binary files differ
diff --git a/rockwork/artwork/tintin-green.png b/rockwork/artwork/tintin-green.png
new file mode 100644
index 0000000..17df060
--- /dev/null
+++ b/rockwork/artwork/tintin-green.png
Binary files differ
diff --git a/rockwork/artwork/tintin-grey.png b/rockwork/artwork/tintin-grey.png
new file mode 100644
index 0000000..4f9988b
--- /dev/null
+++ b/rockwork/artwork/tintin-grey.png
Binary files differ
diff --git a/rockwork/artwork/tintin-orange.png b/rockwork/artwork/tintin-orange.png
new file mode 100644
index 0000000..5956126
--- /dev/null
+++ b/rockwork/artwork/tintin-orange.png
Binary files differ
diff --git a/rockwork/artwork/tintin-pink.png b/rockwork/artwork/tintin-pink.png
new file mode 100644
index 0000000..ee69d67
--- /dev/null
+++ b/rockwork/artwork/tintin-pink.png
Binary files differ
diff --git a/rockwork/artwork/tintin-red.png b/rockwork/artwork/tintin-red.png
new file mode 100644
index 0000000..6c7b7e2
--- /dev/null
+++ b/rockwork/artwork/tintin-red.png
Binary files differ
diff --git a/rockwork/artwork/tintin-white.png b/rockwork/artwork/tintin-white.png
new file mode 100644
index 0000000..912ea19
--- /dev/null
+++ b/rockwork/artwork/tintin-white.png
Binary files differ
diff --git a/rockwork/main.cpp b/rockwork/main.cpp
new file mode 100644
index 0000000..70fd0d7
--- /dev/null
+++ b/rockwork/main.cpp
@@ -0,0 +1,37 @@
+#include <QGuiApplication>
+#include <QQmlApplicationEngine>
+#include <QQuickView>
+#include <QtQml>
+#include <QFile>
+
+#include "notificationsourcemodel.h"
+#include "servicecontrol.h"
+#include "pebbles.h"
+#include "pebble.h"
+#include "applicationsmodel.h"
+#include "applicationsfiltermodel.h"
+#include "appstoreclient.h"
+#include "screenshotmodel.h"
+
+int main(int argc, char *argv[])
+{
+ QGuiApplication app(argc, argv);
+
+ qmlRegisterUncreatableType<Pebble>("RockWork", 1, 0, "Pebble", "Get them from the model");
+ qmlRegisterUncreatableType<ApplicationsModel>("RockWork", 1, 0, "ApplicationsModel", "Get them from a Pebble object");
+ qmlRegisterUncreatableType<AppItem>("RockWork", 1, 0, "AppItem", "Get them from an ApplicationsModel");
+ qmlRegisterType<ApplicationsFilterModel>("RockWork", 1, 0, "ApplicationsFilterModel");
+ qmlRegisterType<Pebbles>("RockWork", 1, 0, "Pebbles");
+ qmlRegisterUncreatableType<NotificationSourceModel>("RockWork", 1, 0, "NotificationSourceModel", "Get it from a Pebble object");
+ qmlRegisterType<ServiceControl>("RockWork", 1, 0, "ServiceController");
+ qmlRegisterType<AppStoreClient>("RockWork", 1, 0, "AppStoreClient");
+ qmlRegisterType<ScreenshotModel>("RockWork", 1, 0, "ScreenshotModel");
+
+ QQuickView view;
+ view.engine()->rootContext()->setContextProperty("version", QStringLiteral(VERSION));
+ view.engine()->rootContext()->setContextProperty("homePath", QStandardPaths::standardLocations(QStandardPaths::HomeLocation).first());
+ view.setSource(QUrl(QStringLiteral("qrc:///Main.qml")));
+ view.setResizeMode(QQuickView::SizeRootObjectToView);
+ view.show();
+ return app.exec();
+}
diff --git a/rockwork/notificationsourcemodel.cpp b/rockwork/notificationsourcemodel.cpp
new file mode 100644
index 0000000..cbb75ca
--- /dev/null
+++ b/rockwork/notificationsourcemodel.cpp
@@ -0,0 +1,117 @@
+#include "notificationsourcemodel.h"
+
+#include <QSettings>
+#include <QDebug>
+
+NotificationSourceModel::NotificationSourceModel(QObject *parent) : QAbstractListModel(parent)
+{
+}
+
+int NotificationSourceModel::rowCount(const QModelIndex &parent) const
+{
+ Q_UNUSED(parent)
+ return m_sources.count();
+}
+
+QVariant NotificationSourceModel::data(const QModelIndex &index, int role) const
+{
+ NotificationSourceItem item = m_sources.at(index.row());
+ switch (role) {
+ case RoleName:
+ return item.m_displayName;
+ case RoleEnabled:
+ return item.m_enabled;
+ case RoleIcon:
+ return item.m_icon;
+ }
+ return QVariant();
+}
+
+QHash<int, QByteArray> NotificationSourceModel::roleNames() const
+{
+ QHash<int, QByteArray> roles;
+ roles.insert(RoleName, "name");
+ roles.insert(RoleEnabled, "enabled");
+ roles.insert(RoleIcon, "icon");
+ return roles;
+}
+
+void NotificationSourceModel::insert(const QString &sourceId, bool enabled)
+{
+ qDebug() << "changed" << sourceId << enabled;
+
+ int idx = -1;
+ for (int i = 0; i < m_sources.count(); i++) {
+ if (m_sources.at(i).m_id == sourceId) {
+ idx = i;
+ }
+ }
+
+ if (idx >= 0) {
+ m_sources[idx].m_enabled = enabled;
+ emit dataChanged(index(idx), index(idx), {RoleEnabled});
+ } else {
+ beginInsertRows(QModelIndex(), m_sources.count(), m_sources.count());
+ NotificationSourceItem item = fromDesktopFile(sourceId);
+ item.m_enabled = enabled;
+ m_sources.append(item);
+ endInsertRows();
+ }
+}
+
+#include <QStandardPaths>
+#include <QFileInfo>
+#include <QDir>
+
+NotificationSourceItem NotificationSourceModel::fromDesktopFile(const QString &sourceId)
+{
+ NotificationSourceItem ret;
+ ret.m_id = sourceId;
+ ret.m_icon = "dialog-question-symbolic";
+
+ QString desktopFilePath;
+ QStringList appsDirs = QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation);
+ foreach (const QString &appsDir, appsDirs) {
+ QDir dir(appsDir);
+ QFileInfoList entries = dir.entryInfoList({sourceId + "*.desktop"});
+ if (entries.count() > 0) {
+ desktopFilePath = entries.first().absoluteFilePath();
+ break;
+ }
+ }
+
+ if (desktopFilePath.isEmpty()) {
+ // Lets see if this is an indicator
+ QDir dir("/usr/share/upstart/xdg/autostart/");
+ QString serviceName = sourceId;
+ serviceName.remove("-service");
+ QFileInfoList entries = dir.entryInfoList({serviceName + "*.desktop"});
+ if (entries.count() > 0) {
+ desktopFilePath = entries.first().absoluteFilePath();
+ if (sourceId == "indicator-power-service") {
+ ret.m_icon = "gpm-battery-050";
+ } else if (sourceId == "indicator-datetime-service") {
+ ret.m_icon = "alarm-clock";
+ } else {
+ ret.m_icon = "settings";
+ }
+ }
+ }
+
+ if (desktopFilePath.isEmpty()) {
+ qWarning() << ".desktop file not found for" << sourceId;
+ ret.m_displayName = sourceId;
+ return ret;
+ }
+
+ QSettings s(desktopFilePath, QSettings::IniFormat);
+ s.beginGroup("Desktop Entry");
+ ret.m_displayName = s.value("Name").toString();
+ if (!s.value("Icon").toString().isEmpty()) {
+ ret.m_icon = s.value("Icon").toString();
+ }
+
+ qDebug() << "parsed file:" << desktopFilePath << ret.m_displayName << ret.m_icon;
+ return ret;
+}
+
diff --git a/rockwork/notificationsourcemodel.h b/rockwork/notificationsourcemodel.h
new file mode 100644
index 0000000..89fa26f
--- /dev/null
+++ b/rockwork/notificationsourcemodel.h
@@ -0,0 +1,48 @@
+#ifndef NOTIFICATIONSOURCEMODEL_H
+#define NOTIFICATIONSOURCEMODEL_H
+
+#include <QAbstractListModel>
+
+class NotificationSourceItem
+{
+public:
+ QString m_id;
+ QString m_displayName;
+ QString m_icon;
+ bool m_enabled = false;
+
+ bool operator ==(const NotificationSourceItem &other) {
+ return m_id == other.m_id;
+ }
+};
+
+class NotificationSourceModel : public QAbstractListModel
+{
+ Q_OBJECT
+ Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
+public:
+ enum Roles {
+ RoleName,
+ RoleEnabled,
+ RoleIcon
+ };
+
+ explicit NotificationSourceModel(QObject *parent = 0);
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ QHash<int, QByteArray> roleNames() const override;
+
+ void insert(const QString &sourceId, bool enabled);
+
+signals:
+ void countChanged();
+
+private:
+ NotificationSourceItem fromDesktopFile(const QString &sourceId);
+
+private:
+ QList<NotificationSourceItem> m_sources;
+};
+
+#endif // NOTIFICATIONSOURCEMODEL_H
diff --git a/rockwork/org.freedesktop.Notifications.xml b/rockwork/org.freedesktop.Notifications.xml
new file mode 100644
index 0000000..b694a55
--- /dev/null
+++ b/rockwork/org.freedesktop.Notifications.xml
@@ -0,0 +1,45 @@
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+<node>
+ <interface name="org.freedesktop.Notifications">
+ <!-- Desktop Notification Specification interface -->
+ <method name="GetCapabilities">
+ <arg name="capabilities" type="as" direction="out"/>
+ </method>
+ <method name="Notify">
+ <arg name="app_name" type="s" direction="in"/>
+ <arg name="replaces_id" type="u" direction="in"/>
+ <arg name="app_icon" type="s" direction="in"/>
+ <arg name="summary" type="s" direction="in"/>
+ <arg name="body" type="s" direction="in"/>
+ <arg name="actions" type="as" direction="in"/>
+ <arg name="hints" type="a{sv}" direction="in"/>
+ <arg name="expire_timeout" type="i" direction="in"/>
+ <arg name="id" type="u" direction="out"/>
+ <annotation name="org.qtproject.QtDBus.QtTypeName.In6" value="QVariantMap"/>
+ </method>
+ <method name="CloseNotification">
+ <arg name="id" type="u" direction="in"/>
+ </method>
+ <method name="GetServerInformation">
+ <arg name="name" type="s" direction="out"/>
+ <arg name="vendor" type="s" direction="out"/>
+ <arg name="version" type="s" direction="out"/>
+ <arg name="specVersion" type="s" direction="out"/>
+ </method>
+ <signal name="NotificationClosed">
+ <arg name="id" type="u"/>
+ <arg name="reason" type="u"/>
+ </signal>
+ <signal name="ActionInvoked">
+ <arg name="id" type="u"/>
+ <arg name="action_key" type="s"/>
+ </signal>
+
+ <!-- Extra method to enable testing -->
+ <method name="GetNotifications">
+ <arg name="app_name" type="s" direction="in"/>
+ <arg name="notifications" type="a(sussasa{sv}i)" direction="out"/>
+ <annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="NotificationDataList"/>
+ </method>
+ </interface>
+</node>
diff --git a/rockwork/pebble.cpp b/rockwork/pebble.cpp
new file mode 100644
index 0000000..cba97d3
--- /dev/null
+++ b/rockwork/pebble.cpp
@@ -0,0 +1,432 @@
+#include "pebble.h"
+#include "notificationsourcemodel.h"
+#include "applicationsmodel.h"
+#include "screenshotmodel.h"
+
+#include <QDBusArgument>
+#include <QDebug>
+
+Pebble::Pebble(const QDBusObjectPath &path, QObject *parent):
+ QObject(parent),
+ m_path(path)
+{
+ m_iface = new QDBusInterface("org.rockwork", path.path(), "org.rockwork.Pebble", QDBusConnection::sessionBus(), this);
+ m_notifications = new NotificationSourceModel(this);
+ m_installedApps = new ApplicationsModel(this);
+ connect(m_installedApps, &ApplicationsModel::appsSorted, this, &Pebble::appsSorted);
+ m_installedWatchfaces = new ApplicationsModel(this);
+ m_screenshotModel = new ScreenshotModel(this);
+
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "Connected", this, SLOT(pebbleConnected()));
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "Disconnected", this, SLOT(pebbleDisconnected()));
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "InstalledAppsChanged", this, SLOT(refreshApps()));
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "OpenURL", this, SIGNAL(openURL(const QString&, const QString&)));
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "NotificationFilterChanged", this, SLOT(notificationFilterChanged(const QString &, bool)));
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "ScreenshotAdded", this, SLOT(screenshotAdded(const QString &)));
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "ScreenshotRemoved", this, SLOT(screenshotRemoved(const QString &)));
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "FirmwareUpgradeAvailableChanged", this, SLOT(refreshFirmwareUpdateInfo()));
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "UpgradingFirmwareChanged", this, SIGNAL(refreshFirmwareUpdateInfo()));
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "LogsDumped", this, SIGNAL(logsDumped(bool)));
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "HealthParamsChanged", this, SIGNAL(healthParamsChanged()));
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "ImperialUnitsChanged", this, SIGNAL(imperialUnitsChanged()));
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "CalendarSyncEnabledChanged", this, SIGNAL(calendarSyncEnabledChanged()));
+
+ dataChanged();
+ refreshApps();
+ refreshNotifications();
+ refreshScreenshots();
+ refreshFirmwareUpdateInfo();
+}
+
+bool Pebble::connected() const
+{
+ return m_connected;
+}
+
+QDBusObjectPath Pebble::path()
+{
+ return m_path;
+}
+
+QString Pebble::address() const
+{
+ return m_address;
+}
+
+QString Pebble::name() const
+{
+ return m_name;
+}
+
+QString Pebble::hardwarePlatform() const
+{
+ return m_hardwarePlatform;
+}
+
+QString Pebble::serialNumber() const
+{
+ return m_serialNumber;
+}
+
+QString Pebble::softwareVersion() const
+{
+ return m_softwareVersion;
+}
+
+int Pebble::model() const
+{
+ return m_model;
+}
+
+bool Pebble::recovery() const
+{
+ return m_recovery;
+}
+
+bool Pebble::upgradingFirmware() const
+{
+ qDebug() << "upgrading firmware" << m_upgradingFirmware;
+ return m_upgradingFirmware;
+}
+
+NotificationSourceModel *Pebble::notifications() const
+{
+ return m_notifications;
+}
+
+ApplicationsModel *Pebble::installedApps() const
+{
+ return m_installedApps;
+}
+
+ApplicationsModel *Pebble::installedWatchfaces() const
+{
+ return m_installedWatchfaces;
+}
+
+ScreenshotModel *Pebble::screenshots() const
+{
+ return m_screenshotModel;
+}
+
+bool Pebble::firmwareUpgradeAvailable() const
+{
+ return m_firmwareUpgradeAvailable;
+}
+
+QString Pebble::firmwareReleaseNotes() const
+{
+ return m_firmwareReleaseNotes;
+}
+
+QString Pebble::candidateVersion() const
+{
+ return m_candidateVersion;
+}
+
+QVariantMap Pebble::healthParams() const
+{
+ QDBusMessage m = m_iface->call("HealthParams");
+ if (m.type() == QDBusMessage::ErrorMessage || m.arguments().count() == 0) {
+ qWarning() << "Could not fetch health params" << m.errorMessage();
+ return QVariantMap();
+ }
+
+ const QDBusArgument &arg = m.arguments().first().value<QDBusArgument>();
+
+ QVariantMap mapEntryVariant;
+ arg >> mapEntryVariant;
+
+ qDebug() << "have health params" << mapEntryVariant;
+ return mapEntryVariant;
+}
+
+void Pebble::setHealthParams(const QVariantMap &healthParams)
+{
+ m_iface->call("SetHealthParams", healthParams);
+}
+
+bool Pebble::imperialUnits() const
+{
+ return fetchProperty("ImperialUnits").toBool();
+}
+
+void Pebble::setImperialUnits(bool imperialUnits)
+{
+ qDebug() << "setting im units" << imperialUnits;
+ m_iface->call("SetImperialUnits", imperialUnits);
+}
+
+bool Pebble::calendarSyncEnabled() const
+{
+ return fetchProperty("CalendarSyncEnabled").toBool();
+}
+
+void Pebble::setCalendarSyncEnabled(bool enabled)
+{
+ m_iface->call("SetCalendarSyncEnabled", enabled);
+}
+
+void Pebble::configurationClosed(const QString &uuid, const QString &url)
+{
+ m_iface->call("ConfigurationClosed", uuid, url.mid(17));
+}
+
+void Pebble::launchApp(const QString &uuid)
+{
+ m_iface->call("LaunchApp", uuid);
+}
+
+void Pebble::requestConfigurationURL(const QString &uuid)
+{
+ m_iface->call("ConfigurationURL", uuid);
+}
+
+void Pebble::removeApp(const QString &uuid)
+{
+ qDebug() << "should remove app" << uuid;
+ m_iface->call("RemoveApp", uuid);
+}
+
+void Pebble::installApp(const QString &storeId)
+{
+ qDebug() << "should install app" << storeId;
+ m_iface->call("InstallApp", storeId);
+}
+
+void Pebble::sideloadApp(const QString &packageFile)
+{
+ m_iface->call("SideloadApp", packageFile);
+}
+
+QVariant Pebble::fetchProperty(const QString &propertyName) const
+{
+ QDBusMessage m = m_iface->call(propertyName);
+ if (m.type() != QDBusMessage::ErrorMessage && m.arguments().count() == 1) {
+ qDebug() << "property" << propertyName << m.arguments().first();
+ return m.arguments().first();
+
+ }
+ qDebug() << "error getting property:" << propertyName << m.errorMessage();
+ return QVariant();
+}
+
+void Pebble::dataChanged()
+{
+ qDebug() << "data changed";
+ m_name = fetchProperty("Name").toString();
+ m_address = fetchProperty("Address").toString();
+ m_serialNumber = fetchProperty("SerialNumber").toString();
+ m_serialNumber = fetchProperty("SerialNumber").toString();
+ QString hardwarePlatform = fetchProperty("HardwarePlatform").toString();
+ if (hardwarePlatform != m_hardwarePlatform) {
+ m_hardwarePlatform = hardwarePlatform;
+ emit hardwarePlatformChanged();
+ }
+ m_softwareVersion = fetchProperty("SoftwareVersion").toString();
+ m_model = fetchProperty("Model").toInt();
+ m_recovery = fetchProperty("Recovery").toBool();
+ qDebug() << "model is" << m_model;
+ emit modelChanged();
+
+ bool connected = fetchProperty("IsConnected").toBool();
+ if (connected != m_connected) {
+ m_connected = connected;
+ emit connectedChanged();
+ }
+}
+
+void Pebble::pebbleConnected()
+{
+
+ dataChanged();
+ m_connected = true;
+ emit connectedChanged();
+
+ refreshApps();
+ refreshNotifications();
+ refreshScreenshots();
+}
+
+void Pebble::pebbleDisconnected()
+{
+ m_connected = false;
+ emit connectedChanged();
+}
+
+void Pebble::notificationFilterChanged(const QString &sourceId, bool enabled)
+{
+ m_notifications->insert(sourceId, enabled);
+}
+
+void Pebble::refreshNotifications()
+{
+ QDBusMessage m = m_iface->call("NotificationsFilter");
+ if (m.type() == QDBusMessage::ErrorMessage || m.arguments().count() == 0) {
+ qWarning() << "Could not fetch notifications filter" << m.errorMessage();
+ return;
+ }
+
+ const QDBusArgument &arg = m.arguments().first().value<QDBusArgument>();
+
+ QVariantMap mapEntryVariant;
+ arg >> mapEntryVariant;
+
+ foreach (const QString &sourceId, mapEntryVariant.keys()) {
+ m_notifications->insert(sourceId, mapEntryVariant.value(sourceId).toBool());
+ }
+}
+
+void Pebble::setNotificationFilter(const QString &sourceId, bool enabled)
+{
+ m_iface->call("SetNotificationFilter", sourceId, enabled);
+}
+
+void Pebble::moveApp(const QString &uuid, int toIndex)
+{
+ // This is a bit tricky:
+ AppItem *item = m_installedApps->findByUuid(uuid);
+ if (!item) {
+ qWarning() << "item not found";
+ return;
+ }
+ int realToIndex = 0;
+ for (int i = 0; i < m_installedApps->rowCount(); i++) {
+ if (item->isWatchFace() && m_installedApps->get(i)->isWatchFace()) {
+ realToIndex++;
+ } else if (!item->isWatchFace() && !m_installedApps->get(i)->isWatchFace()) {
+ realToIndex++;
+ }
+ if (realToIndex == toIndex) {
+ realToIndex = i+1;
+ break;
+ }
+ }
+ m_iface->call("MoveApp", m_installedApps->indexOf(item), realToIndex);
+}
+
+void Pebble::refreshApps()
+{
+
+ QDBusMessage m = m_iface->call("InstalledApps");
+ if (m.type() == QDBusMessage::ErrorMessage || m.arguments().count() == 0) {
+ qWarning() << "Could not fetch installed apps" << m.errorMessage();
+ return;
+ }
+
+ m_installedApps->clear();
+ m_installedWatchfaces->clear();
+
+ const QDBusArgument &arg = m.arguments().first().value<QDBusArgument>();
+
+ QVariantList appList;
+
+ arg.beginArray();
+ while (!arg.atEnd()) {
+ QVariant mapEntryVariant;
+ arg >> mapEntryVariant;
+
+ QDBusArgument mapEntry = mapEntryVariant.value<QDBusArgument>();
+ QVariantMap appMap;
+ mapEntry >> appMap;
+ appList.append(appMap);
+
+ }
+ arg.endArray();
+
+
+ qDebug() << "have apps" << appList;
+ foreach (const QVariant &v, appList) {
+ AppItem *app = new AppItem(this);
+ app->setStoreId(v.toMap().value("storeId").toString());
+ app->setUuid(v.toMap().value("uuid").toString());
+ app->setName(v.toMap().value("name").toString());
+ app->setIcon(v.toMap().value("icon").toString());
+ app->setVendor(v.toMap().value("vendor").toString());
+ app->setVersion(v.toMap().value("version").toString());
+ app->setIsWatchFace(v.toMap().value("watchface").toBool());
+ app->setHasSettings(v.toMap().value("hasSettings").toBool());
+ app->setIsSystemApp(v.toMap().value("systemApp").toBool());
+
+ if (app->isWatchFace()) {
+ m_installedWatchfaces->insert(app);
+ } else {
+ m_installedApps->insert(app);
+ }
+ }
+}
+
+void Pebble::appsSorted()
+{
+ QStringList newList;
+ for (int i = 0; i < m_installedApps->rowCount(); i++) {
+ newList << m_installedApps->get(i)->uuid();
+ }
+ for (int i = 0; i < m_installedWatchfaces->rowCount(); i++) {
+ newList << m_installedWatchfaces->get(i)->uuid();
+ }
+ m_iface->call("SetAppOrder", newList);
+}
+
+void Pebble::refreshScreenshots()
+{
+ m_screenshotModel->clear();
+ QStringList screenshots = fetchProperty("Screenshots").toStringList();
+ foreach (const QString &filename, screenshots) {
+ m_screenshotModel->insert(filename);
+ }
+}
+
+void Pebble::screenshotAdded(const QString &filename)
+{
+ qDebug() << "screenshot added" << filename;
+ m_screenshotModel->insert(filename);
+}
+
+void Pebble::screenshotRemoved(const QString &filename)
+{
+ m_screenshotModel->remove(filename);
+}
+
+void Pebble::refreshFirmwareUpdateInfo()
+{
+ bool firmwareUpgradeAvailable = fetchProperty("FirmwareUpgradeAvailable").toBool();
+ if (firmwareUpgradeAvailable && !m_firmwareUpgradeAvailable) {
+ m_firmwareUpgradeAvailable = true;
+ m_firmwareReleaseNotes = fetchProperty("FirmwareReleaseNotes").toString();
+ m_candidateVersion = fetchProperty("CandidateFirmwareVersion").toString();
+ qDebug() << "firmare upgrade" << m_firmwareUpgradeAvailable << m_firmwareReleaseNotes << m_candidateVersion;
+ emit firmwareUpgradeAvailableChanged();
+ } else if (!firmwareUpgradeAvailable && m_firmwareUpgradeAvailable) {
+ m_firmwareUpgradeAvailable = false;
+ m_firmwareReleaseNotes.clear();;
+ m_candidateVersion.clear();
+ emit firmwareUpgradeAvailableChanged();
+ }
+ bool upgradingFirmware = fetchProperty("UpgradingFirmware").toBool();
+ if (m_upgradingFirmware != upgradingFirmware) {
+ m_upgradingFirmware = upgradingFirmware;
+ emit upgradingFirmwareChanged();
+ }
+}
+
+void Pebble::requestScreenshot()
+{
+ m_iface->call("RequestScreenshot");
+}
+
+void Pebble::removeScreenshot(const QString &filename)
+{
+ qDebug() << "removing screenshot" << filename;
+ m_iface->call("RemoveScreenshot", filename);
+}
+
+void Pebble::performFirmwareUpgrade()
+{
+ m_iface->call("PerformFirmwareUpgrade");
+}
+
+void Pebble::dumpLogs(const QString &filename)
+{
+ m_iface->call("DumpLogs", filename);
+}
diff --git a/rockwork/pebble.h b/rockwork/pebble.h
new file mode 100644
index 0000000..58e13a1
--- /dev/null
+++ b/rockwork/pebble.h
@@ -0,0 +1,131 @@
+#ifndef PEBBLE_H
+#define PEBBLE_H
+
+#include <QObject>
+#include <QDBusInterface>
+
+class NotificationSourceModel;
+class ApplicationsModel;
+class ScreenshotModel;
+
+class Pebble : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QString name READ name CONSTANT)
+ Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged)
+ Q_PROPERTY(QString hardwarePlatform READ hardwarePlatform NOTIFY hardwarePlatformChanged)
+ Q_PROPERTY(int model READ model NOTIFY modelChanged)
+ Q_PROPERTY(NotificationSourceModel* notifications READ notifications CONSTANT)
+ Q_PROPERTY(ApplicationsModel* installedApps READ installedApps CONSTANT)
+ Q_PROPERTY(ApplicationsModel* installedWatchfaces READ installedWatchfaces CONSTANT)
+ Q_PROPERTY(ScreenshotModel* screenshots READ screenshots CONSTANT)
+ Q_PROPERTY(bool recovery READ recovery NOTIFY connectedChanged)
+ Q_PROPERTY(QString softwareVersion READ softwareVersion NOTIFY connectedChanged)
+ Q_PROPERTY(bool firmwareUpgradeAvailable READ firmwareUpgradeAvailable NOTIFY firmwareUpgradeAvailableChanged)
+ Q_PROPERTY(QString firmwareReleaseNotes READ firmwareReleaseNotes NOTIFY firmwareUpgradeAvailableChanged)
+ Q_PROPERTY(QString candidateVersion READ candidateVersion NOTIFY firmwareUpgradeAvailableChanged)
+ Q_PROPERTY(bool upgradingFirmware READ upgradingFirmware NOTIFY upgradingFirmwareChanged)
+ Q_PROPERTY(QVariantMap healthParams READ healthParams WRITE setHealthParams NOTIFY healthParamsChanged)
+ Q_PROPERTY(bool imperialUnits READ imperialUnits WRITE setImperialUnits NOTIFY imperialUnitsChanged)
+ Q_PROPERTY(bool calendarSyncEnabled READ calendarSyncEnabled WRITE setCalendarSyncEnabled NOTIFY calendarSyncEnabledChanged)
+
+public:
+ explicit Pebble(const QDBusObjectPath &path, QObject *parent = 0);
+
+ QDBusObjectPath path();
+
+ bool connected() const;
+ QString address() const;
+ QString name() const;
+ QString hardwarePlatform() const;
+ QString serialNumber() const;
+ QString softwareVersion() const;
+ int model() const;
+ bool recovery() const;
+ bool upgradingFirmware() const;
+
+ NotificationSourceModel *notifications() const;
+ ApplicationsModel* installedApps() const;
+ ApplicationsModel* installedWatchfaces() const;
+ ScreenshotModel* screenshots() const;
+
+ bool firmwareUpgradeAvailable() const;
+ QString firmwareReleaseNotes() const;
+ QString candidateVersion() const;
+
+ QVariantMap healthParams() const;
+ void setHealthParams(const QVariantMap &healthParams);
+
+ bool imperialUnits() const;
+ void setImperialUnits(bool imperialUnits);
+
+ bool calendarSyncEnabled() const;
+ void setCalendarSyncEnabled(bool enabled);
+
+public slots:
+ void setNotificationFilter(const QString &sourceId, bool enabled);
+ void removeApp(const QString &uuid);
+ void installApp(const QString &storeId);
+ void sideloadApp(const QString &packageFile);
+ void moveApp(const QString &uuid, int toIndex);
+ void requestConfigurationURL(const QString &uuid);
+ void configurationClosed(const QString &uuid, const QString &url);
+ void launchApp(const QString &uuid);
+ void requestScreenshot();
+ void removeScreenshot(const QString &filename);
+ void performFirmwareUpgrade();
+ void dumpLogs(const QString &filename);
+
+signals:
+ void connectedChanged();
+ void hardwarePlatformChanged();
+ void modelChanged();
+ void firmwareUpgradeAvailableChanged();
+ void upgradingFirmwareChanged();
+ void logsDumped(bool success);
+ void healthParamsChanged();
+ void imperialUnitsChanged();
+ void calendarSyncEnabledChanged();
+
+ void openURL(const QString &uuid, const QString &url);
+
+private:
+ QVariant fetchProperty(const QString &propertyName) const;
+
+private slots:
+ void dataChanged();
+ void pebbleConnected();
+ void pebbleDisconnected();
+ void notificationFilterChanged(const QString &sourceId, bool enabled);
+ void refreshNotifications();
+ void refreshApps();
+ void appsSorted();
+ void refreshScreenshots();
+ void screenshotAdded(const QString &filename);
+ void screenshotRemoved(const QString &filename);
+ void refreshFirmwareUpdateInfo();
+
+private:
+ QDBusObjectPath m_path;
+
+ bool m_connected = false;
+ QString m_address;
+ QString m_name;
+ QString m_hardwarePlatform;
+ QString m_serialNumber;
+ QString m_softwareVersion;
+ bool m_recovery = false;
+ int m_model = 0;
+ QDBusInterface *m_iface;
+ NotificationSourceModel *m_notifications;
+ ApplicationsModel *m_installedApps;
+ ApplicationsModel *m_installedWatchfaces;
+ ScreenshotModel *m_screenshotModel;
+
+ bool m_firmwareUpgradeAvailable = false;
+ QString m_firmwareReleaseNotes;
+ QString m_candidateVersion;
+ bool m_upgradingFirmware = false;
+};
+
+#endif // PEBBLE_H
diff --git a/rockwork/pebbles.cpp b/rockwork/pebbles.cpp
new file mode 100644
index 0000000..e45691e
--- /dev/null
+++ b/rockwork/pebbles.cpp
@@ -0,0 +1,180 @@
+#include "pebbles.h"
+#include "pebble.h"
+
+#include <QDBusConnection>
+#include <QDBusInterface>
+#include <QDebug>
+#include <QDBusArgument>
+#include <QDBusServiceWatcher>
+#include <algorithm>
+
+#define ROCKWORK_SERVICE QStringLiteral("org.rockwork")
+#define ROCKWORK_MANAGER_PATH QStringLiteral("/org/rockwork/Manager")
+#define ROCKWORK_MANAGER_INTERFACE QStringLiteral("org.rockwork.Manager")
+
+Pebbles::Pebbles(QObject *parent):
+ QAbstractListModel(parent)
+{
+ refresh();
+ m_watcher = new QDBusServiceWatcher(ROCKWORK_SERVICE, QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForRegistration, this);
+ QDBusConnection::sessionBus().connect(ROCKWORK_SERVICE, ROCKWORK_MANAGER_PATH, ROCKWORK_MANAGER_INTERFACE, "PebblesChanged", this, SLOT(refresh()));
+ connect(m_watcher, &QDBusServiceWatcher::serviceRegistered, [this]() {
+ refresh();
+ QDBusConnection::sessionBus().connect(ROCKWORK_SERVICE, ROCKWORK_MANAGER_PATH, ROCKWORK_MANAGER_INTERFACE, "PebblesChanged", this, SLOT(refresh()));
+ });
+}
+
+int Pebbles::rowCount(const QModelIndex &parent) const
+{
+ Q_UNUSED(parent)
+ return m_pebbles.count();
+}
+
+QVariant Pebbles::data(const QModelIndex &index, int role) const
+{
+ switch (role) {
+ case RoleAddress:
+ return m_pebbles.at(index.row())->address();
+ case RoleName:
+ return m_pebbles.at(index.row())->name();
+ case RoleSerialNumber:
+ return m_pebbles.at(index.row())->serialNumber();
+ case RoleConnected:
+ return m_pebbles.at(index.row())->connected();
+ }
+
+ return QVariant();
+}
+
+QHash<int, QByteArray> Pebbles::roleNames() const
+{
+ QHash<int,QByteArray> roles;
+ roles.insert(RoleAddress, "address");
+ roles.insert(RoleName, "name");
+ roles.insert(RoleSerialNumber, "serialNumber");
+ roles.insert(RoleConnected, "connected");
+ return roles;
+}
+
+QString Pebbles::version() const
+{
+ QDBusInterface iface(ROCKWORK_SERVICE, ROCKWORK_MANAGER_PATH, ROCKWORK_MANAGER_INTERFACE);
+ if (!iface.isValid()) {
+ qWarning() << "Could not connect to rockworkd.";
+ return QString();
+ }
+ QDBusMessage reply = iface.call("Version");
+ if (reply.type() == QDBusMessage::ErrorMessage) {
+ qWarning() << "Error refreshing watches:" << reply.errorMessage();
+ return QString();
+ }
+ if (reply.arguments().count() == 0) {
+ qWarning() << "No reply from service.";
+ return QString();
+ }
+ return reply.arguments().first().toString();
+}
+
+Pebble *Pebbles::get(int index) const
+{
+ return m_pebbles.at(index);
+}
+
+int Pebbles::find(const QString &address) const
+{
+ for (int i = 0; i < m_pebbles.count(); i++) {
+ if (m_pebbles.at(i)->address() == address) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+void Pebbles::refresh()
+{
+ qDebug() << "pebbles changed";
+ QDBusInterface iface(ROCKWORK_SERVICE, ROCKWORK_MANAGER_PATH, ROCKWORK_MANAGER_INTERFACE);
+ if (!iface.isValid()) {
+ qWarning() << "Could not connect to rockworkd.";
+ return;
+ }
+ QDBusMessage reply = iface.call("ListWatches");
+ if (reply.type() == QDBusMessage::ErrorMessage) {
+ qWarning() << "Error refreshing watches:" << reply.errorMessage();
+ return;
+ }
+ if (reply.arguments().count() == 0) {
+ qWarning() << "No reply from service.";
+ return;
+ }
+ QDBusArgument arg = reply.arguments().first().value<QDBusArgument>();
+ QStringList availableList;
+ arg.beginArray();
+ while (!arg.atEnd()) {
+ QDBusObjectPath p;
+ arg >> p;
+ if (find(p) == -1) {
+ Pebble *pebble = new Pebble(p, this);
+ connect(pebble, &Pebble::connectedChanged, this, &Pebbles::pebbleConnectedChanged);
+ beginInsertRows(QModelIndex(), m_pebbles.count(), m_pebbles.count());
+ m_pebbles.append(pebble);
+ endInsertRows();
+ emit countChanged();
+ }
+ availableList << p.path();
+ std::sort(m_pebbles.begin(), m_pebbles.end(), Pebbles::sortPebbles);
+ }
+ arg.endArray();
+
+ QList<Pebble*> toRemove;
+ foreach (Pebble *pebble, m_pebbles) {
+ bool found = false;
+ foreach (const QString &path, availableList) {
+ if (path == pebble->path().path()) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ toRemove << pebble;
+ }
+ }
+
+ while (!toRemove.isEmpty()) {
+ Pebble *pebble = toRemove.takeFirst();
+ int idx = m_pebbles.indexOf(pebble);
+ beginRemoveRows(QModelIndex(), idx, idx);
+ m_pebbles.takeAt(idx)->deleteLater();
+ endRemoveRows();
+ emit countChanged();
+ }
+}
+
+bool Pebbles::sortPebbles(Pebble *a, Pebble *b)
+{
+ if (a->connected() && !b->connected()) {
+ return true;
+ }
+ else if (!a->connected() && b->connected()) {
+ return false;
+ }
+ else {
+ return a->name() < b->name();
+ }
+}
+
+void Pebbles::pebbleConnectedChanged()
+{
+ Pebble *pebble = static_cast<Pebble*>(sender());
+ emit dataChanged(index(find(pebble->address())), index(find(pebble->address())), {RoleConnected});
+}
+
+int Pebbles::find(const QDBusObjectPath &path) const
+{
+ for (int i = 0; i < m_pebbles.count(); i++) {
+ if (m_pebbles.at(i)->path() == path) {
+ return i;
+ }
+ }
+ return -1;
+}
diff --git a/rockwork/pebbles.h b/rockwork/pebbles.h
new file mode 100644
index 0000000..0fef3bb
--- /dev/null
+++ b/rockwork/pebbles.h
@@ -0,0 +1,56 @@
+#ifndef PEBBLES_H
+#define PEBBLES_H
+
+#include <QObject>
+#include <QAbstractListModel>
+#include <QDBusServiceWatcher>
+#include <QDBusObjectPath>
+
+class Pebble;
+class QDBusInterface;
+
+class Pebbles : public QAbstractListModel
+{
+ Q_OBJECT
+ Q_PROPERTY(QString version READ version)
+
+ Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
+public:
+ enum Roles {
+ RoleAddress,
+ RoleName,
+ RoleSerialNumber,
+ RoleConnected
+ };
+
+ Pebbles(QObject *parent = 0);
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ QHash<int, QByteArray> roleNames() const override;
+
+ QString version() const;
+
+ Q_INVOKABLE Pebble *get(int index) const;
+ int find(const QString &address) const;
+
+
+signals:
+ void countChanged();
+
+private slots:
+ void refresh();
+
+ void pebbleConnectedChanged();
+
+private:
+ int find(const QDBusObjectPath &path) const;
+ static bool sortPebbles(Pebble *a, Pebble *b);
+
+private:
+ QDBusInterface *m_iface;
+ QList<Pebble*> m_pebbles;
+ QDBusServiceWatcher *m_watcher;
+};
+
+#endif // PEBBLES_H
diff --git a/rockwork/rockwork.apparmor b/rockwork/rockwork.apparmor
new file mode 100644
index 0000000..9756323
--- /dev/null
+++ b/rockwork/rockwork.apparmor
@@ -0,0 +1,7 @@
+{
+ "policy_groups": [
+ "networking"
+ ],
+ "policy_version": 1.3,
+ "template": "unconfined"
+}
diff --git a/rockwork/rockwork.desktop b/rockwork/rockwork.desktop
new file mode 100644
index 0000000..0a75199
--- /dev/null
+++ b/rockwork/rockwork.desktop
@@ -0,0 +1,8 @@
+[Desktop Entry]
+Name=RockWork
+Exec=rockwork
+Icon=rockwork/rockwork.svg
+Terminal=false
+Type=Application
+X-Ubuntu-Touch=true
+
diff --git a/rockwork/rockwork.pro b/rockwork/rockwork.pro
new file mode 100644
index 0000000..43b1dea
--- /dev/null
+++ b/rockwork/rockwork.pro
@@ -0,0 +1,72 @@
+TEMPLATE = app
+TARGET = rockwork
+
+include(../version.pri)
+load(ubuntu-click)
+
+QT += qml quick dbus
+
+CONFIG += c++11
+
+HEADERS += \
+ notificationsourcemodel.h \
+ servicecontrol.h \
+ pebble.h \
+ pebbles.h \
+ applicationsmodel.h \
+ applicationsfiltermodel.h \
+ appstoreclient.h \
+ screenshotmodel.h
+
+SOURCES += main.cpp \
+ notificationsourcemodel.cpp \
+ servicecontrol.cpp \
+ pebble.cpp \
+ pebbles.cpp \
+ applicationsmodel.cpp \
+ applicationsfiltermodel.cpp \
+ appstoreclient.cpp \
+ screenshotmodel.cpp
+
+RESOURCES += rockwork.qrc
+
+QML_FILES += $$files(*.qml,true) \
+ $$files(*.js,true)
+
+CONF_FILES += rockwork.apparmor \
+ rockwork.svg \
+ rockwork.desktop \
+ rockwork.url-dispatcher
+
+AP_TEST_FILES += tests/autopilot/run \
+ $$files(tests/*.py,true)
+
+#show all the files in QtCreator
+OTHER_FILES += $${CONF_FILES} \
+ $${QML_FILES} \
+ $${AP_TEST_FILES} \
+
+
+#specify where the config files are installed to
+config_files.path = /rockwork
+config_files.files += $${CONF_FILES}
+INSTALLS+=config_files
+
+#install the desktop file, a translated version is
+#automatically created in the build directory
+desktop_file.path = /rockwork
+desktop_file.files = $$OUT_PWD/rockwork.desktop
+desktop_file.CONFIG += no_check_exist
+INSTALLS+=desktop_file
+
+# Default rules for deployment.
+target.path = $${UBUNTU_CLICK_BINARY_PATH}
+INSTALLS+=target
+
+DISTFILES += \
+ NotificationsPage.qml \
+ PebblesPage.qml \
+ AppStorePage.qml \
+ AppStoreDetailsPage.qml \
+ PebbleModels.qml \
+ InfoPage.qml
diff --git a/rockwork/rockwork.qrc b/rockwork/rockwork.qrc
new file mode 100644
index 0000000..1d565a1
--- /dev/null
+++ b/rockwork/rockwork.qrc
@@ -0,0 +1,48 @@
+<RCC>
+ <qresource prefix="/">
+ <file>Main.qml</file>
+ <file>NotificationsPage.qml</file>
+ <file>PebblesPage.qml</file>
+ <file>InstalledAppsPage.qml</file>
+ <file>MainMenuPage.qml</file>
+ <file>AppSettingsPage.qml</file>
+ <file>AppStorePage.qml</file>
+ <file>AppStoreDetailsPage.qml</file>
+ <file>InstalledAppDelegate.qml</file>
+ <file>SystemAppIcon.qml</file>
+ <file>ScreenshotsPage.qml</file>
+ <file>snowywhite.svg</file>
+ <file>snowywhite.png</file>
+ <file>artwork/bianca-black.png</file>
+ <file>artwork/bianca-silver.png</file>
+ <file>artwork/black-20mm-hole.png</file>
+ <file>artwork/bobby-black.png</file>
+ <file>artwork/bobby-gold.png</file>
+ <file>artwork/bobby-silver.png</file>
+ <file>artwork/snowy-black.png</file>
+ <file>artwork/snowy-red.png</file>
+ <file>artwork/snowy-white.png</file>
+ <file>artwork/spalding-14mm-black.png</file>
+ <file>artwork/spalding-14mm-rose-gold.png</file>
+ <file>artwork/spalding-14mm-silver.png</file>
+ <file>artwork/spalding-20mm-black.png</file>
+ <file>artwork/spalding-20mm-silver.png</file>
+ <file>artwork/tintin-black.png</file>
+ <file>artwork/tintin-blue.png</file>
+ <file>artwork/tintin-green.png</file>
+ <file>artwork/tintin-grey.png</file>
+ <file>artwork/tintin-orange.png</file>
+ <file>artwork/tintin-pink.png</file>
+ <file>artwork/tintin-red.png</file>
+ <file>artwork/tintin-white.png</file>
+ <file>PebbleModels.qml</file>
+ <file>FirmwareUpgradePage.qml</file>
+ <file>InfoPage.qml</file>
+ <file>artwork/rockwork.svg</file>
+ <file>DeveloperToolsPage.qml</file>
+ <file>ContentPeerPickerPage.qml</file>
+ <file>HealthSettingsDialog.qml</file>
+ <file>SettingsPage.qml</file>
+ <file>ImportPackagePage.qml</file>
+ </qresource>
+</RCC>
diff --git a/rockwork/rockwork.svg b/rockwork/rockwork.svg
new file mode 100644
index 0000000..e4e92c0
--- /dev/null
+++ b/rockwork/rockwork.svg
@@ -0,0 +1,275 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="72.248886mm"
+ height="72.248886mm"
+ viewBox="0 0 255.99999 255.99999"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="upebble.svg">
+ <defs
+ id="defs4">
+ <filter
+ inkscape:collect="always"
+ style="color-interpolation-filters:sRGB"
+ id="filter4248"
+ x="-0.025328101"
+ width="1.0506562"
+ y="-0.013960773"
+ height="1.0279215">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="2.3907822"
+ id="feGaussianBlur4250" />
+ </filter>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="3.959798"
+ inkscape:cx="89.121544"
+ inkscape:cy="77.044911"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="2880"
+ inkscape:window-height="1752"
+ inkscape:window-x="0"
+ inkscape:window-y="48"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136"
+ originx="-40.000001"
+ originy="-539"
+ snapvisiblegridlinesonly="true"
+ enabled="false" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-40,-257.36221)">
+ <rect
+ style="opacity:1;fill:#78d3fc;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect4200"
+ width="256"
+ height="256"
+ x="40"
+ y="257.36221" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:0.0479798;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 40,257.36221 256,0 -256,256 z"
+ id="rect4252"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ <g
+ id="g4300">
+ <g
+ style="fill:#000000;fill-opacity:1;opacity:0.291;filter:url(#filter4248)"
+ id="g4202"
+ transform="matrix(0.60632857,0,0,0.60632857,-37.462675,74.399202)">
+ <path
+ sodipodi:nodetypes="czccc"
+ inkscape:connector-curvature="0"
+ id="path4204"
+ d="m 437.97969,445.08937 c 0,0 11.49464,-4.59544 12.27285,0.25253 0.77821,4.84797 2.06459,45.23266 2.06459,45.23266 -8.36034,0.32794 -13.15013,-0.0886 -13.15013,-0.0886 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path4206"
+ d="m 439.49492,491.6046 12.68287,0.70015 0.54937,42.27954 -13.73731,0.25254 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <rect
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect4208"
+ width="120"
+ height="95"
+ x="280"
+ y="623.36218" />
+ <rect
+ y="307.36221"
+ x="280"
+ height="95"
+ width="120"
+ id="rect4210"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="zcczz"
+ inkscape:connector-curvature="0"
+ id="path4212"
+ d="M 228.0862,442.4309 C 228.58744,435.98794 240,437.36221 240,437.36221 l 0,42.02031 c 0,0 -14.31567,-1.22669 -13.80125,-2.84014 0.51442,-1.61345 1.3862,-27.66851 1.88745,-34.11148 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 420,643.715 337.60905,658.36221 255,643.715 l 0,-20 165,0 z"
+ id="path4214"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="path4216"
+ d="M 255,382.36221 337.39095,367.715 420,382.36221 l 0,20 -165,0 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="cczcc"
+ inkscape:connector-curvature="0"
+ id="path4218"
+ d="m 438.52906,535.82255 c 0,0 5.15979,0.84007 13.83236,0.44761 0.13423,13.76866 -1.20901,37.74804 -1.85634,42.35471 -0.64733,4.60667 -11.01016,-0.50508 -11.01016,-0.50508 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="zzzzzzzzz"
+ inkscape:connector-curvature="0"
+ id="path4220"
+ d="m 244.85206,406.56127 c 16.07143,-13.92858 66.12644,-13.34299 97.65304,-13.30725 31.5266,0.0357 73.56632,-0.53467 90.70918,15.17961 17.14286,15.71428 12.91706,70.98675 13.01566,106.0726 0.0986,35.08586 5.19864,81.42741 -13.01565,99.64169 C 415,632.3622 371.12033,628.47664 339.15317,628.18658 307.186,627.89652 263.91063,632.7014 245.3392,615.91569 226.76777,599.12997 231.43107,540.45867 231.61582,505.9352 c 0.18475,-34.52347 -2.83519,-85.44536 13.23624,-99.37393 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <rect
+ ry="20"
+ rx="20"
+ y="422.36221"
+ x="260"
+ height="174.99998"
+ width="155"
+ id="rect4222"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <rect
+ y="442.36221"
+ x="280"
+ height="135"
+ width="120"
+ id="rect4224"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="cccccccc"
+ inkscape:connector-curvature="0"
+ id="path4226"
+ d="m 375,442.36221 25,0 0,135 -25,0 0,-109.75206 -7.32361,-5.3033 7.32361,-4.9245 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ </g>
+ <path
+ sodipodi:nodetypes="czccc"
+ inkscape:connector-curvature="0"
+ id="rect4177"
+ d="m 228.09692,344.2696 c 0,0 6.96953,-2.78634 7.44138,0.15312 0.47185,2.93946 1.25182,27.42585 1.25182,27.42585 -5.06911,0.19884 -7.9733,-0.0537 -7.9733,-0.0537 z"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="rect4179"
+ d="m 229.01565,372.47312 7.68999,0.42452 0.3331,25.63529 -8.32933,0.15312 z"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <rect
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect4167"
+ width="72.75943"
+ height="57.601215"
+ x="132.30933"
+ y="452.36151" />
+ <rect
+ y="260.76169"
+ x="132.30933"
+ height="57.601215"
+ width="72.75943"
+ id="rect4165"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="zcczz"
+ inkscape:connector-curvature="0"
+ id="rect4169"
+ d="m 100.8325,342.6577 c 0.30392,-3.90655 7.22368,-3.07329 7.22368,-3.07329 l 0,25.47811 c 0,0 -8.679998,-0.74378 -8.36809,-1.72206 0.311907,-0.97828 0.84049,-16.77621 1.14441,-20.68276 z"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 217.19532,464.702 -49.95598,8.88102 -50.08823,-8.88102 0,-12.12657 100.04421,0 z"
+ id="path4175"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="rect4172"
+ d="m 117.15111,306.23633 49.95599,-8.88102 50.08822,8.88102 0,12.12658 -100.04421,0 z"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="cczcc"
+ inkscape:connector-curvature="0"
+ id="rect4181"
+ d="m 228.43002,399.28372 c 0,0 3.12853,0.50936 8.38696,0.2714 0.0814,8.34833 -0.73306,22.88772 -1.12555,25.68087 -0.3925,2.79316 -6.67578,-0.30624 -6.67578,-0.30624 z"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="zzzzzzzzz"
+ inkscape:connector-curvature="0"
+ id="rect4149"
+ d="m 110.99812,320.90892 c 9.74457,-8.4453 40.09435,-8.09024 59.20983,-8.06857 19.11548,0.0217 44.60536,-0.32419 54.99957,9.20383 10.39421,9.52802 7.83198,43.0413 7.89177,64.31485 0.0598,21.27356 3.15208,49.37176 -7.89176,60.4156 -11.04385,11.04384 -37.64935,8.68791 -57.03195,8.51204 -19.38261,-0.17587 -45.6217,2.73747 -56.88209,-7.44019 -11.26039,-10.17766 -8.4329,-45.75175 -8.32088,-66.68431 0.11202,-20.93257 -1.71905,-51.80796 8.02551,-60.25325 z"
+ style="opacity:1;fill:#cbcbcb;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 133.22587,330.48948 69.72779,0 c 6.71812,0 11.83367,5.41484 12.12657,12.12657 1.19073,27.28478 1.19022,54.56956 0,81.85435 -0.29278,6.71174 -5.40845,12.12657 -12.12657,12.12657 l -69.72779,0 c -6.71812,0 -11.82926,-5.41504 -12.12657,-12.12657 -1.18246,-26.69356 -1.65764,-53.74075 0,-81.85435 0.39543,-6.70647 5.40845,-12.12657 12.12657,-12.12657 z"
+ id="rect4152"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="sssssssss" />
+ <rect
+ y="342.61606"
+ x="131.62029"
+ height="81.854355"
+ width="72.75943"
+ id="rect4154"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="cccccccc"
+ inkscape:connector-curvature="0"
+ id="rect4156"
+ d="m 189.2215,342.61605 15.15821,0 0,81.85436 -15.15821,0 0,-66.54581 -4.44052,-3.21555 4.44052,-2.98586 z"
+ style="opacity:1;fill:#78d3fc;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ </g>
+ <path
+ style="fill:#000000"
+ d="m 143.63128,393.17695 c -0.82884,-0.82884 0.32908,-1.46136 2.67524,-1.46136 3.02637,0 3.07586,-0.7116 0.31911,-4.58902 l -2.10477,-2.96041 2.0287,-2.94922 c 1.15063,-1.67272 2.20678,-4.13671 2.44014,-5.69282 0.40696,-2.71383 0.44252,-2.74828 3.27625,-3.17322 1.57564,-0.23629 4.03472,-1.23453 5.46462,-2.21832 1.42988,-0.98379 2.85437,-1.78871 3.16551,-1.78871 0.31114,0 1.68177,0.80847 3.04585,1.7966 1.40341,1.01663 3.70432,1.98019 5.29992,2.21946 2.92933,0.43928 2.99923,0.52347 3.81465,4.59457 0.23714,1.18397 1.22649,3.16119 2.19857,4.39382 0.97208,1.23265 1.76741,2.42825 1.76741,2.6569 0,0.22864 -0.83031,1.62253 -1.84514,3.09753 -2.44659,3.556 -2.38325,4.49115 0.32038,4.72947 1.19102,0.10499 2.22891,0.46766 2.30641,0.80594 0.15778,0.68864 -33.49245,1.21919 -34.17285,0.53879 z m 27.8729,-2.84521 c 0.009,-0.76112 0.83738,-2.4679 1.83992,-3.79283 l 1.82279,-2.40898 -1.58789,-2.08183 c -0.87334,-1.14501 -1.90888,-3.37431 -2.30119,-4.954 -0.68475,-2.75721 -0.82407,-2.89364 -3.48101,-3.40876 -1.52223,-0.29514 -3.68339,-1.22898 -4.80255,-2.0752 l -2.03486,-1.53859 -2.27046,1.53481 c -2.19134,1.48132 -2.85113,1.74296 -6.34208,2.51493 -1.32103,0.29212 -1.68348,0.80695 -2.01126,2.85674 -0.2199,1.37522 -1.11243,3.57721 -1.98338,4.8933 l -1.58355,2.39291 1.54803,2.0343 c 0.85141,1.11887 1.73796,2.79542 1.97011,3.72567 l 0.42209,1.69138 10.38909,0 c 10.35877,0 10.38914,-0.004 10.4062,-1.38385 z m -16.16204,-1.17442 c -3.00082,-0.83158 -3.29439,-2.70542 -0.30753,-1.96294 1.0994,0.27329 2.9676,0.49689 4.15156,0.49689 1.18396,0 3.05216,-0.2236 4.15155,-0.49689 1.34833,-0.33517 1.9989,-0.27586 1.9989,0.18225 0,1.61808 -6.44052,2.76557 -9.99448,1.78069 z m -1.87921,-5.21512 c -0.23497,-0.23497 -0.42721,-1.51123 -0.42721,-2.83614 0,-2.76084 1.40937,-2.98541 1.73071,-0.27578 0.21201,1.78766 -0.62633,3.78908 -1.3035,3.11192 z m 10.02856,-2.68238 c 0,-1.7768 0.25627,-2.46018 0.92256,-2.46018 0.6663,0 0.92257,0.68338 0.92257,2.46018 0,1.7768 -0.25627,2.46018 -0.92257,2.46018 -0.66629,0 -0.92256,-0.68338 -0.92256,-2.46018 z m -22.75668,2.7677 c 0,-0.69366 0.43166,-0.94682 1.38386,-0.81158 0.76111,0.10809 1.38385,0.47331 1.38385,0.81158 0,0.33828 -0.62274,0.70349 -1.38385,0.81159 -0.9522,0.13523 -1.38386,-0.11793 -1.38386,-0.81159 z m 37.8226,0.30318 c -0.46699,-0.7556 1.22568,-1.49279 2.13592,-0.93022 0.35999,0.22248 0.4953,0.66216 0.30068,0.97707 -0.46943,0.75955 -1.95589,0.73096 -2.4366,-0.0469 z m -32.49939,-13.42132 c -1.23387,-1.36341 -1.29125,-1.95046 -0.19065,-1.95046 1.05106,0 3.00139,2.11681 2.45435,2.66385 -0.65422,0.65422 -1.17438,0.49029 -2.2637,-0.71339 z m 27.38286,0.11435 c 0.23773,-1.20797 2.11252,-2.50883 2.72827,-1.89307 0.48678,0.48678 -1.47947,2.90348 -2.36232,2.90348 -0.31065,0 -0.47533,-0.45468 -0.36595,-1.01041 z m -13.38512,-4.7488 c -0.52662,-1.37234 -0.0519,-3.6431 0.82178,-3.93083 0.51752,-0.17043 0.76881,0.45533 0.76881,1.91443 0,2.29458 -0.99824,3.56005 -1.59059,2.0164 z"
+ id="path4285"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ssscsssssssssssssssscsssscsssscsscsssssssssssssssssssssscssccssssssssssss" />
+ <text
+ xml:space="preserve"
+ style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ x="136.46957"
+ y="405.82156"
+ id="text4342"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan4344"
+ x="136.46957"
+ y="405.82156"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10px;font-family:Ubuntu;-inkscape-font-specification:Ubuntu">Tomorrow</tspan></text>
+ </g>
+</svg>
diff --git a/rockwork/rockwork.url-dispatcher b/rockwork/rockwork.url-dispatcher
new file mode 100644
index 0000000..3453482
--- /dev/null
+++ b/rockwork/rockwork.url-dispatcher
@@ -0,0 +1,5 @@
+[
+ {
+ "protocol": "pebblejs"
+ }
+]
diff --git a/rockwork/screenshotmodel.cpp b/rockwork/screenshotmodel.cpp
new file mode 100644
index 0000000..e943aa6
--- /dev/null
+++ b/rockwork/screenshotmodel.cpp
@@ -0,0 +1,71 @@
+#include "screenshotmodel.h"
+
+#include <QDebug>
+
+ScreenshotModel::ScreenshotModel(QObject *parent):
+ QAbstractListModel(parent)
+{
+}
+
+int ScreenshotModel::rowCount(const QModelIndex &parent) const
+{
+ Q_UNUSED(parent)
+ return m_files.count();
+}
+
+QVariant ScreenshotModel::data(const QModelIndex &index, int role) const
+{
+ switch (role) {
+ case RoleFileName:
+ return m_files.at(index.row());
+ }
+ return QVariant();
+}
+
+QHash<int, QByteArray> ScreenshotModel::roleNames() const
+{
+ QHash<int, QByteArray> roles;
+ roles.insert(RoleFileName, "filename");
+ return roles;
+}
+
+QString ScreenshotModel::get(int index) const
+{
+ if (index >= 0 && index < m_files.count()) {
+ return m_files.at(index);
+ }
+ return QString();
+}
+
+QString ScreenshotModel::latestScreenshot() const
+{
+ return get(0);
+}
+
+void ScreenshotModel::clear()
+{
+ beginResetModel();
+ m_files.clear();
+ endResetModel();
+}
+
+void ScreenshotModel::insert(const QString &filename)
+{
+ qDebug() << "should insert filename" << filename;
+ if (!m_files.contains(filename)) {
+ beginInsertRows(QModelIndex(), 0, 0);
+ m_files.prepend(filename);
+ endInsertRows();
+ emit latestScreenshotChanged();
+ }
+}
+
+void ScreenshotModel::remove(const QString &filename)
+{
+ if (m_files.contains(filename)) {
+ int idx = m_files.indexOf(filename);
+ beginRemoveRows(QModelIndex(), idx, idx);
+ m_files.removeOne(filename);
+ endRemoveRows();
+ }
+}
diff --git a/rockwork/screenshotmodel.h b/rockwork/screenshotmodel.h
new file mode 100644
index 0000000..bc855f1
--- /dev/null
+++ b/rockwork/screenshotmodel.h
@@ -0,0 +1,38 @@
+#ifndef SCREENSHOTMODEL_H
+#define SCREENSHOTMODEL_H
+
+#include <QAbstractListModel>
+
+class ScreenshotModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+ Q_PROPERTY(QString latestScreenshot READ latestScreenshot NOTIFY latestScreenshotChanged)
+
+public:
+ enum Role {
+ RoleFileName
+ };
+
+ ScreenshotModel(QObject *parent = nullptr);
+ QString path() const;
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ QHash<int, QByteArray> roleNames() const override;
+
+ Q_INVOKABLE QString get(int index) const;
+ QString latestScreenshot() const;
+
+ void clear();
+ void insert(const QString &filename);
+ void remove(const QString &filename);
+
+signals:
+ void latestScreenshotChanged();
+
+private:
+ QStringList m_files;
+};
+
+#endif // SCREENSHOTMODEL_H
diff --git a/rockwork/servicecontrol.cpp b/rockwork/servicecontrol.cpp
new file mode 100644
index 0000000..4d6903f
--- /dev/null
+++ b/rockwork/servicecontrol.cpp
@@ -0,0 +1,118 @@
+#include "servicecontrol.h"
+
+#include <QFile>
+#include <QDir>
+#include <QDebug>
+#include <QCoreApplication>
+#include <QProcess>
+
+ServiceControl::ServiceControl(QObject *parent) : QObject(parent)
+{
+
+}
+
+QString ServiceControl::serviceName() const
+{
+ return m_serviceName;
+}
+
+void ServiceControl::setServiceName(const QString &serviceName)
+{
+ if (m_serviceName != serviceName) {
+ m_serviceName = serviceName;
+ emit serviceNameChanged();
+ }
+}
+
+bool ServiceControl::serviceFileInstalled() const
+{
+ if (m_serviceName.isEmpty()) {
+ qDebug() << "Service name not set.";
+ return false;
+ }
+ QFile f(QDir::homePath() + "/.config/upstart/" + m_serviceName + ".conf");
+ return f.exists();
+}
+
+bool ServiceControl::installServiceFile()
+{
+ if (m_serviceName.isEmpty()) {
+ qDebug() << "Service name not set. Cannot generate service file.";
+ return false;
+ }
+
+ QFile f(QDir::homePath() + "/.config/upstart/" + m_serviceName + ".conf");
+ if (f.exists()) {
+ qDebug() << "Service file already existing...";
+ return false;
+ }
+
+ if (!f.open(QFile::WriteOnly | QFile::Truncate)) {
+ qDebug() << "Cannot create service file";
+ return false;
+ }
+
+ QString appDir = qApp->applicationDirPath();
+ // Try to replace version with "current" to be more robust against updates
+ appDir.replace(QRegExp("rockwork.mzanetti\/[0-9.]*\/"), "rockwork.mzanetti/current/");
+
+ f.write("start on started unity8\n");
+ f.write("pre-start script\n");
+ f.write(" initctl set-env LD_LIBRARY_PATH=" + appDir.toUtf8() + "/../:$LD_LIBRARY_PATH\n");
+ f.write("end script\n");
+ f.write("exec " + appDir.toUtf8() + "/" + m_serviceName.toUtf8() + "\n");
+ f.close();
+ return true;
+}
+
+bool ServiceControl::removeServiceFile()
+{
+ if (m_serviceName.isEmpty()) {
+ qDebug() << "Service name not set.";
+ return false;
+ }
+ QFile f(QDir::homePath() + "/.config/upstart/" + m_serviceName + ".conf");
+ return f.remove();
+}
+
+bool ServiceControl::serviceRunning() const
+{
+ QProcess p;
+ p.start("initctl", {"status", m_serviceName});
+ p.waitForFinished();
+ QByteArray output = p.readAll();
+ qDebug() << output;
+ return output.contains("running");
+}
+
+bool ServiceControl::setServiceRunning(bool running)
+{
+ if (running && !serviceRunning()) {
+ return startService();
+ } else if (!running && serviceRunning()) {
+ return stopService();
+ }
+ return true; // Requested state is already the current state.
+}
+
+bool ServiceControl::startService()
+{
+ qDebug() << "should start service";
+ int ret = QProcess::execute("start", {m_serviceName});
+ return ret == 0;
+}
+
+bool ServiceControl::stopService()
+{
+ qDebug() << "should stop service";
+ int ret = QProcess::execute("stop", {m_serviceName});
+ return ret == 0;
+}
+
+bool ServiceControl::restartService()
+{
+ qDebug() << "should stop service";
+ int ret = QProcess::execute("restart", {m_serviceName});
+ return ret == 0;
+}
+
diff --git a/rockwork/servicecontrol.h b/rockwork/servicecontrol.h
new file mode 100644
index 0000000..4689506
--- /dev/null
+++ b/rockwork/servicecontrol.h
@@ -0,0 +1,38 @@
+#ifndef SERVICECONTROL_H
+#define SERVICECONTROL_H
+
+#include <QObject>
+
+class ServiceControl : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QString serviceName READ serviceName WRITE setServiceName NOTIFY serviceNameChanged)
+ Q_PROPERTY(bool serviceFileInstalled READ serviceFileInstalled NOTIFY serviceFileInstalledChanged)
+ Q_PROPERTY(bool serviceRunning READ serviceRunning WRITE setServiceRunning NOTIFY serviceRunningChanged)
+
+public:
+ explicit ServiceControl(QObject *parent = 0);
+
+ QString serviceName() const;
+ void setServiceName(const QString &serviceName);
+
+ bool serviceFileInstalled() const;
+ Q_INVOKABLE bool installServiceFile();
+ Q_INVOKABLE bool removeServiceFile();
+
+ bool serviceRunning() const;
+ bool setServiceRunning(bool running);
+ Q_INVOKABLE bool startService();
+ Q_INVOKABLE bool stopService();
+ Q_INVOKABLE bool restartService();
+
+signals:
+ void serviceNameChanged();
+ void serviceFileInstalledChanged();
+ void serviceRunningChanged();
+
+private:
+ QString m_serviceName;
+};
+
+#endif // SERVICECONTROL_H
diff --git a/rockwork/snowywhite.png b/rockwork/snowywhite.png
new file mode 100644
index 0000000..1a354b4
--- /dev/null
+++ b/rockwork/snowywhite.png
Binary files differ
diff --git a/rockwork/snowywhite.svg b/rockwork/snowywhite.svg
new file mode 100644
index 0000000..0544670
--- /dev/null
+++ b/rockwork/snowywhite.svg
@@ -0,0 +1,241 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="72.248886mm"
+ height="72.248886mm"
+ viewBox="0 0 255.99999 255.99999"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="snowywhite.svg">
+ <defs
+ id="defs4">
+ <filter
+ inkscape:collect="always"
+ style="color-interpolation-filters:sRGB"
+ id="filter4364"
+ x="-0.059098901"
+ width="1.1181978"
+ y="-0.032575137"
+ height="1.0651503">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="5.5784918"
+ id="feGaussianBlur4366" />
+ </filter>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="3.959798"
+ inkscape:cx="89.121544"
+ inkscape:cy="77.044911"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="2880"
+ inkscape:window-height="1752"
+ inkscape:window-x="0"
+ inkscape:window-y="48"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136"
+ originx="-40.000001"
+ originy="-539"
+ snapvisiblegridlinesonly="true"
+ enabled="false" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-40,-257.36221)">
+ <g
+ transform="matrix(0.60632857,0,0,0.60632857,-37.462675,74.399202)"
+ id="g4202"
+ style="opacity:0.581;fill:#000000;fill-opacity:1;filter:url(#filter4364)">
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 437.97969,445.08937 c 0,0 11.49464,-4.59544 12.27285,0.25253 0.77821,4.84797 2.06459,45.23266 2.06459,45.23266 -8.36034,0.32794 -13.15013,-0.0886 -13.15013,-0.0886 z"
+ id="path4204"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="czccc" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 439.49492,491.6046 12.68287,0.70015 0.54937,42.27954 -13.73731,0.25254 z"
+ id="path4206"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <rect
+ y="623.36218"
+ x="280"
+ height="95"
+ width="120"
+ id="rect4208"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <rect
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect4210"
+ width="120"
+ height="95"
+ x="280"
+ y="307.36221" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 228.0862,442.4309 C 228.58744,435.98794 240,437.36221 240,437.36221 l 0,42.02031 c 0,0 -14.31567,-1.22669 -13.80125,-2.84014 0.51442,-1.61345 1.3862,-27.66851 1.88745,-34.11148 z"
+ id="path4212"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="zcczz" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="path4214"
+ d="M 420,643.715 337.60905,658.36221 255,643.715 l 0,-20 165,0 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 255,382.36221 337.39095,367.715 420,382.36221 l 0,20 -165,0 z"
+ id="path4216"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 438.52906,535.82255 c 0,0 5.15979,0.84007 13.83236,0.44761 0.13423,13.76866 -1.20901,37.74804 -1.85634,42.35471 -0.64733,4.60667 -11.01016,-0.50508 -11.01016,-0.50508 z"
+ id="path4218"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cczcc" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 244.85206,406.56127 c 16.07143,-13.92858 66.12644,-13.34299 97.65304,-13.30725 31.5266,0.0357 73.56632,-0.53467 90.70918,15.17961 17.14286,15.71428 12.91706,70.98675 13.01566,106.0726 0.0986,35.08586 5.19864,81.42741 -13.01565,99.64169 C 415,632.3622 371.12033,628.47664 339.15317,628.18658 307.186,627.89652 263.91063,632.7014 245.3392,615.91569 226.76777,599.12997 231.43107,540.45867 231.61582,505.9352 c 0.18475,-34.52347 -2.83519,-85.44536 13.23624,-99.37393 z"
+ id="path4220"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="zzzzzzzzz" />
+ <rect
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect4222"
+ width="155"
+ height="174.99998"
+ x="260"
+ y="422.36221"
+ rx="20"
+ ry="20" />
+ <rect
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect4224"
+ width="120"
+ height="135"
+ x="280"
+ y="442.36221" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 375,442.36221 25,0 0,135 -25,0 0,-109.75206 -7.32361,-5.3033 7.32361,-4.9245 z"
+ id="path4226"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccccc" />
+ </g>
+ <path
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 228.09692,344.2696 c 0,0 6.96953,-2.78634 7.44138,0.15312 0.47185,2.93946 1.25182,27.42585 1.25182,27.42585 -5.06911,0.19884 -7.9733,-0.0537 -7.9733,-0.0537 z"
+ id="rect4177"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="czccc" />
+ <path
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 229.01565,372.47312 7.68999,0.42452 0.3331,25.63529 -8.32933,0.15312 z"
+ id="rect4179"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <rect
+ y="452.36151"
+ x="132.30933"
+ height="57.601215"
+ width="72.75943"
+ id="rect4167"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <rect
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect4165"
+ width="72.75943"
+ height="57.601215"
+ x="132.30933"
+ y="260.76169" />
+ <path
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 100.8325,342.6577 c 0.30392,-3.90655 7.22368,-3.07329 7.22368,-3.07329 l 0,25.47811 c 0,0 -8.679998,-0.74378 -8.36809,-1.72206 0.311907,-0.97828 0.84049,-16.77621 1.14441,-20.68276 z"
+ id="rect4169"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="zcczz" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="path4175"
+ d="m 217.19532,464.702 -49.95598,8.88102 -50.08823,-8.88102 0,-12.12657 100.04421,0 z"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 117.15111,306.23633 49.95599,-8.88102 50.08822,8.88102 0,12.12658 -100.04421,0 z"
+ id="rect4172"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc" />
+ <path
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 228.43002,399.28372 c 0,0 3.12853,0.50936 8.38696,0.2714 0.0814,8.34833 -0.73306,22.88772 -1.12555,25.68087 -0.3925,2.79316 -6.67578,-0.30624 -6.67578,-0.30624 z"
+ id="rect4181"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cczcc" />
+ <path
+ style="opacity:1;fill:#cbcbcb;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 110.99812,320.90892 c 9.74457,-8.4453 40.09435,-8.09024 59.20983,-8.06857 19.11548,0.0217 44.60536,-0.32419 54.99957,9.20383 10.39421,9.52802 7.83198,43.0413 7.89177,64.31485 0.0598,21.27356 3.15208,49.37176 -7.89176,60.4156 -11.04385,11.04384 -37.64935,8.68791 -57.03195,8.51204 -19.38261,-0.17587 -45.6217,2.73747 -56.88209,-7.44019 -11.26039,-10.17766 -8.4329,-45.75175 -8.32088,-66.68431 0.11202,-20.93257 -1.71905,-51.80796 8.02551,-60.25325 z"
+ id="rect4149"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="zzzzzzzzz" />
+ <path
+ sodipodi:nodetypes="sssssssss"
+ inkscape:connector-curvature="0"
+ id="rect4152"
+ d="m 133.22587,330.48948 69.72779,0 c 6.71812,0 11.83367,5.41484 12.12657,12.12657 1.19073,27.28478 1.19022,54.56956 0,81.85435 -0.29278,6.71174 -5.40845,12.12657 -12.12657,12.12657 l -69.72779,0 c -6.71812,0 -11.82926,-5.41504 -12.12657,-12.12657 -1.18246,-26.69356 -1.65764,-53.74075 0,-81.85435 0.39543,-6.70647 5.40845,-12.12657 12.12657,-12.12657 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <rect
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect4154"
+ width="72.75943"
+ height="81.854355"
+ x="131.62029"
+ y="342.61606" />
+ <path
+ style="opacity:1;fill:#78d3fc;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 189.2215,342.61605 15.15821,0 0,81.85436 -15.15821,0 0,-66.54581 -4.44052,-3.21555 4.44052,-2.98586 z"
+ id="rect4156"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccccc" />
+ </g>
+</svg>