diff --git a/examples/meshtastic/cpp/ble/ble.cpp b/examples/meshtastic/cpp/ble/ble.cpp index 9f8c41d..74e9e9f 100644 --- a/examples/meshtastic/cpp/ble/ble.cpp +++ b/examples/meshtastic/cpp/ble/ble.cpp @@ -13,8 +13,12 @@ BLE::BLE(const QBluetoothUuid& uuid) : mainServiceUuid(uuid) { connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &BLE::addDevice); +#if QT_VERSION < 0x060000 connect(discoveryAgent, QOverload::of(&QBluetoothDeviceDiscoveryAgent::error), this, &BLE::deviceScanError); +#else + connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::errorOccurred, this, &BLE::deviceScanError); +#endif connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, &BLE::deviceScanFinished); } @@ -83,8 +87,12 @@ void BLE::scanServices() { controller = QLowEnergyController::createCentral(currentDevice); connect(controller, &QLowEnergyController::connected, this, &BLE::deviceConnected); +#if QT_VERSION < 0x060000 connect(controller, QOverload::of(&QLowEnergyController::error), this, &BLE::errorReceived); +#else + connect(controller, &QLowEnergyController::errorOccurred, this, &BLE::errorReceived); +#endif connect(controller, &QLowEnergyController::disconnected, this, &BLE::deviceDisconnected); connect(controller, &QLowEnergyController::serviceDiscovered, diff --git a/examples/meshtastic/cpp/ble/ble_meshtastic.cpp b/examples/meshtastic/cpp/ble/ble_meshtastic.cpp index ab994b5..148e458 100644 --- a/examples/meshtastic/cpp/ble/ble_meshtastic.cpp +++ b/examples/meshtastic/cpp/ble/ble_meshtastic.cpp @@ -44,8 +44,12 @@ void BLE_ME::ini() { this, &BLE_ME::characteristicRead); connect(mainService, &QLowEnergyService::characteristicWritten, this, &BLE_ME::characteristicWritten); +#if QT_VERSION < 0x060000 connect(mainService, QOverload::of(&QLowEnergyService::error), this, &BLE_ME::serviceError); +#else + connect(mainService, &QLowEnergyService::errorOccurred, this, &BLE_ME::serviceError); +#endif connect(mainService, &QLowEnergyService::descriptorWritten, [](const QLowEnergyDescriptor&, const QByteArray& value) { @@ -86,7 +90,11 @@ void BLE_ME::searchCharacteristics() { qDebug() << "...found 'fromNum'"; // fromNum // enable notifications +#if QT_VERSION < 0x060000 notifications = ch.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration); +#else + notifications = ch.descriptor(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration); +#endif if (notifications.isValid()) { mainService->writeDescriptor(notifications, QByteArray::fromHex("0100")); qDebug() << "notifications enabled"; @@ -123,6 +131,12 @@ void BLE_ME::characteristicRead(const QLowEnergyCharacteristic&, if (data.isEmpty()) { if (!backgroundMode) { emitter->receivingDone(); + static bool startup = true; + if (startup) { + sendSavedBytes(); // for eventual, saved but not sent packets + } else { + startup = false; + } } } else { if (backgroundMode) { diff --git a/examples/meshtastic/cpp/qt.cpp b/examples/meshtastic/cpp/qt.cpp index ee032fd..aa4d924 100644 --- a/examples/meshtastic/cpp/qt.cpp +++ b/examples/meshtastic/cpp/qt.cpp @@ -59,7 +59,7 @@ QT::QT() : QObject() { QObject::connect(ble, &BLE::bleError, #endif []() { - ecl_fun("radios:reset-default-radio"); + ecl_fun("radios:reset"); }); #ifdef Q_OS_ANDROID @@ -81,7 +81,7 @@ QT::QT() : QObject() { QObject::connect(ble, &BLE_ME::receivedFromRadio, #endif [](const QByteArray& data, const QString& notified) { - ecl_fun("lora:received-from-radio", data, notified); + ecl_fun("lora:received-from-radio", data, notified.isEmpty() ? QVariant() : notified); }); #ifdef Q_OS_ANDROID diff --git a/examples/meshtastic/lisp/package.lisp b/examples/meshtastic/lisp/package.lisp index f0b295a..75085e4 100644 --- a/examples/meshtastic/lisp/package.lisp +++ b/examples/meshtastic/lisp/package.lisp @@ -124,7 +124,7 @@ #:clear #:device-discovered #:ini - #:reset-default-radio)) + #:reset)) (defpackage :location (:nicknames :loc) diff --git a/examples/meshtastic/lisp/radios.lisp b/examples/meshtastic/lisp/radios.lisp index ab14867..8e7819b 100644 --- a/examples/meshtastic/lisp/radios.lisp +++ b/examples/meshtastic/lisp/radios.lisp @@ -41,7 +41,6 @@ (qlater (lambda () (lora:start-device-discovery name))) (values)) -(defun reset-default-radio () - (app:change-setting :device nil) +(defun reset () (lora:start-device-discovery)) diff --git a/examples/meshtastic/platforms/macos/Info.plist b/examples/meshtastic/platforms/macos/Info.plist index 3b1ef40..8cf3c4e 100644 --- a/examples/meshtastic/platforms/macos/Info.plist +++ b/examples/meshtastic/platforms/macos/Info.plist @@ -24,5 +24,7 @@ NSBluetoothAlwaysUsageDescription For communicating with meshtastic radio devices. + NSBluetoothPeripheralUsageDescription + For communicating with meshtastic radio devices. diff --git a/examples/meshtastic/qml6/ext/common/ComboBox.qml b/examples/meshtastic/qml6/ext/common/ComboBox.qml new file mode 100644 index 0000000..8e077fa --- /dev/null +++ b/examples/meshtastic/qml6/ext/common/ComboBox.qml @@ -0,0 +1,54 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic + +ComboBox { + id: control + font.pixelSize: 16 + font.family: fontText.name + + delegate: ItemDelegate { + width: control.width + height: control.height + contentItem: Text { + text: modelData + font: control.font + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + highlighted: control.highlightedIndex === index + } + + contentItem: Text { + text: control.displayText + font: control.font + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + radius: 5 + color: "#f0f0f0" + } + + popup: Popup { + objectName: "popup" + y: control.height + width: control.width + 24 + implicitHeight: contentItem.implicitHeight + 14 + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + 10 + model: control.popup.visible ? control.delegateModel : null + currentIndex: control.highlightedIndex + } + + background: Rectangle { + color: "#e0e0e0" + border.width: 1 + border.color: "gray" + radius: 10 + } + } +} diff --git a/examples/meshtastic/qml6/ext/common/Help.qml b/examples/meshtastic/qml6/ext/common/Help.qml new file mode 100644 index 0000000..025bf46 --- /dev/null +++ b/examples/meshtastic/qml6/ext/common/Help.qml @@ -0,0 +1,142 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic + +Rectangle { + id: help + y: -rootItem.height + color: "#e0e0f0" + opacity: 0 + + Button { + width: 42 + height: width + z: 1 // stay on top + anchors.right: parent.right + flat: true + font.family: fontText.name + font.pixelSize: 22 + text: "x" + + onClicked: help.enabled = false + } + + Flickable { + id: flick + anchors.fill: parent + contentWidth: html.paintedWidth + 2 * html.padding + contentHeight: html.paintedHeight + 50 + clip: true + + Text { + id: html + width: help.width + padding: 10 + wrapMode: Text.WordWrap + font.family: fontText.name + font.pixelSize: 18 + color: "#303030" + textFormat: Text.RichText + text: " +

+ +
Radios +

+

+If you use more than 1 radio, switch here to the radio you want to use. +

+%1 +

+ +
Group +

+

+Here you can see the list of all radios using your same channel name. Every radio represents a person. This view is populated automatically. +

+

+Choose 'Broadcast' (on top) to send a message to every person in the group. +

+

+You can set a name to every radio/person listed here, which defaults to 'Anonym': a press-and-hold on the name will enter edit mode. +

+

+In the main menu you can change your channel name (which defaults to 'LongFast'). Only radios which share the same channel name are able to exchange messages. +

+

+A tap on the location item on the right shows a map with all known positions of the persons. The map is cached automatically for offline usage, which means: once you visited a place on the map, it will remain available even without internet connection. +

+

+To set your location manually, see 'hand' button (top right). This will override any eventually received GPS location. +

+

+ +
Messages +

+

+Since the message length is limited, the border of the editor will turn red if the message is too long for sending. +

+

+To copy a message to the clipboard, press-and-hold it. +

