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