From 29aaea2d80a9eb1715b6cddfac2d2aacf76358bd Mon Sep 17 00:00:00 2001 From: Andrew Branson Date: Thu, 11 Feb 2016 23:55:16 +0100 Subject: launchpad ~mzanetti/rockwork/trunk r87 --- rockwork/AppSettingsPage.qml | 78 +++++ rockwork/AppStoreDetailsPage.qml | 278 +++++++++++++++++ rockwork/AppStorePage.qml | 266 +++++++++++++++++ rockwork/ContentPeerPickerPage.qml | 41 +++ rockwork/DeveloperToolsPage.qml | 157 ++++++++++ rockwork/FirmwareUpgradePage.qml | 58 ++++ rockwork/HealthSettingsDialog.qml | 115 +++++++ rockwork/ImportPackagePage.qml | 32 ++ rockwork/InfoPage.qml | 86 ++++++ rockwork/InstalledAppDelegate.qml | 88 ++++++ rockwork/InstalledAppsPage.qml | 201 +++++++++++++ rockwork/Main.qml | 53 ++++ rockwork/MainMenuPage.qml | 317 ++++++++++++++++++++ rockwork/NotificationsPage.qml | 88 ++++++ rockwork/PebbleModels.qml | 28 ++ rockwork/PebblesPage.qml | 69 +++++ rockwork/ScreenshotsPage.qml | 107 +++++++ rockwork/SettingsPage.qml | 80 +++++ rockwork/SystemAppIcon.qml | 67 +++++ rockwork/applicationsfiltermodel.cpp | 102 +++++++ rockwork/applicationsfiltermodel.h | 54 ++++ rockwork/applicationsmodel.cpp | 365 ++++++++++++++++++++++ rockwork/applicationsmodel.h | 160 ++++++++++ rockwork/appstoreclient.cpp | 323 ++++++++++++++++++++ rockwork/appstoreclient.h | 62 ++++ rockwork/artwork/bianca-black.png | Bin 0 -> 9165 bytes rockwork/artwork/bianca-silver.png | Bin 0 -> 10766 bytes rockwork/artwork/black-20mm-hole.png | Bin 0 -> 44522 bytes rockwork/artwork/bobby-black.png | Bin 0 -> 16390 bytes rockwork/artwork/bobby-gold.png | Bin 0 -> 35026 bytes rockwork/artwork/bobby-silver.png | Bin 0 -> 16599 bytes rockwork/artwork/rockwork.svg | 275 +++++++++++++++++ rockwork/artwork/snowy-black.png | Bin 0 -> 28360 bytes rockwork/artwork/snowy-red.png | Bin 0 -> 29962 bytes rockwork/artwork/snowy-white.png | Bin 0 -> 25610 bytes rockwork/artwork/spalding-14mm-black.png | Bin 0 -> 45124 bytes rockwork/artwork/spalding-14mm-rose-gold.png | Bin 0 -> 52798 bytes rockwork/artwork/spalding-14mm-silver.png | Bin 0 -> 36782 bytes rockwork/artwork/spalding-20mm-black.png | Bin 0 -> 44592 bytes rockwork/artwork/spalding-20mm-silver.png | Bin 0 -> 38164 bytes rockwork/artwork/tintin-black.png | Bin 0 -> 5497 bytes rockwork/artwork/tintin-blue.png | Bin 0 -> 8409 bytes rockwork/artwork/tintin-green.png | Bin 0 -> 8338 bytes rockwork/artwork/tintin-grey.png | Bin 0 -> 5493 bytes rockwork/artwork/tintin-orange.png | Bin 0 -> 6384 bytes rockwork/artwork/tintin-pink.png | Bin 0 -> 8897 bytes rockwork/artwork/tintin-red.png | Bin 0 -> 6160 bytes rockwork/artwork/tintin-white.png | Bin 0 -> 6089 bytes rockwork/main.cpp | 37 +++ rockwork/notificationsourcemodel.cpp | 117 ++++++++ rockwork/notificationsourcemodel.h | 48 +++ rockwork/org.freedesktop.Notifications.xml | 45 +++ rockwork/pebble.cpp | 432 +++++++++++++++++++++++++++ rockwork/pebble.h | 131 ++++++++ rockwork/pebbles.cpp | 180 +++++++++++ rockwork/pebbles.h | 56 ++++ rockwork/rockwork.apparmor | 7 + rockwork/rockwork.desktop | 8 + rockwork/rockwork.pro | 72 +++++ rockwork/rockwork.qrc | 48 +++ rockwork/rockwork.svg | 275 +++++++++++++++++ rockwork/rockwork.url-dispatcher | 5 + rockwork/screenshotmodel.cpp | 71 +++++ rockwork/screenshotmodel.h | 38 +++ rockwork/servicecontrol.cpp | 118 ++++++++ rockwork/servicecontrol.h | 38 +++ rockwork/snowywhite.png | Bin 0 -> 14213 bytes rockwork/snowywhite.svg | 241 +++++++++++++++ 68 files changed, 5517 insertions(+) create mode 100644 rockwork/AppSettingsPage.qml create mode 100644 rockwork/AppStoreDetailsPage.qml create mode 100644 rockwork/AppStorePage.qml create mode 100644 rockwork/ContentPeerPickerPage.qml create mode 100644 rockwork/DeveloperToolsPage.qml create mode 100644 rockwork/FirmwareUpgradePage.qml create mode 100644 rockwork/HealthSettingsDialog.qml create mode 100644 rockwork/ImportPackagePage.qml create mode 100644 rockwork/InfoPage.qml create mode 100644 rockwork/InstalledAppDelegate.qml create mode 100644 rockwork/InstalledAppsPage.qml create mode 100644 rockwork/Main.qml create mode 100644 rockwork/MainMenuPage.qml create mode 100644 rockwork/NotificationsPage.qml create mode 100644 rockwork/PebbleModels.qml create mode 100644 rockwork/PebblesPage.qml create mode 100644 rockwork/ScreenshotsPage.qml create mode 100644 rockwork/SettingsPage.qml create mode 100644 rockwork/SystemAppIcon.qml create mode 100644 rockwork/applicationsfiltermodel.cpp create mode 100644 rockwork/applicationsfiltermodel.h create mode 100644 rockwork/applicationsmodel.cpp create mode 100644 rockwork/applicationsmodel.h create mode 100644 rockwork/appstoreclient.cpp create mode 100644 rockwork/appstoreclient.h create mode 100644 rockwork/artwork/bianca-black.png create mode 100644 rockwork/artwork/bianca-silver.png create mode 100644 rockwork/artwork/black-20mm-hole.png create mode 100644 rockwork/artwork/bobby-black.png create mode 100644 rockwork/artwork/bobby-gold.png create mode 100644 rockwork/artwork/bobby-silver.png create mode 100644 rockwork/artwork/rockwork.svg create mode 100644 rockwork/artwork/snowy-black.png create mode 100644 rockwork/artwork/snowy-red.png create mode 100644 rockwork/artwork/snowy-white.png create mode 100644 rockwork/artwork/spalding-14mm-black.png create mode 100644 rockwork/artwork/spalding-14mm-rose-gold.png create mode 100644 rockwork/artwork/spalding-14mm-silver.png create mode 100644 rockwork/artwork/spalding-20mm-black.png create mode 100644 rockwork/artwork/spalding-20mm-silver.png create mode 100644 rockwork/artwork/tintin-black.png create mode 100644 rockwork/artwork/tintin-blue.png create mode 100644 rockwork/artwork/tintin-green.png create mode 100644 rockwork/artwork/tintin-grey.png create mode 100644 rockwork/artwork/tintin-orange.png create mode 100644 rockwork/artwork/tintin-pink.png create mode 100644 rockwork/artwork/tintin-red.png create mode 100644 rockwork/artwork/tintin-white.png create mode 100644 rockwork/main.cpp create mode 100644 rockwork/notificationsourcemodel.cpp create mode 100644 rockwork/notificationsourcemodel.h create mode 100644 rockwork/org.freedesktop.Notifications.xml create mode 100644 rockwork/pebble.cpp create mode 100644 rockwork/pebble.h create mode 100644 rockwork/pebbles.cpp create mode 100644 rockwork/pebbles.h create mode 100644 rockwork/rockwork.apparmor create mode 100644 rockwork/rockwork.desktop create mode 100644 rockwork/rockwork.pro create mode 100644 rockwork/rockwork.qrc create mode 100644 rockwork/rockwork.svg create mode 100644 rockwork/rockwork.url-dispatcher create mode 100644 rockwork/screenshotmodel.cpp create mode 100644 rockwork/screenshotmodel.h create mode 100644 rockwork/servicecontrol.cpp create mode 100644 rockwork/servicecontrol.h create mode 100644 rockwork/snowywhite.png create mode 100644 rockwork/snowywhite.svg (limited to 'rockwork') 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("" + root.pebble.softwareVersion + "") + width: parent.width + wrapMode: Text.WordWrap + } + + Label { + text: i18n.tr("Candidate firmware version: %1").arg("" + root.pebble.candidateVersion + "") + width: parent.width + wrapMode: Text.WordWrap + } + + Label { + text: "" + i18n.tr("Release Notes: %1").arg("
" + root.pebble.firmwareReleaseNotes) + width: parent.width + wrapMode: Text.WordWrap + } + + Label { + text: "" + i18n.tr("Important:") + " " + 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
Brian Douglas
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.
" + + + "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.
" + + + "You should have received a copy of the GNU General Public License" + + "along with this program. If not, see ." + 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 + +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 + + +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 ApplicationsModel::roleNames() const +{ + QHash 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 +#include + +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 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 m_apps; + QHash m_groupNames; + QHash m_groupLinks; + QStringList m_links; + QHash 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 +#include +#include +#include +#include +#include + +#include + +/* 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 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 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 + +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 Binary files /dev/null and b/rockwork/artwork/bianca-black.png differ diff --git a/rockwork/artwork/bianca-silver.png b/rockwork/artwork/bianca-silver.png new file mode 100644 index 0000000..2d003e8 Binary files /dev/null and b/rockwork/artwork/bianca-silver.png differ diff --git a/rockwork/artwork/black-20mm-hole.png b/rockwork/artwork/black-20mm-hole.png new file mode 100644 index 0000000..ff61e66 Binary files /dev/null and b/rockwork/artwork/black-20mm-hole.png differ diff --git a/rockwork/artwork/bobby-black.png b/rockwork/artwork/bobby-black.png new file mode 100644 index 0000000..83177b5 Binary files /dev/null and b/rockwork/artwork/bobby-black.png differ diff --git a/rockwork/artwork/bobby-gold.png b/rockwork/artwork/bobby-gold.png new file mode 100644 index 0000000..d97f2f4 Binary files /dev/null and b/rockwork/artwork/bobby-gold.png differ diff --git a/rockwork/artwork/bobby-silver.png b/rockwork/artwork/bobby-silver.png new file mode 100644 index 0000000..44efdf8 Binary files /dev/null and b/rockwork/artwork/bobby-silver.png 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 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tomorrow + + diff --git a/rockwork/artwork/snowy-black.png b/rockwork/artwork/snowy-black.png new file mode 100644 index 0000000..acf3439 Binary files /dev/null and b/rockwork/artwork/snowy-black.png differ diff --git a/rockwork/artwork/snowy-red.png b/rockwork/artwork/snowy-red.png new file mode 100644 index 0000000..b0bdc8e Binary files /dev/null and b/rockwork/artwork/snowy-red.png differ diff --git a/rockwork/artwork/snowy-white.png b/rockwork/artwork/snowy-white.png new file mode 100644 index 0000000..3bfe6d1 Binary files /dev/null and b/rockwork/artwork/snowy-white.png differ diff --git a/rockwork/artwork/spalding-14mm-black.png b/rockwork/artwork/spalding-14mm-black.png new file mode 100644 index 0000000..47b5b03 Binary files /dev/null and b/rockwork/artwork/spalding-14mm-black.png 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 Binary files /dev/null and b/rockwork/artwork/spalding-14mm-rose-gold.png differ diff --git a/rockwork/artwork/spalding-14mm-silver.png b/rockwork/artwork/spalding-14mm-silver.png new file mode 100644 index 0000000..bcc5f16 Binary files /dev/null and b/rockwork/artwork/spalding-14mm-silver.png differ diff --git a/rockwork/artwork/spalding-20mm-black.png b/rockwork/artwork/spalding-20mm-black.png new file mode 100644 index 0000000..d00a1f7 Binary files /dev/null and b/rockwork/artwork/spalding-20mm-black.png differ diff --git a/rockwork/artwork/spalding-20mm-silver.png b/rockwork/artwork/spalding-20mm-silver.png new file mode 100644 index 0000000..18b0e02 Binary files /dev/null and b/rockwork/artwork/spalding-20mm-silver.png differ diff --git a/rockwork/artwork/tintin-black.png b/rockwork/artwork/tintin-black.png new file mode 100644 index 0000000..dcf2c31 Binary files /dev/null and b/rockwork/artwork/tintin-black.png differ diff --git a/rockwork/artwork/tintin-blue.png b/rockwork/artwork/tintin-blue.png new file mode 100644 index 0000000..eca2d3b Binary files /dev/null and b/rockwork/artwork/tintin-blue.png differ diff --git a/rockwork/artwork/tintin-green.png b/rockwork/artwork/tintin-green.png new file mode 100644 index 0000000..17df060 Binary files /dev/null and b/rockwork/artwork/tintin-green.png differ diff --git a/rockwork/artwork/tintin-grey.png b/rockwork/artwork/tintin-grey.png new file mode 100644 index 0000000..4f9988b Binary files /dev/null and b/rockwork/artwork/tintin-grey.png differ diff --git a/rockwork/artwork/tintin-orange.png b/rockwork/artwork/tintin-orange.png new file mode 100644 index 0000000..5956126 Binary files /dev/null and b/rockwork/artwork/tintin-orange.png differ diff --git a/rockwork/artwork/tintin-pink.png b/rockwork/artwork/tintin-pink.png new file mode 100644 index 0000000..ee69d67 Binary files /dev/null and b/rockwork/artwork/tintin-pink.png differ diff --git a/rockwork/artwork/tintin-red.png b/rockwork/artwork/tintin-red.png new file mode 100644 index 0000000..6c7b7e2 Binary files /dev/null and b/rockwork/artwork/tintin-red.png differ diff --git a/rockwork/artwork/tintin-white.png b/rockwork/artwork/tintin-white.png new file mode 100644 index 0000000..912ea19 Binary files /dev/null and b/rockwork/artwork/tintin-white.png 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 +#include +#include +#include +#include + +#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("RockWork", 1, 0, "Pebble", "Get them from the model"); + qmlRegisterUncreatableType("RockWork", 1, 0, "ApplicationsModel", "Get them from a Pebble object"); + qmlRegisterUncreatableType("RockWork", 1, 0, "AppItem", "Get them from an ApplicationsModel"); + qmlRegisterType("RockWork", 1, 0, "ApplicationsFilterModel"); + qmlRegisterType("RockWork", 1, 0, "Pebbles"); + qmlRegisterUncreatableType("RockWork", 1, 0, "NotificationSourceModel", "Get it from a Pebble object"); + qmlRegisterType("RockWork", 1, 0, "ServiceController"); + qmlRegisterType("RockWork", 1, 0, "AppStoreClient"); + qmlRegisterType("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 +#include + +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 NotificationSourceModel::roleNames() const +{ + QHash 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 +#include +#include + +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 + +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 roleNames() const override; + + void insert(const QString &sourceId, bool enabled); + +signals: + void countChanged(); + +private: + NotificationSourceItem fromDesktopFile(const QString &sourceId); + +private: + QList 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 +#include + +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(); + + 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(); + + 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(); + + QVariantList appList; + + arg.beginArray(); + while (!arg.atEnd()) { + QVariant mapEntryVariant; + arg >> mapEntryVariant; + + QDBusArgument mapEntry = mapEntryVariant.value(); + 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 +#include + +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 +#include +#include +#include +#include +#include + +#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 Pebbles::roleNames() const +{ + QHash 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(); + 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 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(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 +#include +#include +#include + +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 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 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 @@ + + + Main.qml + NotificationsPage.qml + PebblesPage.qml + InstalledAppsPage.qml + MainMenuPage.qml + AppSettingsPage.qml + AppStorePage.qml + AppStoreDetailsPage.qml + InstalledAppDelegate.qml + SystemAppIcon.qml + ScreenshotsPage.qml + snowywhite.svg + snowywhite.png + artwork/bianca-black.png + artwork/bianca-silver.png + artwork/black-20mm-hole.png + artwork/bobby-black.png + artwork/bobby-gold.png + artwork/bobby-silver.png + artwork/snowy-black.png + artwork/snowy-red.png + artwork/snowy-white.png + artwork/spalding-14mm-black.png + artwork/spalding-14mm-rose-gold.png + artwork/spalding-14mm-silver.png + artwork/spalding-20mm-black.png + artwork/spalding-20mm-silver.png + artwork/tintin-black.png + artwork/tintin-blue.png + artwork/tintin-green.png + artwork/tintin-grey.png + artwork/tintin-orange.png + artwork/tintin-pink.png + artwork/tintin-red.png + artwork/tintin-white.png + PebbleModels.qml + FirmwareUpgradePage.qml + InfoPage.qml + artwork/rockwork.svg + DeveloperToolsPage.qml + ContentPeerPickerPage.qml + HealthSettingsDialog.qml + SettingsPage.qml + ImportPackagePage.qml + + 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 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tomorrow + + 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 + +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 ScreenshotModel::roleNames() const +{ + QHash 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 + +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 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 +#include +#include +#include +#include + +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 + +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 Binary files /dev/null and b/rockwork/snowywhite.png 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 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -- cgit v1.2.3