+

+To see the exact date of a message, tap on its hour. +

+

+To delete a message, swipe it to the right and tap on the delete button. +

+

+Tap on search (icon on the right) to enter/leave search mode. The search term (case insensitive) is highlighted in red. +

+

+Eventual unread messages from other persons are indicated by a red circle, and the number of unread messages in Group. +

+

+A double click on a message will switch to Group. +

+

 

+

Advanced topics

+

Simple signal strength test

+

+For a trivial signal test you can use the special text message :e (for 'echo'), which will send back the text you sent, adding signal SNR/RSSI, position and distance. This is convenient to test signal strength from different places, and have it logged in your messages. +

+

+Please note that this requires the receiver to run this app in foreground mode. +

+

Save / Restore app data

+

+A local web-server is included on mobile for saving and restoring all of: message database, app settings, eventually cached map tiles (for offline usage). Just use special text message :w (for 'web-server') and :ws (for 'stop web-server') after you're done. +

+

+After starting the server, enter the shown URL in your desktop browser, and follow the instructions. +

+

+Using this method you can easily transfer all data from one mobile device to any other device. +

+

+The desktop data paths are: +

    +
  • Linux:
    /home/<user>/.local/share/cl-meshtastic/ +
  • macOS:
    /Users/<user>/Library/Application Support/cl-meshtastic/ +
  • Windows:
    C:\\Users\\<user>\\AppData\\Local\\cl-meshtastic\\ +
+

+Eventual backups are saved in above path under backups/. On the desktop see 'Make backup' in main menu. +

+

+To autmatically restore data from a backup on the desktop, put the backup files directly in above path (that is, under .../cl-meshtastic/) and restart the app. The data will be restored and the (obsolete) backup files will be deleted. +

".arg((Qt.platform.os === "android") + ? "

N.B: If you previously used a radio with the official app, you'll need to set the radio to 'None (disabled)' in the official app first, otherwise it will not show up in this app.

" + : "") + + } + } + + states: [ + State { when: help.enabled; PropertyChanges { target: help; opacity: 1; y: 0; }}, + State { when: !help.enabled; PropertyChanges { target: help; opacity: 0; y: -rootItem.height; }} + ] + + transitions: [ + Transition { NumberAnimation { properties: "opacity,y"; duration: 300; easing.type: Easing.InOutQuad }} + ] +} diff --git a/examples/meshtastic/qml6/ext/common/Hourglass.qml b/examples/meshtastic/qml6/ext/common/Hourglass.qml new file mode 100644 index 0000000..efcf95d --- /dev/null +++ b/examples/meshtastic/qml6/ext/common/Hourglass.qml @@ -0,0 +1,42 @@ +import QtQuick + +Rectangle { + anchors.fill: parent + color: "#ccebc5" + visible: animation.running + + Image { + id: hourglass1 + anchors.centerIn: parent + width: 40 + fillMode: Image.PreserveAspectFit + source: "../../img/hourglass.png" + } + + Image { + id: hourglass2 + anchors.centerIn: parent + width: hourglass1.width + fillMode: Image.PreserveAspectFit + source: "../../img/hourglass.png" + opacity: 0 + } + + SequentialAnimation { + id: animation + objectName: "hourglass" + loops: Animation.Infinite + running: true + + RotationAnimation { target: hourglass1; from: 0; to: 180; duration: 1000; easing.type: Easing.InOutSine } + + ParallelAnimation { + NumberAnimation { target: hourglass1; property: "opacity"; from: 1; to: 0; duration: 1500; easing.type: Easing.InOutSine } + NumberAnimation { target: hourglass2; property: "opacity"; from: 0; to: 1; duration: 1500; easing.type: Easing.InOutSine } + } + + // reset + NumberAnimation { target: hourglass1; property: "opacity"; to: 1; duration: 0 } + NumberAnimation { target: hourglass2; property: "opacity"; to: 0; duration: 0 } + } +} diff --git a/examples/meshtastic/qml6/ext/common/MainIcon.qml b/examples/meshtastic/qml6/ext/common/MainIcon.qml new file mode 100644 index 0000000..28d685c --- /dev/null +++ b/examples/meshtastic/qml6/ext/common/MainIcon.qml @@ -0,0 +1,14 @@ +import QtQuick + +Image { + horizontalAlignment: Image.AlignHCenter + verticalAlignment: Image.AlignVCenter + width: header.height + height: width + + MouseArea { + anchors.fill: parent + onClicked: swipeView.currentIndex = parent.Positioner.index + onPressAndHold: Lisp.call("app:icon-press-and-hold", parent.objectName) + } +} diff --git a/examples/meshtastic/qml6/ext/common/MainView.qml b/examples/meshtastic/qml6/ext/common/MainView.qml new file mode 100644 index 0000000..77015a9 --- /dev/null +++ b/examples/meshtastic/qml6/ext/common/MainView.qml @@ -0,0 +1,87 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic +import "." as Ext +import "../group/" as Grp +import "../messages/" as Msg +import "../radios/" as Rad + +Item { + anchors.fill: parent + + property alias currentIndex: swipeView.currentIndex + + Rectangle { + id: header + width: parent.width + height: rootItem.headerHeight + color: "#f2f2f2" + + Row { + height: parent.height + spacing: 5 + anchors.horizontalCenter: parent.horizontalCenter + + Ext.MainIcon { + objectName: "group_icon" + source: "../../img/group.png" + + Rectangle { + objectName: "unread_messages" + width: 10 + height: width + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: 7 + radius: width / 2 + color: "#ff4040" + visible: false + } + } + + Ext.MainIcon { + objectName: "message_icon" + source: "../../img/message.png" + } + + Ext.MainIcon { + objectName: "radio_icon" + source: "../../img/radio.png" + } + } + } + + SwipeView { + id: swipeView + objectName: "main_view" + y: header.height + width: parent.width + height: parent.height - header.height + currentIndex: 1 + interactive: false + + Grp.Group { id: group } + Msg.Messages {} + Rad.Radios {} + + onCurrentIndexChanged: Lisp.call("app:view-index-changed", currentIndex) + } + + PageIndicator { + id: control + y: header.height - 12 + count: swipeView.count + currentIndex: swipeView.currentIndex + anchors.horizontalCenter: parent.horizontalCenter + + delegate: Rectangle { + width: header.height + height: 5 + radius: width / 2 + color: "dodgerblue" + opacity: (index === control.currentIndex) ? 1 : 0 + + Behavior on opacity { OpacityAnimator { duration: 500 }} + } + } +} diff --git a/examples/meshtastic/qml6/ext/common/Menu.qml b/examples/meshtastic/qml6/ext/common/Menu.qml new file mode 100644 index 0000000..92bad03 --- /dev/null +++ b/examples/meshtastic/qml6/ext/common/Menu.qml @@ -0,0 +1,10 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic + +Menu { + width: 250 + font.family: fontText.name + font.pixelSize: 18 +} + diff --git a/examples/meshtastic/qml6/ext/common/MenuItem.qml b/examples/meshtastic/qml6/ext/common/MenuItem.qml new file mode 100644 index 0000000..b8965f6 --- /dev/null +++ b/examples/meshtastic/qml6/ext/common/MenuItem.qml @@ -0,0 +1,9 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic + +MenuItem { + font.family: fontText.name + font.pixelSize: 18 +} + diff --git a/examples/meshtastic/qml6/ext/common/Toast.qml b/examples/meshtastic/qml6/ext/common/Toast.qml new file mode 100644 index 0000000..c08ce4d --- /dev/null +++ b/examples/meshtastic/qml6/ext/common/Toast.qml @@ -0,0 +1,67 @@ +import QtQuick + +Rectangle { + id: toast + objectName: "toast" + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + z: 99 + width: msg.contentWidth + 70 + height: msg.contentHeight + 30 + color: "#303030" + border.width: 2 + border.color: "white" + radius: Math.min(25, height / 2) + opacity: 0 + visible: false + + function message(text, seconds) { // called from Lisp + pause.duration = 1000 * ((seconds === 0) ? (24 * 60 * 60) : seconds) + toast.visible = true + msg.text = text + anim.start() + } + + Text { + id: msg + font.pixelSize: 16 + font.bold: true + anchors.centerIn: parent + color: "white" + wrapMode: Text.WordWrap + width: toast.parent.width - 2 * toast.radius - 10 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + MouseArea { + anchors.fill: parent + onClicked: toast.visible = false + } + } + + SequentialAnimation { + id: anim + onFinished: { toast.visible = false } + + OpacityAnimator { + from: 0 + to: 0.8 + target: toast + easing.type: Easing.InOutQuart + duration: 500 + } + + PauseAnimation { + id: pause + duration: 3000 + } + + OpacityAnimator { + from: 0.8 + to: 0 + target: toast + easing.type: Easing.InOutQuart + duration: 1500 + } + } +} diff --git a/examples/meshtastic/qml6/ext/dialogs/Confirm.qml b/examples/meshtastic/qml6/ext/dialogs/Confirm.qml new file mode 100644 index 0000000..44b34ef --- /dev/null +++ b/examples/meshtastic/qml6/ext/dialogs/Confirm.qml @@ -0,0 +1,25 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +Dialog { + anchors.centerIn: parent + standardButtons: Dialog.Ok | Dialog.Cancel + + property alias text: message.text + property string callback + + Column { + width: parent.width + spacing: 5 + + Text { + id: message + width: parent.width + wrapMode: Text.Wrap + } + } + + onAccepted: Lisp.call(callback, true) + onRejected: Lisp.call(callback, false) +} diff --git a/examples/meshtastic/qml6/ext/dialogs/ConfirmMobile.qml b/examples/meshtastic/qml6/ext/dialogs/ConfirmMobile.qml new file mode 100644 index 0000000..2710f7c --- /dev/null +++ b/examples/meshtastic/qml6/ext/dialogs/ConfirmMobile.qml @@ -0,0 +1,28 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic + +Dialog { + anchors.centerIn: parent + font.pixelSize: 18 + modal: true + standardButtons: Dialog.Ok | Dialog.Cancel + + property alias text: message.text + property string callback + + Column { + width: parent.width + spacing: 5 + + Text { + id: message + width: parent.width + wrapMode: Text.Wrap + font.pixelSize: 18 + } + } + + onAccepted: Lisp.call(callback, true) + onRejected: Lisp.call(callback, false) +} diff --git a/examples/meshtastic/qml6/ext/dialogs/Dialogs.qml b/examples/meshtastic/qml6/ext/dialogs/Dialogs.qml new file mode 100644 index 0000000..adf4d8d --- /dev/null +++ b/examples/meshtastic/qml6/ext/dialogs/Dialogs.qml @@ -0,0 +1,61 @@ +import QtQuick + +Item { + id: dialogs + objectName: "dialogs" + anchors.fill: parent + + Loader { + id: loader + anchors.centerIn: parent + } + + function message(text) { + loader.active = false // force reload + if (rootItem.mobile) { + loader.source = "MessageMobile.qml" + } else { + loader.source = "Message.qml" + } + loader.active = true + loader.item.text = text + rootItem.showKeyboard(false) + loader.item.open() + } + + function confirm(text, callback) { + loader.active = false // force reload + if (rootItem.mobile) { + loader.source = "ConfirmMobile.qml" + } else { + loader.source = "Confirm.qml" + } + loader.active = true + loader.item.text = text + loader.item.callback = callback + rootItem.showKeyboard(false) + loader.item.open() + } + + function input(title, label, callback, text, maxLength, from, to, value) { + loader.active = false // force reload + if (rootItem.mobile) { + loader.source = "InputMobile.qml" + } else { + loader.source = "Input.qml" + } + loader.active = true + loader.item.title = title + loader.item.label = label + loader.item.callback = callback + loader.item.text = text + loader.item.maxLength = maxLength + loader.item.from = from + loader.item.to = to + loader.item.value = value + var keyboard = (text !== "") + rootItem.showKeyboard(keyboard) + loader.item.open() + if (keyboard) loader.item.focus() + } +} diff --git a/examples/meshtastic/qml6/ext/dialogs/Input.qml b/examples/meshtastic/qml6/ext/dialogs/Input.qml new file mode 100644 index 0000000..3c8833a --- /dev/null +++ b/examples/meshtastic/qml6/ext/dialogs/Input.qml @@ -0,0 +1,47 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +Dialog { + anchors.centerIn: parent + standardButtons: Dialog.Ok | Dialog.Cancel + + property alias label: label.text + property alias text: edit.text + property alias maxLength: edit.maximumLength + property alias from: spinBox.from + property alias to: spinBox.to + property alias value: spinBox.value + property string callback + + function focus() { edit.forceActiveFocus() } + + Column { + width: parent.width + spacing: 5 + + Text { + id: label + width: parent.width + wrapMode: Text.Wrap + visible: (text !== "") + } + + TextField { + id: edit + objectName: "dialog_line_edit" + width: parent.width + visible: !spinBox.visible + } + + SpinBox { + id: spinBox + objectName: "dialog_spin_box" + anchors.horizontalCenter: parent.horizontalCenter + visible: !!value + } + } + + onAccepted: Lisp.call(callback, true) + onRejected: Lisp.call(callback, false) +} diff --git a/examples/meshtastic/qml6/ext/dialogs/InputMobile.qml b/examples/meshtastic/qml6/ext/dialogs/InputMobile.qml new file mode 100644 index 0000000..4b19fc0 --- /dev/null +++ b/examples/meshtastic/qml6/ext/dialogs/InputMobile.qml @@ -0,0 +1,50 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic + +Dialog { + anchors.centerIn: parent + font.pixelSize: 18 + modal: true + standardButtons: Dialog.Ok | Dialog.Cancel + + property alias label: label.text + property alias text: edit.text + property alias maxLength: edit.maximumLength + property alias from: spinBox.from + property alias to: spinBox.to + property alias value: spinBox.value + property string callback + + function focus() { edit.forceActiveFocus() } + + Column { + width: parent.width + spacing: 5 + + Text { + id: label + width: parent.width + wrapMode: Text.Wrap + font.pixelSize: 18 + visible: (text !== "") + } + + TextField { + id: edit + objectName: "dialog_line_edit" + width: parent.width + visible: !spinBox.visible + } + + SpinBox { + id: spinBox + objectName: "dialog_spin_box" + anchors.horizontalCenter: parent.horizontalCenter + visible: !!value + } + } + + onAccepted: Lisp.call(callback, true) + onRejected: Lisp.call(callback, false) +} diff --git a/examples/meshtastic/qml6/ext/dialogs/Message.qml b/examples/meshtastic/qml6/ext/dialogs/Message.qml new file mode 100644 index 0000000..7bb7115 --- /dev/null +++ b/examples/meshtastic/qml6/ext/dialogs/Message.qml @@ -0,0 +1,16 @@ +import QtQuick +import QtQuick.Dialogs + +Dialog { + anchors.centerIn: parent + title: qsTr("Info") + standardButtons: Dialog.Ok + + property alias text: message.text + + Text { + id: message + width: parent.width + wrapMode: Text.Wrap + } +} diff --git a/examples/meshtastic/qml6/ext/dialogs/MessageMobile.qml b/examples/meshtastic/qml6/ext/dialogs/MessageMobile.qml new file mode 100644 index 0000000..41af84e --- /dev/null +++ b/examples/meshtastic/qml6/ext/dialogs/MessageMobile.qml @@ -0,0 +1,20 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic + +Dialog { + anchors.centerIn: parent + title: qsTr("Info") + font.pixelSize: 18 + modal: true + standardButtons: Dialog.Ok + + property alias text: message.text + + Text { + id: message + width: parent.width + wrapMode: Text.Wrap + font.pixelSize: 18 + } +} diff --git a/examples/meshtastic/qml6/ext/group/Group.qml b/examples/meshtastic/qml6/ext/group/Group.qml new file mode 100644 index 0000000..a842d47 --- /dev/null +++ b/examples/meshtastic/qml6/ext/group/Group.qml @@ -0,0 +1,274 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic +import "." as Grp +import "../common/" as Com + +Rectangle { + id: rect + color: "#ccebc5" + + Row { + id: rowModem + padding: 9 + spacing: 9 + + Com.ComboBox { + id: modem + objectName: "modem" + width: 160 + font.pixelSize: 16 + font.family: fontText.name + + onActivated: Lisp.call("lora:change-modem-preset", currentText) + } + + Text { + height: modem.height + font.pixelSize: 16 + font.family: fontText.name + verticalAlignment: Text.AlignVCenter + text: qsTr("modem preset") + } + } + + ListView { + id: view + objectName: "group_view" + anchors.topMargin: rowModem.height + anchors.bottomMargin: channel.height + anchors.fill: parent + anchors.margins: 9 + spacing: 9 + clip: true + delegate: groupDelegate + model: group + currentIndex: -1 + } + + Rectangle { + id: channel + anchors.bottom: parent.bottom + width: parent.width + height: 28 + color: "#555" + + Text { + objectName: "channel_name" + anchors.centerIn: parent + font.pixelSize: 16 + font.family: fontText.name + font.weight: Font.DemiBold + color: rect.color + text: "cl-app" + } + } + + ListModel { + id: group + objectName: "group" + + // hack to define all model key _types_ + ListElement { + radioName: ""; customName: ""; nodeNum: ""; unread: 0; current: false + } + + function addPerson(person) { + // insert sorted + var i = 1; // 0 is broadcast + var broadcast = (count === 0) + for (; i < count; i++) { + if (person.customName < get(i).customName) { + insert(i, person) + break + } + } + if (broadcast || (i === count)) { + append(person) + } + + if (person.current) { + view.currentIndex = broadcast ? 0 : i + view.positionViewAtIndex(view.currentIndex, ListView.Contain) + rootItem.broadcast = broadcast + } + } + + function sortRenamed(name, index) { + var to = -1 + if (name < get(1).customName) { // 0 is broadcast + to = 1 + } else if (name >= get(count - 1).customName) { + to = count - 1 + } else { + for (var i = 1; i < count; i++) { + if ((i !== index) && (name < get(i).customName)) { + to = (index > i) ? i : i - 1 + break + } + } + } + if (to !== -1) { + move(index, to, 1) + view.currentIndex = to + view.positionViewAtIndex(to, ListView.Contain) + } + } + + function radioNames() { + var names = [] + for (var i = 0; i < count; i++) { + names.push(get(i).radioName) + } + return names + } + + function setUnread(name, n) { + for (var i = 0; i < count; i++) { + if (get(i).radioName === name) { + setProperty(i, "unread", n) + break + } + } + } + + Component.onCompleted: remove(0) // see hack above + } + + Component { + id: groupDelegate + + Rectangle { + id: delegate + width: Math.min(265, view.width) + height: 35 + color: (index === view.currentIndex) ? "firebrick" : "darkcyan" + radius: height / 2 + + Rectangle { + id: rectRadio + x: (index === 0) ? 18 : 10 + width: (index === 0) ? 28 : 42 + height: (index === 0) ? width : 15 + anchors.verticalCenter: parent.verticalCenter + color: "#f0f0f0" + radius: height / 2 + + Image { + anchors.centerIn: parent + width: 20 + height: width + source: "../../img/broadcast.png" + visible: (index === 0) + } + + Text { + anchors.centerIn: parent + font.pixelSize: 12 + font.family: fontText.name + font.weight: Font.DemiBold + color: "black" + text: model.radioName + visible: (index !== 0) + } + } + + function selected() { + view.currentIndex = index + Lisp.call("lora:change-receiver", model.nodeNum) + rootItem.broadcast = (index === 0) + } + + MouseArea { + id: mouseArea + anchors.fill: parent + + onClicked: selected() + } + + // custom name + + TextField { + id: name + x: 58 + anchors.verticalCenter: parent.verticalCenter + leftPadding: 2 + font.pixelSize: 18 + font.family: fontText.name + font.weight: Font.DemiBold + color: readOnly ? "white" : "#505050" + palette.highlight: "darkcyan" + palette.highlightedText: "white" + text: (model.customName === "") ? qsTr("Anonym") : model.customName + readOnly: true + + background: Rectangle { + y: 4 + width: delegate.width - 1.5 * delegate.height - rectRadio.width + height: delegate.height - 12 + color: name.readOnly ? "transparent" : "#f0f0f0" + border.width: 0 + } + + onPressAndHold: { + if (index !== 0) { + readOnly = false + selectAll() + forceActiveFocus() + Qt.inputMethod.show() // needed for SailfishOS + } + } + + onEditingFinished: { + if (!readOnly) { + readOnly = true + group.setProperty(index, "customName", text) + Lisp.call("group:name-edited", model.radioName, text) + if (text === "") text = qsTr("Anonym") + Qt.callLater(group.sortRenamed, text, index) // 'Qt.callLater': prevent UI thread related crash + } + } + + onReleased: if (readOnly) selected() + } + + // unread messages + + Rectangle { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.rightMargin: 8 + width: 22 + height: width + radius: width / 2 + color: "#ff4040" + visible: (model.unread > 0) + + Text { + anchors.fill: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pixelSize: 12 + font.weight: Font.DemiBold + font.family: fontText.name + text: model.unread + color: "white" + } + } + } + } + + Grp.Map { + objectName: "map_view" + anchors.fill: rect + visible: false + } + + Timer { + interval: 15 * 60 * 1000 // 15 min + repeat: true + running: true + onTriggered: Lisp.call("lora:get-node-config") + } +} + diff --git a/examples/meshtastic/qml6/ext/group/Map.qml b/examples/meshtastic/qml6/ext/group/Map.qml new file mode 100644 index 0000000..d97b453 --- /dev/null +++ b/examples/meshtastic/qml6/ext/group/Map.qml @@ -0,0 +1,149 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic +import QtLocation +import QtPositioning +import "." as Ext + +Item { + anchors.fill: parent + + Component { + id: mapComponent + + Map { + id: map + objectName: "map" + anchors.fill: parent + plugin: mapPlugin + zoomLevel: 14 + + property bool manualLocation: false + property var myMarker + + Ext.MapButton { + id: hand + objectName: "add_manual_marker" + anchors.top: parent.top + icon.source: "../../img/hand.png" + visible: false + + onClicked: { + manualLocation = !manualLocation + if (manualLocation) { + if (markers.count === 0) { + Lisp.call("loc:add-manual-marker") + } + myMarker = markers.itemAt(0) + } else { + myMarker = null + } + } + } + + Ext.MapButton { + objectName: "remove_marker" + anchors.top: hand.bottom + icon.source: "../../img/remove-marker.png" + + onClicked: { + markers.itemAt(0).visible = false + Lisp.call("loc:remove-marker") + visible = false + } + } + + SequentialAnimation { + id: markerAnimation + loops: Animation.Infinite + running: manualLocation && !!myMarker + + OpacityAnimator { target: myMarker; from: 1.0; to: 0.2; duration: 500; easing.type: Easing.InOutSine } + OpacityAnimator { target: myMarker; from: 0.2; to: 1.0; duration: 500; easing.type: Easing.InOutSine } + } + + MouseArea { + anchors.fill: parent + onClicked: { + manualLocation = false + var coord = map.toCoordinate(Qt.point(mouse.x, mouse.y)) + myMarker.coordinate = coord + myMarker.opacity = 1 + Lisp.call("loc:position-selected", coord.latitude, coord.longitude) + } + } + + function coordinate(pos) { + return QtPositioning.coordinate(pos[0], pos[1]) + } + + function setCenter(pos) { + center = coordinate(pos) + } + + function showMarker(n, nodeNum, name, customName = "") { + var pos = Lisp.call("loc:position*", nodeNum) + if (pos) { + var marker = markers.itemAt(n) + marker.radioName = name + marker.customName = customName + marker.coordinate = coordinate(pos) + marker.visible = true + } + } + + function updatePositions(myNum, myName, group) { + var n = 0 + showMarker(n++, myNum, myName) + for (var i = 0; i < group.count; i++) { + var data = group.get(i) + showMarker(n++, data.nodeNum, data.radioName, data.customName) + } + } + + Plugin { + id: mapPlugin + name: "osm" // Open Street Map + + // for downloading tiles + PluginParameter { + name: "osm.mapping.cache.directory" + value: Lisp.call("loc:tile-path") + } + // for offline tiles (from cache) + PluginParameter { + name: "osm.mapping.offline.directory" + value: Lisp.call("loc:tile-path") + } + // number tiles (instead of MB) + PluginParameter { + name: "osm.mapping.cache.disk.cost_strategy" + value: "unitary" + } + // max number cached/offline tiles + PluginParameter { + name: "osm.mapping.cache.disk.size" + value: 10000 + } + // local tile provider (no API key needed) + PluginParameter { + name: "osm.mapping.providersrepository.address" + value: Lisp.call("loc:tile-provider-path") + } + } + + Ext.Markers { + id: markers + objectName: "markers" + } + } + } + + Loader { + id: mapLoader + objectName: "map_loader" + anchors.fill: parent + sourceComponent: mapComponent + active: false + } +} diff --git a/examples/meshtastic/qml6/ext/group/MapButton.qml b/examples/meshtastic/qml6/ext/group/MapButton.qml new file mode 100644 index 0000000..d654d03 --- /dev/null +++ b/examples/meshtastic/qml6/ext/group/MapButton.qml @@ -0,0 +1,15 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic + +RoundButton { + z: 1 + anchors.right: parent.right + anchors.margins: 5 + icon.color: "#eee" + icon.width: 28 + width: 38 + height: width + radius: width / 2 + palette.button: "#555" +} diff --git a/examples/meshtastic/qml6/ext/group/Markers.qml b/examples/meshtastic/qml6/ext/group/Markers.qml new file mode 100644 index 0000000..17a2849 --- /dev/null +++ b/examples/meshtastic/qml6/ext/group/Markers.qml @@ -0,0 +1,61 @@ +import QtQuick +import QtLocation + +Repeater { + model: Lisp.call("loc:position-count") + + MapQuickItem { + anchorPoint.x: image.width / 2 + anchorPoint.y: image.height + visible: false + + property alias radioName: radioName.text + property alias customName: customName.text + + sourceItem: Image { + id: image + width: 25 + height: width + source: "../../img/marker.png" + + Rectangle { + x: -(width - image.width) / 2 + y: image.height + 5 + width: customName.width + 42 + height: 20 + color: (index === 0) ? "#ff3d00" : "darkcyan" + radius: height / 2 + + Rectangle { + x: 2 + anchors.verticalCenter: parent.verticalCenter + width: 38 + height: 16 + color: "#f0f0f0" + radius: height / 2 + + Text { + id: radioName + anchors.centerIn: parent + font.pixelSize: 12 + font.family: fontText.name + font.weight: Font.DemiBold + color: "black" + } + } + + Text { + id: customName + x: 44 + anchors.verticalCenter: parent.verticalCenter + width: paintedWidth ? (paintedWidth + 10) : 0 + font.pixelSize: 14 + font.family: fontText.name + font.weight: Font.DemiBold + color: "white" + } + } + } + } +} + diff --git a/examples/meshtastic/qml6/ext/messages/EmojiView.qml b/examples/meshtastic/qml6/ext/messages/EmojiView.qml new file mode 100644 index 0000000..918e0b2 --- /dev/null +++ b/examples/meshtastic/qml6/ext/messages/EmojiView.qml @@ -0,0 +1,40 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic + +ScrollView { + width: parent.width + ScrollBar.vertical.policy: ScrollBar.AlwaysOn + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + + property alias model: grid.model + + GridView { + id: grid + anchors.fill: parent + cellWidth: emojis.itemSize + cellHeight: emojis.itemSize + leftMargin: 2 + topMargin: 2 + clip: true + highlightFollowsCurrentItem: false + focus: true + + delegate: Text { + width: emojis.itemSize + height: emojis.itemSize + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pixelSize: emojis.itemSize - 4 + text: modelData + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + + onClicked: Lisp.call("app:emoji-clicked", + grid.itemAtIndex(grid.indexAt(mouse.x, mouse.y + grid.contentY)).text) + } + } +} diff --git a/examples/meshtastic/qml6/ext/messages/Emojis.qml b/examples/meshtastic/qml6/ext/messages/Emojis.qml new file mode 100644 index 0000000..d44fc36 --- /dev/null +++ b/examples/meshtastic/qml6/ext/messages/Emojis.qml @@ -0,0 +1,42 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic +import "." as Msg + +Rectangle { + id: emojis + objectName: "emojis" + height: 7 * itemSize + 2 * column.spacing + 8 + radius: 12 + color: "white" + border.width: 1 + border.color: "gray" + + property int itemSize: 42 + + Column { + id: column + anchors.fill: parent + spacing: 5 + + Msg.EmojiView { + objectName: "recent_emojis" + height: itemSize + 1 + ScrollBar.vertical.policy: ScrollBar.AlwaysOff + + model: ["๐Ÿ™‚","๐Ÿคฃ","๐Ÿ‘"] + } + + Msg.EmojiView { + height: itemSize * 3 + 1 + + model: ["๐Ÿ˜ƒ","๐Ÿ˜„","๐Ÿ˜","๐Ÿ˜†","๐Ÿ˜…","๐Ÿ˜‚","๐Ÿคฃ","๐Ÿฅฒ","๐Ÿฅน","๐Ÿ˜Š","๐Ÿ˜‡","๐Ÿ™‚","๐Ÿ™ƒ","๐Ÿ˜‰","๐Ÿ˜Œ","๐Ÿ˜","๐Ÿฅฐ","๐Ÿ˜˜","๐Ÿ˜—","๐Ÿ˜™","๐Ÿ˜š","๐Ÿ˜‹","๐Ÿ˜›","๐Ÿ˜","๐Ÿ˜œ","๐Ÿคช","๐Ÿคจ","๐Ÿง","๐Ÿค“","๐Ÿ˜Ž","๐Ÿฅธ","๐Ÿคฉ","๐Ÿฅณ","๐Ÿ˜","๐Ÿ˜’","๐Ÿ˜ž","๐Ÿ˜”","๐Ÿ˜Ÿ","๐Ÿ˜•","๐Ÿ™","๐Ÿ˜ฃ","๐Ÿ˜–","๐Ÿ˜ซ","๐Ÿ˜ฉ","๐Ÿฅบ","๐Ÿ˜ข","๐Ÿ˜ญ","๐Ÿ˜ฎโ€๐Ÿ’จ","๐Ÿ˜ค","๐Ÿ˜ ","๐Ÿ˜ก","๐Ÿคฌ","๐Ÿคฏ","๐Ÿ˜ณ","๐Ÿฅต","๐Ÿฅถ","๐Ÿ˜ฑ","๐Ÿ˜จ","๐Ÿ˜ฐ","๐Ÿ˜ฅ","๐Ÿ˜“","๐Ÿซฃ","๐Ÿค—","๐Ÿซก","๐Ÿค”","๐Ÿซข","๐Ÿคญ","๐Ÿคซ","๐Ÿคฅ","๐Ÿ˜ถ","๐Ÿ˜ถโ€๐ŸŒซ๏ธ","๐Ÿ˜","๐Ÿ˜‘","๐Ÿ˜ฌ","๐Ÿซจ","๐Ÿซ ","๐Ÿ™„","๐Ÿ˜ฏ","๐Ÿ˜ฆ","๐Ÿ˜ง","๐Ÿ˜ฎ","๐Ÿ˜ฒ","๐Ÿฅฑ","๐Ÿ˜ด","๐Ÿคค","๐Ÿ˜ช","๐Ÿ˜ต","๐Ÿ˜ตโ€๐Ÿ’ซ","๐Ÿซฅ","๐Ÿค","๐Ÿฅด","๐Ÿคข","๐Ÿคฎ","๐Ÿคง","๐Ÿ˜ท","๐Ÿค’","๐Ÿค•","๐Ÿค‘","๐Ÿค ","๐Ÿ˜ˆ","๐Ÿ‘ฟ","๐Ÿ‘น","๐Ÿ‘บ","๐Ÿคก","๐Ÿ’ฉ","๐Ÿ‘ป","๐Ÿ’€","๐Ÿ‘ฝ","๐Ÿ‘พ","๐Ÿค–","๐ŸŽƒ","๐Ÿ˜บ","๐Ÿ˜ธ","๐Ÿ˜น","๐Ÿ˜ป","๐Ÿ˜ผ","๐Ÿ˜ฝ","๐Ÿ™€","๐Ÿ˜ฟ","๐Ÿ˜พ"] + } + + Msg.EmojiView { + height: itemSize * 3 + 1 + + model: ["๐Ÿ‘‹","๐Ÿคš","๐Ÿ–","โœ‹","๐Ÿ––","๐Ÿ‘Œ","๐ŸคŒ","๐Ÿค","๐Ÿคž","๐Ÿซฐ","๐ŸคŸ","๐Ÿค˜","๐Ÿค™","๐Ÿซต","๐Ÿซฑ","๐Ÿซฒ","๐Ÿซธ","๐Ÿซท","๐Ÿซณ","๐Ÿซด","๐Ÿ‘ˆ","๐Ÿ‘‰","๐Ÿ‘†","๐Ÿ–•","๐Ÿ‘‡","๐Ÿ‘","๐Ÿ‘Ž","โœŠ","๐Ÿ‘Š","๐Ÿค›","๐Ÿคœ","๐Ÿ‘","๐Ÿซถ","๐Ÿ™Œ","๐Ÿ‘","๐Ÿคฒ","๐Ÿค","๐Ÿ™","๐Ÿ’…","๐Ÿคณ","๐Ÿ’ช","๐Ÿฆพ","๐Ÿฆต","๐Ÿฆฟ","๐Ÿฆถ","๐Ÿ‘ฃ","๐Ÿ‘‚","๐Ÿฆป","๐Ÿ‘ƒ","๐Ÿซ€","๐Ÿซ","๐Ÿง ","๐Ÿฆท","๐Ÿฆด","๐Ÿ‘€","๐Ÿ‘","๐Ÿ‘…","๐Ÿ‘„","๐Ÿซฆ","๐Ÿ’‹","๐Ÿฉธ"] + } + } +} diff --git a/examples/meshtastic/qml6/ext/messages/Messages.qml b/examples/meshtastic/qml6/ext/messages/Messages.qml new file mode 100644 index 0000000..9f25f3d --- /dev/null +++ b/examples/meshtastic/qml6/ext/messages/Messages.qml @@ -0,0 +1,331 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic +import "." as Msg + +Rectangle { + id: main + color: hourglass.visible ? "#d2eecc" : "#e5d8bd" + + ListView { + id: view + objectName: "message_view" + anchors.topMargin: rectFind.height + 4 + anchors.fill: parent + anchors.bottomMargin: rectEdit.height + 3 + anchors.margins: 5 + model: messages + clip: true + visible: false + + property int fontSize: 18 + + delegate: SwipeDelegate { + id: swipeDelegate + width: view.width + height: delegate.height + clip: true + + onPressAndHold: Lisp.call("msg:message-press-and-hold", model.text) + onDoubleClicked: Lisp.call("msg:swipe-to-left") + + background: Item { + id: delegate + width: Math.max(text.paintedWidth, rowSender.width + view.fontSize / 4 * text.padding) + + 2 * text.padding + view.fontSize / 4 + height: model.hidden ? 0 : (text.contentHeight + 2 * text.padding + sender.contentHeight + 8) + + Rectangle { + anchors.centerIn: parent + width: parent.width + height: parent.height - 4 + color: model.me ? "#f2f2f2" : "#ffffcc" + radius: 12 + + Row { + id: rowSender + padding: text.padding + spacing: padding - 2 + + AnimatedImage { + id: semaphore + anchors.verticalCenter: sender.verticalCenter + anchors.verticalCenterOffset: -0.5 + width: view.fontSize / 2 - 1 + height: width + playing: false + source: "../../img/semaphore.gif" + currentFrame: model.ackState ? parseInt(model.ackState.substr(2), 16) : 0 // see 'qml:hex' + visible: model.me + } + + Text { + id: sender + font.pixelSize: 2/3 * view.fontSize + font.family: fontText.name + color: "#8B0000" + text: model.senderName ? model.senderName : model.sender + } + } + + Text { + id: timestamp + x: delegate.width - contentWidth - text.padding + y: text.padding + font.pixelSize: 2/3 * view.fontSize + font.family: fontText.name + color: "#505050" + text: model.hour + + MouseArea { + anchors.fill: parent + onClicked: Lisp.call("msg:show-date", model.timestamp) + } + } + + Text { + id: text + y: sender.contentHeight + width: main.width - 10 + padding: 5 + wrapMode: Text.Wrap + font.pixelSize: view.fontSize + font.family: fontText.name + color: "#303030" + textFormat: Text.StyledText // for 'paintedWidth' to always work + text: model.text + } + } + } + + ListView.onRemove: SequentialAnimation { + PropertyAction { + target: swipeDelegate + property: "ListView.delayRemove" + value: true + } + NumberAnimation { + target: swipeDelegate + property: "height" + to: 0 + easing.type: Easing.InOutQuad + } + PropertyAction { + target: swipeDelegate + property: "ListView.delayRemove" + value: false + } + } + + swipe.left: Rectangle { + y: 2 + width: 35 + height: parent.height - 2 * y + color: "#dd4141" + radius: 12 + + Image { + anchors.centerIn: parent + width: 12 + height: width + source: "../../img/delete.png" + } + + MouseArea { + anchors.fill: parent + + onClicked: { + var mid = model.mid + view.model.remove(index) + Lisp.call("db:delete-message", mid) + } + } + } + } + } + + ListModel { + id: messages + objectName: "messages" + + // hack to define all model key _types_ + ListElement { + receiver: ""; sender: ""; senderName: ""; timestamp: ""; hour: ""; + text: ""; text2: ""; mid: ""; ackState: ""; me: true; hidden: false + } + + function addMessage(message) { append(message) } + + function changeState(state, mid) { + for (var i = count - 1; i >= 0; i--) { + if (get(i).mid === mid) { + setProperty(i, "ackState", state) + break + } + } + } + + function find(term) { + for (var i = 0; i < count; i++) { + var text = get(i).text + var highlighted = Lisp.call("msg:highlight-term", text, term) + if (highlighted) { + if (!get(i).text2) { + setProperty(i, "text2", text) + } + setProperty(i, "text", highlighted) + } + setProperty(i, "hidden", !highlighted) + } + view.positionViewAtBeginning() + } + + function clearFind() { + for (var i = 0; i < count; i++) { + var text2 = get(i).text2 + if (text2) { + setProperty(i, "text", text2) + setProperty(i, "text2", "") + } + setProperty(i, "hidden", false) + } + } + + Component.onCompleted: remove(0) // see hack above + } + + // find text + + TextField { + id: findText + objectName: "find_text" + y: 1 + width: parent.width + height: visible ? (edit.paintedHeight + 14) : 0 + font.pixelSize: view.fontSize + font.family: fontText.name + selectionColor: "#228ae3" + selectedTextColor: "white" + placeholderText: qsTr("search") + visible: false + + background: Rectangle { + id: rectFind + color: "white" + border.width: 3 + border.color: findText.focus ? "dodgerblue" : "#c0c0c0" + radius: 12 + } + + onEditingFinished: Lisp.call("msg:find-text", text) + } + + // send text + + Rectangle { + id: rectEdit + anchors.bottom: parent.bottom + anchors.bottomMargin: 1 + width: parent.width + height: edit.paintedHeight + 14 + color: "white" + border.width: 3 + border.color: edit.focus ? (edit.tooLong ? "#ff5f57" : "dodgerblue") : "#c0c0c0" + radius: 12 + + TextArea { + id: edit + objectName: "edit" + anchors.fill: parent + textFormat: TextEdit.PlainText + font.pixelSize: view.fontSize + font.family: fontText.name + selectionColor: "#228ae3" + selectedTextColor: "white" + wrapMode: TextEdit.Wrap + textMargin: 0 + placeholderText: qsTr("message") + + property bool tooLong: false + + onLengthChanged: if (length > 150) Lisp.call("msg:check-utf8-length", text) + Keys.onEscapePressed: emojis.visible = false + + Image { + y: 8 + anchors.right: parent.right + anchors.rightMargin: 7 + width: edit.font.pixelSize + 1 + height: width + source: "../../img/emoji.png" + opacity: 0.55 + visible: edit.focus && (Qt.platform.os !== "android") && (Qt.platform.os !== "ios") + + MouseArea { + anchors.fill: parent + onClicked: emojis.visible = !emojis.visible + } + } + } + + Image { + id: send + anchors.right: parent.right + anchors.bottom: parent.top + anchors.margins: 3 + width: 38 + height: width + source: "../../img/send.png" + visible: edit.focus && !edit.tooLong + + MouseArea { + anchors.fill: parent + onClicked: { + edit.focus = Qt.NoFocus + Lisp.call("lora:send-message", edit.text) + edit.clear() + } + } + } + + Image { + id: broadcast + anchors.right: send.left + anchors.bottom: parent.top + anchors.margins: 3 + width: 38 + height: width + opacity: 0.7 + source: "../../img/broadcast.png" + visible: send.visible && animation.running + + SequentialAnimation { + id: animation + loops: Animation.Infinite + running: rootItem.broadcast + + ScaleAnimator { + target: broadcast + from: 0.8; to: 1.0 + duration: 500 + easing.type: Easing.InOutSine + } + + ScaleAnimator { + target: broadcast + from: 1.0; to: 0.8 + duration: 500 + easing.type: Easing.InOutSine + } + } + } + } + + Msg.Emojis { + id: emojis + anchors.bottom: rectEdit.top + anchors.bottomMargin: -1 + width: main.width + visible: false + } +} diff --git a/examples/meshtastic/qml6/ext/radios/BatteryLevel.qml b/examples/meshtastic/qml6/ext/radios/BatteryLevel.qml new file mode 100644 index 0000000..0055c37 --- /dev/null +++ b/examples/meshtastic/qml6/ext/radios/BatteryLevel.qml @@ -0,0 +1,34 @@ +import QtQuick + +Rectangle { + anchors.verticalCenter: parent.verticalCenter + width: 12 + height: 22 + color: (level > 15) ? "#f0f0f0" : "yellow" + radius: 2 + border.width: 1 + border.color: "#808080" + + property int level: 0 + + Rectangle { + x: 1 + width: parent.width - 2 + height: (parent.height - 2) * level / 100 + anchors.bottom: parent.bottom + anchors.bottomMargin: 1 + color: (level > 15) ? "#28c940" : "#ff5f57" + } + + Text { + x: -4 - paintedWidth + height: parent.height + verticalAlignment: Text.AlignVCenter + font.pixelSize: 12 + font.family: fontText.name + font.weight: Font.DemiBold + color: "white" + text: level + "%" + } +} + diff --git a/examples/meshtastic/qml6/ext/radios/Radios.qml b/examples/meshtastic/qml6/ext/radios/Radios.qml new file mode 100644 index 0000000..3f95eab --- /dev/null +++ b/examples/meshtastic/qml6/ext/radios/Radios.qml @@ -0,0 +1,128 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic +import "." as Rad +import "../common/" as Com + +Rectangle { + id: rect + color: "#b3cde3" + + Row { + id: rowRegion + padding: 9 + spacing: 9 + + Com.ComboBox { + id: region + objectName: "region" + width: 110 + font.pixelSize: 16 + font.family: fontText.name + + onActivated: Lisp.call("lora:change-region", currentIndex ? currentText : "") + } + + Text { + height: region.height + font.pixelSize: 16 + font.family: fontText.name + verticalAlignment: Text.AlignVCenter + text: qsTr("region") + } + } + + ListView { + id: view + anchors.topMargin: rowRegion.height + anchors.fill: parent + anchors.margins: 9 + spacing: 9 + clip: true + delegate: radioDelegate + model: radios + } + + ListModel { + id: radios + objectName: "radios" + + // hack to define all model key _types_ + ListElement { + name: ""; ini: false; hwModel: ""; batteryLevel: 0; current: false + } + + function addRadio(radio) { + // prevent multiple entries on device discovery problems + for (var i = 0; i < count; i++) { + if (get(i).name === radio.name) { + return + } + } + append(radio) + if (radio.current) { + view.currentIndex = view.count - 1 + } + } + + Component.onCompleted: remove(0) // see hack above + } + + Component { + id: radioDelegate + + Rectangle { + id: delegate + width: Math.min(265, view.width) + height: 35 + color: (index === view.currentIndex) ? "firebrick" : (model.ini ? "#808080" : "steelblue") + radius: height / 2 + + Rectangle { + x: 10 + width: 42 + height: 15 + anchors.verticalCenter: parent.verticalCenter + color: "#f0f0f0" + radius: height / 2 + + Text { + anchors.centerIn: parent + font.pixelSize: 12 + font.family: fontText.name + font.weight: Font.DemiBold + color: "black" + text: model.name + } + } + + Text { + x: 58 + anchors.verticalCenter: parent.verticalCenter + font.pixelSize: 18 + font.family: fontText.name + font.weight: Font.DemiBold + color: "white" + text: model.hwModel + } + + Rad.BatteryLevel { + anchors.right: parent.right + anchors.rightMargin: 14 + level: model.batteryLevel + visible: !model.ini + } + + MouseArea { + anchors.fill: parent + + onClicked: { + if (index > 0) { // current radio is 0 + view.currentIndex = index + Lisp.call("radios:change-radio", model.name) + } + } + } + } + } +} diff --git a/examples/meshtastic/qml6/fonts/Ubuntu-Medium.ttf b/examples/meshtastic/qml6/fonts/Ubuntu-Medium.ttf new file mode 100644 index 0000000..7340a40 Binary files /dev/null and b/examples/meshtastic/qml6/fonts/Ubuntu-Medium.ttf differ diff --git a/examples/meshtastic/qml6/fonts/Ubuntu.ttf b/examples/meshtastic/qml6/fonts/Ubuntu.ttf new file mode 100644 index 0000000..f98a2da Binary files /dev/null and b/examples/meshtastic/qml6/fonts/Ubuntu.ttf differ diff --git a/examples/meshtastic/qml6/img/broadcast.png b/examples/meshtastic/qml6/img/broadcast.png new file mode 100644 index 0000000..251b97f Binary files /dev/null and b/examples/meshtastic/qml6/img/broadcast.png differ diff --git a/examples/meshtastic/qml6/img/busy.gif b/examples/meshtastic/qml6/img/busy.gif new file mode 100644 index 0000000..4ed7b6f Binary files /dev/null and b/examples/meshtastic/qml6/img/busy.gif differ diff --git a/examples/meshtastic/qml6/img/delete.png b/examples/meshtastic/qml6/img/delete.png new file mode 100644 index 0000000..9bc0640 Binary files /dev/null and b/examples/meshtastic/qml6/img/delete.png differ diff --git a/examples/meshtastic/qml6/img/emoji.png b/examples/meshtastic/qml6/img/emoji.png new file mode 100644 index 0000000..41e765a Binary files /dev/null and b/examples/meshtastic/qml6/img/emoji.png differ diff --git a/examples/meshtastic/qml6/img/find.png b/examples/meshtastic/qml6/img/find.png new file mode 100644 index 0000000..3b77889 Binary files /dev/null and b/examples/meshtastic/qml6/img/find.png differ diff --git a/examples/meshtastic/qml6/img/group.png b/examples/meshtastic/qml6/img/group.png new file mode 100644 index 0000000..c9e66b3 Binary files /dev/null and b/examples/meshtastic/qml6/img/group.png differ diff --git a/examples/meshtastic/qml6/img/hand.png b/examples/meshtastic/qml6/img/hand.png new file mode 100644 index 0000000..c5c2d77 Binary files /dev/null and b/examples/meshtastic/qml6/img/hand.png differ diff --git a/examples/meshtastic/qml6/img/hourglass.png b/examples/meshtastic/qml6/img/hourglass.png new file mode 100644 index 0000000..36626a3 Binary files /dev/null and b/examples/meshtastic/qml6/img/hourglass.png differ diff --git a/examples/meshtastic/qml6/img/location.png b/examples/meshtastic/qml6/img/location.png new file mode 100644 index 0000000..2e12795 Binary files /dev/null and b/examples/meshtastic/qml6/img/location.png differ diff --git a/examples/meshtastic/qml6/img/logo.png b/examples/meshtastic/qml6/img/logo.png new file mode 100644 index 0000000..8cd1f56 Binary files /dev/null and b/examples/meshtastic/qml6/img/logo.png differ diff --git a/examples/meshtastic/qml6/img/marker.png b/examples/meshtastic/qml6/img/marker.png new file mode 100644 index 0000000..2dc79f2 Binary files /dev/null and b/examples/meshtastic/qml6/img/marker.png differ diff --git a/examples/meshtastic/qml6/img/message.png b/examples/meshtastic/qml6/img/message.png new file mode 100644 index 0000000..1b2d3d5 Binary files /dev/null and b/examples/meshtastic/qml6/img/message.png differ diff --git a/examples/meshtastic/qml6/img/radio.png b/examples/meshtastic/qml6/img/radio.png new file mode 100644 index 0000000..dcd5f50 Binary files /dev/null and b/examples/meshtastic/qml6/img/radio.png differ diff --git a/examples/meshtastic/qml6/img/remove-marker.png b/examples/meshtastic/qml6/img/remove-marker.png new file mode 100644 index 0000000..9aff828 Binary files /dev/null and b/examples/meshtastic/qml6/img/remove-marker.png differ diff --git a/examples/meshtastic/qml6/img/semaphore.gif b/examples/meshtastic/qml6/img/semaphore.gif new file mode 100644 index 0000000..72a63bf Binary files /dev/null and b/examples/meshtastic/qml6/img/semaphore.gif differ diff --git a/examples/meshtastic/qml6/img/send.png b/examples/meshtastic/qml6/img/send.png new file mode 100644 index 0000000..3f08b34 Binary files /dev/null and b/examples/meshtastic/qml6/img/send.png differ diff --git a/examples/meshtastic/qml6/main.qml b/examples/meshtastic/qml6/main.qml new file mode 100644 index 0000000..7f4b946 --- /dev/null +++ b/examples/meshtastic/qml6/main.qml @@ -0,0 +1,183 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Controls.Basic +import QtQuick.Window 2.15 +import QtPositioning 5.15 +import "ext/common/" as Com +import "ext/dialogs/" as Dlg + +Item { + id: rootItem + objectName: "main" + width: 350 + height: 550 + + property double headerHeight: 48 + property bool mobile: Lisp.call("mobile-p") + property bool broadcast: false + + function showKeyboard(show) { + show ? Qt.inputMethod.show() : Qt.inputMethod.hide() + } + + Com.MainView { id: view } + + Com.Menu { + id: menu + objectName: "menu" + + function show() { popup(0, headerHeight) } + + Com.MenuItem { + objectName: "help" + text: qsTr("Help") + onTriggered: help.active ? help.item.enabled = !help.item.enabled : help.active = true + } + + Com.MenuItem { + text: qsTr("Channel name...") + onTriggered: Lisp.call("lora:edit-channel-name") + enabled: (view.currentIndex === 0) + } + + Com.MenuItem { + text: qsTr("Update group/nodes") + onTriggered: Lisp.call("lora:get-node-config") + enabled: (view.currentIndex === 0) + } + + Com.MenuItem { + text: qsTr("Message font size...") + onTriggered: Lisp.call("msg:font-size-dialog") + enabled: (view.currentIndex === 1) + } + + MenuSeparator {} + + Com.MenuItem { + objectName: "share_location" + text: qsTr("Share my location...") + onTriggered: Lisp.call("loc:share-my-location") + } + + MenuSeparator {} + + Com.MenuItem { + text: qsTr("Device filter...") + onTriggered: Lisp.call("lora:edit-device-filter") + enabled: (view.currentIndex === 2) + } + + Com.MenuItem { + text: qsTr("Export DB (Lisp)") + onTriggered: Lisp.call("db:export-to-list") + } + + Com.MenuItem { + text: qsTr("Make backup") + onTriggered: Lisp.call("app:make-backup") + enabled: !mobile + } + } + + Image { + source: "img/logo.png" + x: 2 + y: 2 + width: headerHeight + height: width + + MouseArea { + anchors.fill: parent + onClicked: menu.show() + } + } + + Image { // location icon ('Group') + objectName: "location" + source: "img/location.png" + width: headerHeight + height: width + anchors.right: parent.right + visible: false + + MouseArea { + anchors.fill: parent + onClicked: Lisp.call("loc:show-map-clicked") + } + } + + Image { // find icon ('Messages') + objectName: "find" + source: "img/find.png" + width: headerHeight + height: width + anchors.right: parent.right + + MouseArea { + anchors.fill: parent + onClicked: Lisp.call("msg:find-clicked") + } + } + + Com.Hourglass { // animation while loading app + id: hourglass + } + + AnimatedImage { + objectName: "busy" + anchors.centerIn: parent + width: 42 + height: width + z: 10 + source: "img/busy.gif" + visible: playing + playing: false + } + + // GPS + + PositionSource { + objectName: "position_source" + updateInterval: 2000 + active: false + + property double lat: 0 + property double lon: 0 + property string time: "0" // no 'long' in JS + + onPositionChanged: { + if (position.latitudeValid && position.longitudeValid) { + var coor = position.coordinate; + lat = coor.latitude + lon = coor.longitude + if (position.timestamp) { + var stime = String(position.timestamp.getTime()) + time = stime.substring(0, stime.length - 3) + } else { + time = "0" + } + } + } + + function lastPosition() { + return [lat, lon, time] + } + } + + Com.Toast {} + + Dlg.Dialogs {} + + Loader { + id: help + y: headerHeight + width: parent.width + height: parent.height - headerHeight + source: "ext/common/Help.qml" + active: false + } + + FontLoader { id: fontText; source: "fonts/Ubuntu.ttf" } + FontLoader { id: fontText2; source: "fonts/Ubuntu-Medium.ttf" } +} diff --git a/examples/meshtastic/qml6/tile-provider/cycle b/examples/meshtastic/qml6/tile-provider/cycle new file mode 100644 index 0000000..0e00d28 --- /dev/null +++ b/examples/meshtastic/qml6/tile-provider/cycle @@ -0,0 +1,10 @@ +{ + "UrlTemplate": "https://tile.openstreetmap.org/%z/%x/%y.png", + "ImageFormat": "png", + "QImageFormat": "Indexed8", + "ID": "cl-meshtastic", + "MaximumZoomLevel": 19, + "MapCopyRight": "OpenStreetMap", + "DataCopyRight": "" +} + diff --git a/examples/meshtastic/qml6/tile-provider/hiking b/examples/meshtastic/qml6/tile-provider/hiking new file mode 100644 index 0000000..0e00d28 --- /dev/null +++ b/examples/meshtastic/qml6/tile-provider/hiking @@ -0,0 +1,10 @@ +{ + "UrlTemplate": "https://tile.openstreetmap.org/%z/%x/%y.png", + "ImageFormat": "png", + "QImageFormat": "Indexed8", + "ID": "cl-meshtastic", + "MaximumZoomLevel": 19, + "MapCopyRight": "OpenStreetMap", + "DataCopyRight": "" +} + diff --git a/examples/meshtastic/qml6/tile-provider/night-transit b/examples/meshtastic/qml6/tile-provider/night-transit new file mode 100644 index 0000000..0e00d28 --- /dev/null +++ b/examples/meshtastic/qml6/tile-provider/night-transit @@ -0,0 +1,10 @@ +{ + "UrlTemplate": "https://tile.openstreetmap.org/%z/%x/%y.png", + "ImageFormat": "png", + "QImageFormat": "Indexed8", + "ID": "cl-meshtastic", + "MaximumZoomLevel": 19, + "MapCopyRight": "OpenStreetMap", + "DataCopyRight": "" +} + diff --git a/examples/meshtastic/qml6/tile-provider/satellite b/examples/meshtastic/qml6/tile-provider/satellite new file mode 100644 index 0000000..0e00d28 --- /dev/null +++ b/examples/meshtastic/qml6/tile-provider/satellite @@ -0,0 +1,10 @@ +{ + "UrlTemplate": "https://tile.openstreetmap.org/%z/%x/%y.png", + "ImageFormat": "png", + "QImageFormat": "Indexed8", + "ID": "cl-meshtastic", + "MaximumZoomLevel": 19, + "MapCopyRight": "OpenStreetMap", + "DataCopyRight": "" +} + diff --git a/examples/meshtastic/qml6/tile-provider/street b/examples/meshtastic/qml6/tile-provider/street new file mode 100644 index 0000000..0e00d28 --- /dev/null +++ b/examples/meshtastic/qml6/tile-provider/street @@ -0,0 +1,10 @@ +{ + "UrlTemplate": "https://tile.openstreetmap.org/%z/%x/%y.png", + "ImageFormat": "png", + "QImageFormat": "Indexed8", + "ID": "cl-meshtastic", + "MaximumZoomLevel": 19, + "MapCopyRight": "OpenStreetMap", + "DataCopyRight": "" +} + diff --git a/examples/meshtastic/qml6/tile-provider/terrain b/examples/meshtastic/qml6/tile-provider/terrain new file mode 100644 index 0000000..0e00d28 --- /dev/null +++ b/examples/meshtastic/qml6/tile-provider/terrain @@ -0,0 +1,10 @@ +{ + "UrlTemplate": "https://tile.openstreetmap.org/%z/%x/%y.png", + "ImageFormat": "png", + "QImageFormat": "Indexed8", + "ID": "cl-meshtastic", + "MaximumZoomLevel": 19, + "MapCopyRight": "OpenStreetMap", + "DataCopyRight": "" +} + diff --git a/examples/meshtastic/qml6/tile-provider/transit b/examples/meshtastic/qml6/tile-provider/transit new file mode 100644 index 0000000..0e00d28 --- /dev/null +++ b/examples/meshtastic/qml6/tile-provider/transit @@ -0,0 +1,10 @@ +{ + "UrlTemplate": "https://tile.openstreetmap.org/%z/%x/%y.png", + "ImageFormat": "png", + "QImageFormat": "Indexed8", + "ID": "cl-meshtastic", + "MaximumZoomLevel": 19, + "MapCopyRight": "OpenStreetMap", + "DataCopyRight": "" +} + diff --git a/examples/meshtastic/readme.md b/examples/meshtastic/readme.md index d233e6c..97d42c5 100644 --- a/examples/meshtastic/readme.md +++ b/examples/meshtastic/readme.md @@ -64,8 +64,9 @@ Tested Tested on Linux, macOS, Windows 10+, android, iOS, SailfishOS. -The macOS version currently crashes when built with Qt5, so a (future proof) -port to Qt6 of this example is underway. +The macOS version must be built with Qt6 (Qt5 has a buggy BLE implementation), +and compiled first (see `build/`). So, you'll need to rebuild lqml using Qt6, +and rename the `qml6/` directory of this app to `qml/`. The iOS version also runs on older devices, like the 'iPod touch', as long as they are 64 bit and run at least iOS 12.