├── .gitignore ├── NFCManager.cpp ├── NFCManager.h ├── README.md ├── android ├── AndroidManifest.xml ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── res │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-ldpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── playstore-icon.png │ └── values │ ├── ic_launcher_background.xml │ └── libs.xml ├── assets ├── alarm.wav ├── bake.png └── icons │ ├── readTagGreen.png │ ├── readTagWhite.png │ ├── saveTagGreen.png │ ├── saveTagWhite.png │ ├── timerGreen.png │ └── timerWhite.png ├── main.cpp ├── nfc-demo.pro ├── pictures ├── QtQML-NFC-Demo.jpg ├── built-with-qt.png └── ss-logo.jpg ├── qml.qrc └── qml ├── MainView.qml ├── Theme.qml ├── controls ├── CustomButton.qml ├── CustomPopup.qml ├── CustomTumbler.qml └── PulsatingImage.qml ├── main.qml ├── navigation ├── BottomNavigationBar.qml └── BottomNavigationItem.qml └── pages ├── ReadTagPage.qml ├── SaveTagPage.qml └── TimerPage.qml /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used to ignore files which are generated 2 | # ---------------------------------------------------------------------------- 3 | 4 | build/* 5 | build-* 6 | 7 | firebase-debug.log 8 | FelgoLive/* 9 | 10 | *~ 11 | *.autosave 12 | *.a 13 | *.core 14 | *.moc 15 | *.o 16 | *.obj 17 | *.orig 18 | *.rej 19 | *.so 20 | *.qm 21 | *.so.* 22 | *_pch.h.cpp 23 | *_resource.rc 24 | .#* 25 | *.*# 26 | core 27 | !core/ 28 | tags 29 | .DS_Store 30 | .directory 31 | *.debug 32 | Makefile* 33 | *.prl 34 | *.app 35 | moc_*.cpp 36 | ui_*.h 37 | qrc_*.cpp 38 | Thumbs.db 39 | *.res 40 | *.rc 41 | /.qmake.cache 42 | /.qmake.stash 43 | 44 | # qtcreator generated files 45 | *.pro.user* 46 | 47 | # xemacs temporary files 48 | *.flc 49 | 50 | # Vim temporary files 51 | .*.swp 52 | 53 | # Visual Studio generated files 54 | *.ib_pdb_index 55 | *.idb 56 | *.ilk 57 | *.pdb 58 | *.sln 59 | *.suo 60 | *.vcproj 61 | *vcproj.*.*.user 62 | *.ncb 63 | *.sdf 64 | *.opensdf 65 | *.vcxproj 66 | *vcxproj.* 67 | 68 | # MinGW generated files 69 | *.Debug 70 | *.Release 71 | 72 | # Python byte code 73 | *.pyc 74 | 75 | # Binaries 76 | # -------- 77 | *.dll 78 | *.exe 79 | 80 | -------------------------------------------------------------------------------- /NFCManager.cpp: -------------------------------------------------------------------------------- 1 | #include "NFCManager.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #define DISHNAME "dishname" 13 | #define SECONDS "seconds" 14 | 15 | NFCManager::NFCManager(QObject *parent) 16 | : QObject(parent) 17 | , m_manager(new QNearFieldManager(this)) 18 | { 19 | connect(m_manager, &QNearFieldManager::targetDetected, 20 | this, &NFCManager::onTargetDetected); 21 | connect(m_manager, &QNearFieldManager::targetLost, 22 | this, &NFCManager::onTargetLost); 23 | } 24 | 25 | bool NFCManager::hasTagInRange() const 26 | { 27 | return m_hasTagInRange; 28 | } 29 | 30 | NFCManager::ActionType NFCManager::actionType() const 31 | { 32 | return m_actionType; 33 | } 34 | 35 | void NFCManager::setActionType(NFCManager::ActionType actionType) 36 | { 37 | if (m_actionType == actionType) { 38 | return; 39 | } 40 | 41 | m_actionType = actionType; 42 | emit actionTypeChanged(m_actionType); 43 | } 44 | 45 | void NFCManager::setHasTagInRange(bool hasTagInRange) 46 | { 47 | if (m_hasTagInRange == hasTagInRange) { 48 | return; 49 | } 50 | 51 | m_hasTagInRange = hasTagInRange; 52 | emit hasTagInRangeChanged(m_hasTagInRange); 53 | } 54 | 55 | void NFCManager::startReading() 56 | { 57 | setActionType(ActionType::Reading); 58 | m_manager->setTargetAccessModes(QNearFieldManager::NdefReadTargetAccess); 59 | m_manager->startTargetDetection(); 60 | } 61 | 62 | void NFCManager::stopDetecting() 63 | { 64 | setActionType(ActionType::None); 65 | m_manager->setTargetAccessModes(QNearFieldManager::NoTargetAccess); 66 | m_manager->stopTargetDetection(); 67 | } 68 | 69 | void NFCManager::saveRecord(const QString &dishName, int seconds) 70 | { 71 | m_record.dishName = dishName; 72 | m_record.seconds = seconds; 73 | 74 | setActionType(ActionType::Writing); 75 | m_manager->setTargetAccessModes(QNearFieldManager::NdefWriteTargetAccess); 76 | m_manager->startTargetDetection(); 77 | } 78 | 79 | void NFCManager::onTargetDetected(QNearFieldTarget *target) 80 | { 81 | setHasTagInRange(true); 82 | 83 | switch (m_actionType) { 84 | case None: 85 | break; 86 | case Reading: 87 | connect(target, &QNearFieldTarget::ndefMessageRead, this, &NFCManager::onNdefMessageRead); 88 | connect(target, &QNearFieldTarget::error, this, &NFCManager::handleTargetError); 89 | 90 | m_request = target->readNdefMessages(); 91 | if (!m_request.isValid()) { 92 | handleTargetError(QNearFieldTarget::NdefReadError, m_request); 93 | } 94 | break; 95 | case Writing: 96 | connect(target, &QNearFieldTarget::ndefMessagesWritten, this, &NFCManager::onNdefMessageWritten); 97 | connect(target, &QNearFieldTarget::error, this, &NFCManager::handleTargetError); 98 | 99 | m_request = target->writeNdefMessages(QList() << m_record.generateNdefMessage()); 100 | if (!m_request.isValid()) { 101 | handleTargetError(QNearFieldTarget::NdefWriteError, m_request); 102 | } 103 | break; 104 | } 105 | } 106 | 107 | void NFCManager::onTargetLost(QNearFieldTarget *target) 108 | { 109 | setHasTagInRange(false); 110 | target->deleteLater(); 111 | } 112 | 113 | void NFCManager::onNdefMessageRead(const QNdefMessage &message) 114 | { 115 | bool recordFound = false; 116 | for (const QNdefRecord &record : message) { 117 | if (record.isRecordType()) { 118 | recordFound = m_record.parseNdefMessage(record); 119 | if (recordFound) { 120 | break; 121 | } 122 | } 123 | } 124 | 125 | stopDetecting(); 126 | m_request = QNearFieldTarget::RequestId(); 127 | 128 | if (recordFound) { 129 | emit recordChanged(m_record); 130 | } else { 131 | emit nfcError("Tag does not contain desired record or is malformed"); 132 | } 133 | } 134 | 135 | void NFCManager::onNdefMessageWritten() 136 | { 137 | stopDetecting(); 138 | m_request = QNearFieldTarget::RequestId(); 139 | 140 | emit wroteSuccessfully(); 141 | } 142 | 143 | void NFCManager::handleTargetError(QNearFieldTarget::Error error, const QNearFieldTarget::RequestId &id) 144 | { 145 | if (m_request == id) { 146 | 147 | QString errorMsg; 148 | 149 | switch (error) { 150 | case QNearFieldTarget::NoError: 151 | break; 152 | case QNearFieldTarget::UnsupportedError: 153 | errorMsg = "Unsupported tag"; 154 | break; 155 | case QNearFieldTarget::TargetOutOfRangeError: 156 | errorMsg = "Tag removed from field"; 157 | break; 158 | case QNearFieldTarget::NoResponseError: 159 | errorMsg = "No response from tag"; 160 | break; 161 | case QNearFieldTarget::ChecksumMismatchError: 162 | errorMsg = "Checksum mismatch"; 163 | break; 164 | case QNearFieldTarget::InvalidParametersError: 165 | errorMsg = "Invalid parameters"; 166 | break; 167 | case QNearFieldTarget::NdefReadError: 168 | errorMsg = "NDEF read error"; 169 | break; 170 | case QNearFieldTarget::NdefWriteError: 171 | errorMsg = "NDEF write error"; 172 | break; 173 | default: 174 | errorMsg = "Unknown error"; 175 | } 176 | 177 | m_manager->setTargetAccessModes(QNearFieldManager::NoTargetAccess); 178 | m_manager->stopTargetDetection(); 179 | m_request = QNearFieldTarget::RequestId(); 180 | 181 | if (!errorMsg.isEmpty()) { 182 | emit nfcError(errorMsg); 183 | } 184 | } 185 | } 186 | 187 | Record NFCManager::record() const 188 | { 189 | return m_record; 190 | } 191 | 192 | bool Record::parseNdefMessage(const QNdefNfcTextRecord &record) 193 | { 194 | const QJsonDocument &doc = QJsonDocument::fromJson(record.text().toUtf8()); 195 | const QJsonObject &recordObject = doc.object(); 196 | 197 | if (!recordObject.contains(DISHNAME) || !recordObject.contains(DISHNAME)) { 198 | return false; 199 | } 200 | 201 | dishName = recordObject[DISHNAME].toString(); 202 | seconds = recordObject[SECONDS].toInt(); 203 | 204 | return true; 205 | } 206 | 207 | QNdefMessage Record::generateNdefMessage() const 208 | { 209 | if (dishName.isEmpty() || seconds <= 0) { 210 | return QNdefMessage(); 211 | } 212 | 213 | QNdefMessage message; 214 | 215 | QVariantMap recordMap{}; 216 | recordMap[DISHNAME] = dishName; 217 | recordMap[SECONDS] = seconds; 218 | 219 | const QJsonDocument &doc = QJsonDocument::fromVariant(recordMap); 220 | 221 | QNdefNfcTextRecord record; 222 | record.setEncoding(QNdefNfcTextRecord::Utf8); 223 | record.setText(doc.toJson()); 224 | 225 | message.append(record); 226 | 227 | return message; 228 | } 229 | -------------------------------------------------------------------------------- /NFCManager.h: -------------------------------------------------------------------------------- 1 | #ifndef NFCMANAGER_H 2 | #define NFCMANAGER_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class QNearFieldManager; 9 | class QNdefMessage; 10 | class QNdefNfcTextRecord; 11 | 12 | struct Record { 13 | Q_GADGET 14 | Q_PROPERTY(int seconds MEMBER seconds) 15 | Q_PROPERTY(QString dishName MEMBER dishName) 16 | 17 | public: 18 | int seconds = 0; 19 | QString dishName = ""; 20 | 21 | bool parseNdefMessage(const QNdefNfcTextRecord &record); 22 | QNdefMessage generateNdefMessage() const; 23 | }; 24 | Q_DECLARE_METATYPE(Record) 25 | 26 | class NFCManager : public QObject 27 | { 28 | Q_OBJECT 29 | Q_PROPERTY(bool hasTagInRange READ hasTagInRange NOTIFY hasTagInRangeChanged) 30 | Q_PROPERTY(ActionType actionType READ actionType WRITE setActionType NOTIFY actionTypeChanged) 31 | Q_PROPERTY(Record record READ record NOTIFY recordChanged) 32 | QML_ELEMENT 33 | 34 | public: 35 | explicit NFCManager(QObject *parent = nullptr); 36 | 37 | enum ActionType 38 | { 39 | None = 0, 40 | Reading, 41 | Writing 42 | }; 43 | Q_ENUM(ActionType) 44 | 45 | bool hasTagInRange() const; 46 | ActionType actionType() const; 47 | Record record() const; 48 | 49 | public slots: 50 | void startReading(); 51 | void stopDetecting(); 52 | void saveRecord(const QString &dishName, int seconds); 53 | 54 | signals: 55 | void hasTagInRangeChanged(bool hasTagInRange); 56 | void actionTypeChanged(ActionType actionType); 57 | void recordChanged(const Record &record); 58 | 59 | void wroteSuccessfully(); 60 | void nfcError(const QString &error); 61 | 62 | private slots: 63 | void setActionType(ActionType actionType); 64 | void setHasTagInRange(bool hasTagInRange); 65 | 66 | void onTargetDetected(QNearFieldTarget *target); 67 | void onTargetLost(QNearFieldTarget *target); 68 | 69 | void onNdefMessageRead(const QNdefMessage &message); 70 | void onNdefMessageWritten(); 71 | void handleTargetError(QNearFieldTarget::Error error, const QNearFieldTarget::RequestId &id); 72 | 73 | private: 74 | bool m_hasTagInRange = false; 75 | ActionType m_actionType = ActionType::None; 76 | 77 | Record m_record; 78 | QNearFieldManager *m_manager; 79 | QNearFieldTarget::RequestId m_request; 80 | }; 81 | 82 | #endif // NFCMANAGER_H 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NFC Demo 2 | [![Scythe Studio](./pictures/QtQML-NFC-Demo.jpg)](https://scythe-studio.com/blog/nfc-in-qt-qml-application) 3 | This demo presents how to use NFC for communication using Qt framework on mobile platform. Application has simple UI and logic that can be an example of creative NFC usage. Demo presents following features: 4 | 5 | - NFC tags detection 6 | - NFC tags reading 7 | - NFC tags writing 8 | - Exposing C++ to Qml to build creative cooking mobile app 🍗 🍳 9 | 10 | --- 11 | 12 | [![Scythe Studio](./pictures/ss-logo.jpg)](https://scythe-studio.com) 13 | 14 | [![Built with Qt](./pictures/built-with-qt.png)](https://qt.io) 15 | 16 | --- 17 | 18 | ## How to use NFC in Qt/Qml application? 19 | If you need more detailed blog post on NFC topic you can read this 20 | [blog post on Scythe Studio blog](https://scythe-studio.com/blog/nfc-in-qt-qml-application). Here we will talk only about NFC tags detection. 21 | 22 | ### NFC tags detection 23 | We created a NFCManager class that among other members and methods, have important QNearFieldManager instance saved. Connecting to this object is crucial to control detection and handle signals. 24 | 25 | ```cpp 26 | // directives, forward declarations, structure declaration 27 | 28 | class NFCManager : public QObject 29 | { 30 | Q_OBJECT 31 | // properties 32 | QML_ELEMENT 33 | 34 | public: 35 | explicit NFCManager(QObject *parent = nullptr); 36 | 37 | // ... 38 | 39 | public slots: 40 | void startReading(); 41 | void stopDetecting(); 42 | void saveRecord(const QString &dishName, int seconds); 43 | 44 | signals: 45 | // ... 46 | void recordChanged(const Record &record); 47 | void wroteSuccessfully(); 48 | void nfcError(const QString &error); 49 | 50 | private slots: 51 | // ... 52 | void onTargetDetected(QNearFieldTarget *target); 53 | void onTargetLost(QNearFieldTarget *target); 54 | 55 | void onNdefMessageRead(const QNdefMessage &message); 56 | void onNdefMessageWritten(); 57 | void handleTargetError(QNearFieldTarget::Error error, const QNearFieldTarget::RequestId &id); 58 | 59 | private: 60 | // ... 61 | QNearFieldManager *m_manager; 62 | QNearFieldTarget::RequestId m_request; 63 | }; 64 | 65 | #endif // NFCMANAGER_H 66 | 67 | ``` 68 | 69 | Then we connect to register slots for two important signals emitted by QNearFieldManager instance. The first one - targetDetected, is emitted when target device (device, tag, card) goes into a range. When target device leaves communication range, targetLost signal is emitted. 70 | 71 | ```cpp 72 | NFCManager::NFCManager(QObject *parent) 73 | : QObject(parent) 74 | , m_manager(new QNearFieldManager(this)) 75 | { 76 | 77 | connect(m_manager, &QNearFieldManager::targetDetected, 78 | this, &NFCManager::onTargetDetected); 79 | connect(m_manager, &QNearFieldManager::targetLost, 80 | this, &NFCManager::onTargetLost); 81 | } 82 | ``` 83 | 84 | And that's pretty it. This way you can detect tags, but before you will see slots executed, you need to ask QNearFieldManager to start detection. You start detection by calling `startTargetDetection()` method on manager, but first set target access mode by calling `setTargetAccessModes(mode)`. 85 | 86 | ```cpp 87 | void NFCManager::startReading() 88 | { 89 | // ... 90 | m_manager->setTargetAccessModes(QNearFieldManager::NdefReadTargetAccess); 91 | m_manager->startTargetDetection(); 92 | } 93 | 94 | void NFCManager::stopDetecting() 95 | { 96 | // ... 97 | m_manager->setTargetAccessModes(QNearFieldManager::NoTargetAccess); 98 | m_manager->stopTargetDetection(); 99 | } 100 | ``` 101 | 102 | Once you are done with your NFC feature you can call `stopTargetDetection()` to prevent future NFC target events. 103 | 104 | Yeah so that's it. This Readme is already too long, so feel free to visit our blog to discover how to actually read and write messages from/on NFC tags. 105 | 106 | ## About Scythe Studio 107 | We’re a team of **Qt and C++ enthusiasts** dedicated to helping businesses build great cross-platform applications. As an official Qt Service Partner, we’ve earned the trust of companies across various industries by delivering high-quality, reliable solutions. With years of experience in **Qt and QML development**, we know how to turn ambitious ideas into outstanding products. 108 | 109 | 110 | 111 | 117 | 123 | 128 | 133 | 134 |
112 | 113 | 115 | 116 | 118 | 119 | 121 | 122 | 124 | 125 | 126 | 127 | 129 | 130 | 131 | 132 |
135 | 136 | We offer a wide range of services—from brainstorming ideas to delivering polished applications—always tailored to our clients’ needs. By combining deep knowledge of Qt modules and modern technologies with a practical, cost-effective approach, we create solutions that truly make a difference. 137 | 138 | ## Professional Support 139 | Need help with anything? We’ve got you covered. Our professional support services are here to assist you with. For more details about support options and pricing, just drop us a line at https://scythe-studio.com/en/contact. 140 | 141 | ## Follow us 142 | Check out those links if you want to see Scythe Studio in action and follow the newest trends saying about Qt Qml development. 143 | 144 | * 🌐 [Scythe Studio Website](https://scythe-studio.com/en/) 145 | * ✍️ [Scythe Studio Blog Website](https://scythe-studio.com/en/blog) 146 | * 👔 [Scythe Studio LinkedIn Profile](https://www.linkedin.com/company/scythestudio/mycompany/) 147 | * 👔 [Scythe Studio Facebook Page](https://www.facebook.com/ScytheStudiio) 148 | * 🎥 [Scythe Studio Youtube Channel](https://www.youtube.com/channel/UCf4OHosddUYcfmLuGU9e-SQ/featured) 149 | -------------------------------------------------------------------------------- /android/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.6.0' 9 | } 10 | } 11 | 12 | repositories { 13 | google() 14 | jcenter() 15 | } 16 | 17 | apply plugin: 'com.android.application' 18 | 19 | dependencies { 20 | implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) 21 | } 22 | 23 | android { 24 | /******************************************************* 25 | * The following variables: 26 | * - androidBuildToolsVersion, 27 | * - androidCompileSdkVersion 28 | * - qt5AndroidDir - holds the path to qt android files 29 | * needed to build any Qt application 30 | * on Android. 31 | * 32 | * are defined in gradle.properties file. This file is 33 | * updated by QtCreator and androiddeployqt tools. 34 | * Changing them manually might break the compilation! 35 | *******************************************************/ 36 | 37 | compileSdkVersion androidCompileSdkVersion.toInteger() 38 | 39 | buildToolsVersion '28.0.3' 40 | 41 | sourceSets { 42 | main { 43 | manifest.srcFile 'AndroidManifest.xml' 44 | java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java'] 45 | aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl'] 46 | res.srcDirs = [qt5AndroidDir + '/res', 'res'] 47 | resources.srcDirs = ['resources'] 48 | renderscript.srcDirs = ['src'] 49 | assets.srcDirs = ['assets'] 50 | jniLibs.srcDirs = ['libs'] 51 | } 52 | } 53 | 54 | lintOptions { 55 | abortOnError false 56 | } 57 | 58 | // Do not compress Qt binary resources file 59 | aaptOptions { 60 | noCompress 'rcc' 61 | } 62 | 63 | defaultConfig { 64 | resConfig "en" 65 | minSdkVersion = qtMinSdkVersion 66 | targetSdkVersion = qtTargetSdkVersion 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /android/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/android/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/android/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/android/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/res/mipmap-ldpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/android/res/mipmap-ldpi/ic_launcher.png -------------------------------------------------------------------------------- /android/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/android/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/android/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/android/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/android/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/android/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/android/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/android/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/android/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/android/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/android/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/android/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/android/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/res/playstore-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/android/res/playstore-icon.png -------------------------------------------------------------------------------- /android/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #218165 4 | -------------------------------------------------------------------------------- /android/res/values/libs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | https://download.qt.io/ministro/android/qt5/qt-5.14 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /assets/alarm.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/assets/alarm.wav -------------------------------------------------------------------------------- /assets/bake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/assets/bake.png -------------------------------------------------------------------------------- /assets/icons/readTagGreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/assets/icons/readTagGreen.png -------------------------------------------------------------------------------- /assets/icons/readTagWhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/assets/icons/readTagWhite.png -------------------------------------------------------------------------------- /assets/icons/saveTagGreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/assets/icons/saveTagGreen.png -------------------------------------------------------------------------------- /assets/icons/saveTagWhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/assets/icons/saveTagWhite.png -------------------------------------------------------------------------------- /assets/icons/timerGreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/assets/icons/timerGreen.png -------------------------------------------------------------------------------- /assets/icons/timerWhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/assets/icons/timerWhite.png -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "NFCManager.h" 5 | 6 | int main(int argc, char *argv[]) 7 | { 8 | QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 9 | 10 | QGuiApplication app(argc, argv); 11 | QQmlApplicationEngine engine; 12 | 13 | NFCManager *nfcManager = new NFCManager(&app); 14 | engine.rootContext()->setContextProperty("nfcManager", nfcManager); 15 | 16 | const QUrl url(QStringLiteral("qrc:/qml/main.qml")); 17 | QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, 18 | &app, [url](QObject *obj, const QUrl &objUrl) { 19 | if (!obj && url == objUrl) 20 | QCoreApplication::exit(-1); 21 | }, Qt::QueuedConnection); 22 | engine.load(url); 23 | 24 | return app.exec(); 25 | } 26 | -------------------------------------------------------------------------------- /nfc-demo.pro: -------------------------------------------------------------------------------- 1 | QT += quick nfc multimedia 2 | 3 | CONFIG += qmltypes c++11 4 | QML_IMPORT_NAME = com.scythestudio.nfc 5 | QML_IMPORT_MAJOR_VERSION = 1 6 | 7 | 8 | # The following define makes your compiler emit warnings if you use 9 | # any Qt feature that has been marked deprecated (the exact warnings 10 | # depend on your compiler). Refer to the documentation for the 11 | # deprecated API to know how to port your code away from it. 12 | DEFINES += QT_DEPRECATED_WARNINGS 13 | 14 | # You can also make your code fail to compile if it uses deprecated APIs. 15 | # In order to do so, uncomment the following line. 16 | # You can also select to disable deprecated APIs only up to a certain version of Qt. 17 | #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 18 | 19 | SOURCES += \ 20 | NFCManager.cpp \ 21 | main.cpp 22 | 23 | RESOURCES += qml.qrc 24 | 25 | # Additional import path used to resolve QML modules in Qt Creator's code model 26 | QML_IMPORT_PATH = 27 | 28 | # Additional import path used to resolve QML modules just for Qt Quick Designer 29 | QML_DESIGNER_IMPORT_PATH = 30 | 31 | # Default rules for deployment. 32 | qnx: target.path = /tmp/$${TARGET}/bin 33 | else: unix:!android: target.path = /opt/$${TARGET}/bin 34 | !isEmpty(target.path): INSTALLS += target 35 | 36 | DISTFILES += \ 37 | android/AndroidManifest.xml \ 38 | android/build.gradle \ 39 | android/gradle/wrapper/gradle-wrapper.jar \ 40 | android/gradle/wrapper/gradle-wrapper.properties \ 41 | android/gradlew \ 42 | android/gradlew.bat \ 43 | android/res/values/libs.xml 44 | 45 | ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android 46 | 47 | HEADERS += \ 48 | NFCManager.h 49 | -------------------------------------------------------------------------------- /pictures/QtQML-NFC-Demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/pictures/QtQML-NFC-Demo.jpg -------------------------------------------------------------------------------- /pictures/built-with-qt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/pictures/built-with-qt.png -------------------------------------------------------------------------------- /pictures/ss-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scytheStudio/NFC-Demo/bbf7a3b182ef80eee86cfb4ebee9fe7fe5e12726/pictures/ss-logo.jpg -------------------------------------------------------------------------------- /qml.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | qml/navigation/BottomNavigationItem.qml 4 | qml/navigation/BottomNavigationBar.qml 5 | assets/icons/readTagGreen.png 6 | assets/icons/readTagWhite.png 7 | assets/icons/saveTagGreen.png 8 | assets/icons/saveTagWhite.png 9 | assets/icons/timerGreen.png 10 | assets/icons/timerWhite.png 11 | qml/Theme.qml 12 | qml/main.qml 13 | qml/MainView.qml 14 | qml/pages/ReadTagPage.qml 15 | qml/pages/SaveTagPage.qml 16 | qml/pages/TimerPage.qml 17 | assets/bake.png 18 | assets/alarm.wav 19 | qml/controls/CustomButton.qml 20 | qml/controls/CustomPopup.qml 21 | qml/controls/CustomTumbler.qml 22 | qml/controls/PulsatingImage.qml 23 | 24 | 25 | -------------------------------------------------------------------------------- /qml/MainView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.12 2 | import QtQuick.Controls 2.12 3 | import "navigation" 4 | import "pages" 5 | 6 | Item { 7 | id: root 8 | 9 | SwipeView { 10 | id: swipeView 11 | 12 | anchors { 13 | top: parent.top 14 | bottom: bottomNavigationBar.top 15 | left: parent.left 16 | right: parent.right 17 | } 18 | 19 | state: bottomNavigationBar.state 20 | states: [ 21 | State { 22 | name: "readTag" 23 | PropertyChanges { 24 | target: swipeView 25 | currentIndex: 0 26 | } 27 | }, 28 | State { 29 | name: "timer" 30 | PropertyChanges { 31 | target: swipeView 32 | currentIndex: 1 33 | } 34 | }, 35 | State { 36 | name: "saveTag" 37 | PropertyChanges { 38 | target: swipeView 39 | currentIndex: 2 40 | } 41 | } 42 | ] 43 | 44 | interactive: false 45 | 46 | ReadTagPage { 47 | id: readTagPage 48 | 49 | visible: SwipeView.isCurrentItem 50 | 51 | onTagFound: { 52 | timerPage.startCountdown(dishname, seconds) 53 | bottomNavigationBar.state = "timer" 54 | } 55 | } 56 | 57 | TimerPage { 58 | id: timerPage 59 | 60 | visible: SwipeView.isCurrentItem 61 | 62 | onReadButtonClicked: { 63 | bottomNavigationBar.state = "readTag" 64 | } 65 | 66 | onSaveButtonClicked: { 67 | bottomNavigationBar.state = "saveTag" 68 | } 69 | } 70 | 71 | SaveTagPage { 72 | id: saveTagPage 73 | 74 | visible: SwipeView.isCurrentItem 75 | 76 | onSaved: { 77 | bottomNavigationBar.state = "timer" 78 | } 79 | } 80 | } 81 | 82 | BottomNavigationBar { 83 | id: bottomNavigationBar 84 | 85 | width: parent.width 86 | 87 | anchors { 88 | bottom: parent.bottom 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /qml/Theme.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.12 2 | 3 | QtObject { 4 | id: root 5 | 6 | readonly property color backgroundColor: "#292929" 7 | readonly property color errorColor: "#ca3a1d" 8 | readonly property color secondaryBackgroundColor: "#474747" 9 | readonly property color textColor: "#FFFFFF" 10 | readonly property color tintColor: "#1dca9b" 11 | } 12 | -------------------------------------------------------------------------------- /qml/controls/CustomButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.12 2 | import QtQuick.Controls 2.12 3 | 4 | Button { 5 | id: root 6 | 7 | property color backgroundColor: theme.tintColor 8 | 9 | contentItem: Text { 10 | text: root.text 11 | font: root.font 12 | opacity: enabled ? 1.0 : 0.3 13 | color: root.down ? Qt.darker(theme.textColor) : theme.textColor 14 | horizontalAlignment: Text.AlignHCenter 15 | verticalAlignment: Text.AlignVCenter 16 | elide: Text.ElideRight 17 | } 18 | 19 | background: Rectangle { 20 | opacity: enabled ? 1 : 0.3 21 | color: root.down ? Qt.darker(root.backgroundColor) : root.backgroundColor 22 | radius: 5 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /qml/controls/CustomPopup.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.12 2 | import QtQuick.Controls 2.12 3 | import QtGraphicalEffects 1.12 4 | 5 | Popup { 6 | id: root 7 | 8 | property string message: "" 9 | property bool isErrorPopup: false 10 | readonly property color mainColor: root.isErrorPopup ? theme.errorColor : theme.tintColor 11 | 12 | modal: true 13 | focus: true 14 | closePolicy: Popup.NoAutoClose 15 | 16 | background: Rectangle { 17 | opacity: 0.2 18 | color: "black" 19 | } 20 | 21 | Rectangle { 22 | id: content 23 | anchors.centerIn: parent 24 | width: root.width * 0.8 25 | height: contentColumn.height + 2 * 20 26 | 27 | color: theme.backgroundColor 28 | radius: 10 29 | 30 | Column { 31 | id: contentColumn 32 | anchors.centerIn: parent 33 | width: parent.width 34 | spacing: 20 35 | 36 | Text { 37 | anchors.horizontalCenter: parent.horizontalCenter 38 | 39 | color: root.mainColor 40 | font.bold: true 41 | text: root.isErrorPopup ? "Error" : "Success" 42 | } 43 | 44 | Text { 45 | anchors.horizontalCenter: parent.horizontalCenter 46 | 47 | color: theme.textColor 48 | text: root.message 49 | } 50 | 51 | CustomButton { 52 | anchors.horizontalCenter: parent.horizontalCenter 53 | 54 | backgroundColor: root.mainColor 55 | text: "Ok" 56 | 57 | onClicked: { 58 | root.close() 59 | } 60 | } 61 | } 62 | } 63 | 64 | DropShadow { 65 | anchors.fill: content 66 | 67 | radius: 18 68 | samples: 37 69 | color: root.mainColor 70 | source: content 71 | transparentBorder: true 72 | } 73 | 74 | function show(message, isErrorPopup) { 75 | root.message = message 76 | root.isErrorPopup = isErrorPopup 77 | root.open() 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /qml/controls/CustomTumbler.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.12 2 | import QtQuick.Window 2.12 3 | import QtQuick.Controls 2.12 4 | 5 | Item { 6 | id: root 7 | 8 | property alias hours: hoursTumbler.currentIndex 9 | property alias minutes: minutesTumbler.currentIndex 10 | 11 | width: frame.implicitWidth + 10 12 | height: frame.implicitHeight + 10 13 | 14 | function formatText(count, modelData) { 15 | var data = count === 12 ? modelData + 1 : modelData 16 | return data.toString().length < 2 ? "0" + data : data 17 | } 18 | 19 | FontMetrics { 20 | id: fontMetrics 21 | } 22 | 23 | Component { 24 | id: delegateComponent 25 | 26 | Label { 27 | opacity: 1.0 - Math.abs(Tumbler.displacement) / (Tumbler.tumbler.visibleItemCount / 2) 28 | 29 | color: theme.textColor 30 | font.pixelSize: fontMetrics.font.pixelSize * 1.25 31 | horizontalAlignment: Text.AlignHCenter 32 | verticalAlignment: Text.AlignVCenter 33 | text: formatText(Tumbler.tumbler.count, modelData) 34 | } 35 | } 36 | 37 | Frame { 38 | id: frame 39 | padding: 0 40 | anchors.centerIn: parent 41 | 42 | Row { 43 | id: row 44 | 45 | Tumbler { 46 | id: hoursTumbler 47 | model: 24 48 | delegate: delegateComponent 49 | } 50 | 51 | Tumbler { 52 | id: minutesTumbler 53 | model: 60 54 | delegate: delegateComponent 55 | } 56 | 57 | Tumbler { 58 | id: secondsTumbler 59 | model: 60 60 | delegate: delegateComponent 61 | } 62 | } 63 | } 64 | 65 | function reset() { 66 | hoursTumbler.currentIndex = 0 67 | minutesTumbler.currentIndex = 0 68 | secondsTumbler.currentIndex = 0 69 | } 70 | 71 | function getSeconds() { 72 | var hSec = hoursTumbler.currentIndex * 60 * 60 73 | var mSec = minutesTumbler.currentIndex * 60 74 | 75 | return hSec + mSec + secondsTumbler.currentIndex 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /qml/controls/PulsatingImage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Particles 2.15 3 | 4 | Item { 5 | id: root 6 | 7 | readonly property real maxHeight: root.height 8 | readonly property int pulseInterval: 800 9 | 10 | property alias imageSource: image.source 11 | 12 | Rectangle { 13 | id: rectangleOne 14 | anchors.centerIn: imageRec 15 | height: imageRec.height * 0.7 16 | width: height 17 | radius: height / 2 18 | z: 1 19 | color: theme.backgroundColor 20 | } 21 | 22 | PropertyAnimation { 23 | id: rectangleOneAnim 24 | target: rectangleOne 25 | property: "height" 26 | from: imageRec.height * 0.7 27 | to: maxHeight 28 | duration: root.pulseInterval * 5 29 | loops: Animation.Infinite 30 | 31 | onStarted: { 32 | delayTimer.targetAnimationIndex = 2 33 | delayTimer.start() 34 | } 35 | } 36 | 37 | Timer { 38 | running: rectangleOneAnim.running 39 | repeat: true 40 | interval: rectangleOneAnim.duration 41 | 42 | onTriggered: { 43 | rectangleOne.z = 3 44 | rectangleTwo.z -= 1 45 | rectangleThree.z -= 1 46 | } 47 | } 48 | 49 | Rectangle { 50 | id: rectangleTwo 51 | anchors.centerIn: imageRec 52 | z: 2 53 | height: imageRec.height * 0.7 54 | width: height 55 | radius: height / 2 56 | color: theme.secondaryBackgroundColor 57 | } 58 | 59 | PropertyAnimation { 60 | id: rectangleTwoAnim 61 | target: rectangleTwo 62 | property: "height" 63 | from: imageRec.height * 0.7 64 | to: maxHeight 65 | duration: root.pulseInterval * 5 66 | loops: Animation.Infinite 67 | onStarted: { 68 | delayTimer.targetAnimationIndex = 3 69 | delayTimer.start() 70 | } 71 | } 72 | 73 | Timer { 74 | running: rectangleTwoAnim.running 75 | repeat: true 76 | interval: rectangleTwoAnim.duration 77 | 78 | onTriggered: { 79 | rectangleTwo.z = 3 80 | rectangleOne.z -= 1 81 | rectangleThree.z -= 1 82 | } 83 | } 84 | 85 | Rectangle { 86 | id: rectangleThree 87 | anchors.centerIn: imageRec 88 | z: 3 89 | height: imageRec.height * 0.7 90 | width: height 91 | radius: height / 2 92 | color: Qt.lighter(theme.secondaryBackgroundColor) 93 | 94 | } 95 | 96 | PropertyAnimation { 97 | id: rectangleThreeAnim 98 | target: rectangleThree 99 | property: "height" 100 | from: imageRec.height * 0.7 101 | to: maxHeight 102 | duration: root.pulseInterval * 5 103 | loops: Animation.Infinite 104 | } 105 | 106 | Timer { 107 | running: rectangleThreeAnim.running 108 | repeat: true 109 | interval: rectangleThreeAnim.duration 110 | 111 | onTriggered: { 112 | rectangleThree.z = 3 113 | rectangleOne.z -= 1 114 | rectangleTwo.z -= 1 115 | } 116 | } 117 | 118 | Timer { 119 | id: delayTimer 120 | 121 | property int targetAnimationIndex: 2 122 | property real startDelay: 5 123 | 124 | repeat: false 125 | interval: root.pulseInterval 126 | onTriggered: { 127 | if (startDelay == 0) { 128 | switch (targetAnimationIndex) { 129 | case 2: 130 | rectangleTwoAnim.start() 131 | break 132 | case 3: 133 | rectangleThreeAnim.start() 134 | break 135 | } 136 | } else { 137 | startDelay = 0 138 | rectangleOneAnim.start() 139 | } 140 | } 141 | } 142 | 143 | Rectangle { 144 | id: imageRec 145 | z: 10 146 | width: 80 147 | height: width 148 | anchors.centerIn: parent 149 | 150 | color: theme.backgroundColor 151 | radius: height / 2 152 | 153 | Image { 154 | id: image 155 | width: parent.width / 2 156 | height: width 157 | anchors.centerIn: parent 158 | 159 | antialiasing: true 160 | smooth: true 161 | } 162 | } 163 | 164 | function stopAnimation() { 165 | rectangleOneAnim.complete() 166 | rectangleTwoAnim.complete() 167 | rectangleThreeAnim.complete() 168 | delayTimer.stop() 169 | } 170 | 171 | function resetValues() { 172 | rectangleOne.height = imageRec.height * 0.7 173 | rectangleOne.z = 1 174 | rectangleTwo.height = imageRec.height * 0.7 175 | rectangleTwo.z = 2 176 | rectangleThree.height = imageRec.height * 0.7 177 | rectangleThree.z = 3 178 | delayTimer.startDelay = 10 179 | delayTimer.targetAnimationIndex = 2 180 | } 181 | 182 | function startAnimation() { 183 | resetValues() 184 | delayTimer.start() 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /qml/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.12 2 | import QtQuick.Window 2.12 3 | import QtQuick.Controls 2.12 4 | 5 | Window { 6 | id: root 7 | visible: true 8 | width: 640 9 | height: 480 10 | title: qsTr("Hello World") 11 | color: theme.backgroundColor 12 | 13 | Theme { 14 | id: theme 15 | } 16 | 17 | MainView { 18 | id: mainView 19 | 20 | anchors.fill: parent 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /qml/navigation/BottomNavigationBar.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.12 2 | import QtGraphicalEffects 1.12 3 | 4 | Item { 5 | id: root 6 | 7 | property real realHeight: timerItem.height 8 | 9 | readonly property int navigationItemSize: 50 10 | readonly property int navigationItemImageSize: 35 11 | 12 | height: timerItem.height 13 | 14 | state: "timer" 15 | states: [ 16 | State { 17 | name: "readTag" 18 | PropertyChanges { 19 | target: readTagItem 20 | isSelected: true 21 | } 22 | PropertyChanges { 23 | target: timerItem 24 | isSelected: false 25 | } 26 | }, 27 | State { 28 | name: "saveTag" 29 | PropertyChanges { 30 | target: saveTagItem 31 | isSelected: true 32 | } 33 | PropertyChanges { 34 | target: timerItem 35 | isSelected: false 36 | } 37 | } 38 | ] 39 | 40 | Item { 41 | 42 | height: readTagItem.height 43 | anchors { 44 | verticalCenter: parent.verticalCenter 45 | left: parent.left 46 | right: timerItem.left 47 | } 48 | 49 | BottomNavigationItem { 50 | id: readTagItem 51 | 52 | width: root.navigationItemSize 53 | height: root.navigationItemSize 54 | 55 | iconName: "readTag" 56 | iconSize: root.navigationItemImageSize 57 | 58 | anchors { 59 | horizontalCenter: parent.horizontalCenter 60 | } 61 | 62 | onClicked: { 63 | root.state = "readTag" 64 | } 65 | } 66 | } 67 | 68 | Item { 69 | 70 | height: saveTagItem.height 71 | anchors { 72 | verticalCenter: parent.verticalCenter 73 | left: parent.right 74 | right: timerItem.right 75 | } 76 | 77 | Rectangle { 78 | color: "lime" 79 | opacity: 0.1 80 | } 81 | 82 | BottomNavigationItem { 83 | id: saveTagItem 84 | 85 | width: root.navigationItemSize 86 | height: root.navigationItemSize 87 | 88 | iconName: "saveTag" 89 | iconSize: root.navigationItemImageSize 90 | 91 | anchors { 92 | horizontalCenter: parent.horizontalCenter 93 | } 94 | 95 | onClicked: { 96 | root.state = "saveTag" 97 | } 98 | } 99 | } 100 | 101 | Item { 102 | id: timerItem 103 | 104 | property bool isSelected: true 105 | 106 | anchors { 107 | verticalCenter: parent.verticalCenter 108 | verticalCenterOffset: -15 109 | horizontalCenter: parent.horizontalCenter 110 | } 111 | 112 | width: 75 113 | height: width 114 | 115 | Rectangle { 116 | id: timerItemCircle 117 | 118 | width: parent.width - (timerItemMouseArea.pressed ? 4 : 0) 119 | height: width 120 | anchors.centerIn: parent 121 | 122 | color: theme.backgroundColor 123 | radius: height / 2 124 | 125 | visible: false 126 | } 127 | 128 | DropShadow { 129 | anchors.fill: timerItemCircle 130 | 131 | radius: 18 132 | samples: 37 133 | color: timerItem.isSelected ? theme.tintColor : theme.textColor 134 | source: timerItemCircle 135 | transparentBorder: true 136 | } 137 | 138 | Image { 139 | anchors.centerIn: timerItemCircle 140 | 141 | width: 44 - (timerItemMouseArea.pressed ? 4 : 0) 142 | height: width 143 | 144 | source: "qrc:/assets/icons/timer" + (timerItem.isSelected ? "Green" : "White") + ".png" 145 | } 146 | 147 | MouseArea { 148 | id: timerItemMouseArea 149 | anchors.fill: parent 150 | 151 | onClicked: { 152 | root.state = "timer" 153 | } 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /qml/navigation/BottomNavigationItem.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.12 2 | import QtQuick.Controls 2.12 3 | 4 | Item { 5 | id: root 6 | 7 | property bool isSelected: false 8 | property string iconName: "" 9 | property int iconSize: 0 10 | 11 | property alias imageWidth: image.width 12 | property alias imageHeight: image.height 13 | 14 | signal clicked() 15 | 16 | Image { 17 | id: image 18 | 19 | anchors.centerIn: parent 20 | width: root.iconSize - (mouseArea.pressed ? 4 : 0) 21 | height: width 22 | 23 | source: "qrc:/assets/icons/" + root.iconName + (root.isSelected ? "Green" : "White") + ".png" 24 | } 25 | 26 | Rectangle { 27 | id: dot 28 | 29 | width: 4 30 | height: width 31 | visible: root.isSelected 32 | 33 | anchors { 34 | horizontalCenter: parent.horizontalCenter 35 | verticalCenter: parent.bottom 36 | } 37 | 38 | color: theme.tintColor 39 | radius: height / 2 40 | } 41 | 42 | MouseArea { 43 | id: mouseArea 44 | 45 | anchors.fill: parent 46 | 47 | onClicked: { 48 | if (!root.isSelected) { 49 | root.clicked() 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /qml/pages/ReadTagPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.12 2 | import com.scythestudio.nfc 1.0 3 | import "../" 4 | import "../controls" 5 | 6 | Item { 7 | id: root 8 | 9 | signal tagFound(string dishname, int seconds) 10 | 11 | Text { 12 | id: noTagInRangeText 13 | 14 | width: parent.width * 0.8 15 | visible: !nfcManager.hasTagInRange && nfcManager.actionType === NFCManager.Reading 16 | 17 | anchors { 18 | top: parent.top 19 | bottom: pulsatingImage.top 20 | horizontalCenter: parent.horizontalCenter 21 | } 22 | 23 | color: theme.textColor 24 | text: "There is no NFC tag in range.\nTouch tag to read message" 25 | horizontalAlignment: Text.AlignHCenter 26 | verticalAlignment: Text.AlignVCenter 27 | wrapMode: Text.WordWrap 28 | } 29 | 30 | PulsatingImage { 31 | id: pulsatingImage 32 | width: parent.width * 0.5 33 | height: width 34 | 35 | anchors.centerIn: parent 36 | imageSource: "qrc:/assets/icons/readTagWhite.png" 37 | } 38 | 39 | CustomPopup { 40 | id: popup 41 | 42 | width: parent.width 43 | height: parent.height 44 | 45 | onClosed: { 46 | if (isErrorPopup) { 47 | root.startReading() 48 | } 49 | } 50 | } 51 | 52 | onVisibleChanged: { 53 | if (visible) { 54 | startReading() 55 | } 56 | } 57 | 58 | Connections { 59 | target: nfcManager 60 | enabled: root.visible 61 | 62 | function onRecordChanged(record) { 63 | root.tagFound(record.dishName, record.seconds) 64 | } 65 | 66 | function onNfcError(error) { 67 | popup.show(error, true) 68 | } 69 | } 70 | 71 | function startReading() { 72 | console.log("start reading") 73 | pulsatingImage.startAnimation() 74 | nfcManager.startReading() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /qml/pages/SaveTagPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.12 2 | import com.scythestudio.nfc 1.0 3 | import "../" 4 | import "../controls" 5 | 6 | Item { 7 | id: root 8 | 9 | signal saved() 10 | 11 | states: [ 12 | State { 13 | name: "saveTag" 14 | PropertyChanges { 15 | target: saveDetailsItem 16 | visible: false 17 | } 18 | PropertyChanges { 19 | target: saveTagItem 20 | visible: true 21 | } 22 | } 23 | ] 24 | 25 | Item { 26 | id: saveDetailsItem 27 | anchors.fill: parent 28 | visible: true 29 | 30 | property string error: "" 31 | 32 | Column { 33 | width: parent.width * 0.8 34 | anchors.centerIn: parent 35 | 36 | spacing: 30 37 | 38 | Text { 39 | id: saveDetailsText 40 | width: parent.width 41 | 42 | color: theme.textColor 43 | text: "Input dish name and desired cooking time. It will be saved on tag" 44 | horizontalAlignment: Text.AlignHCenter 45 | wrapMode: Text.WordWrap 46 | } 47 | 48 | Rectangle { 49 | width: parent.width 50 | height: dishnameTextInput.implicitHeight 51 | 52 | color: "transparent" 53 | border { 54 | color: theme.textColor 55 | width: 1 56 | } 57 | 58 | Text { 59 | id: dishnameTextPlaceholderText 60 | anchors.fill: dishnameTextInput 61 | visible: dishnameTextInput.displayText == "" 62 | 63 | font: dishnameTextInput.font 64 | color: Qt.darker(theme.textColor) 65 | padding: dishnameTextInput.padding 66 | text: "Chicken" 67 | } 68 | 69 | TextInput { 70 | id: dishnameTextInput 71 | 72 | width: parent.width 73 | 74 | font.pixelSize: saveDetailsText.font.pixelSize * 2 75 | color: dishnameTextInput.focus ? theme.tintColor : theme.textColor 76 | cursorVisible: false 77 | padding: 10 78 | } 79 | } 80 | 81 | CustomTumbler { 82 | id: tumbler 83 | 84 | anchors.horizontalCenter: parent.horizontalCenter 85 | } 86 | 87 | CustomButton { 88 | anchors.horizontalCenter: parent.horizontalCenter 89 | text: "Save Tag" 90 | 91 | onClicked: { 92 | var dishname = dishnameTextInput.displayText 93 | var seconds = tumbler.getSeconds() 94 | 95 | if (dishname == "") { 96 | saveDetailsItem.error = "Dish name can not be empty" 97 | return 98 | } 99 | 100 | if (seconds <= 0) { 101 | saveDetailsItem.error = "You need to set cooking time" 102 | return 103 | } 104 | 105 | root.startSaving(dishname, seconds) 106 | } 107 | } 108 | 109 | Text { 110 | width: parent.width 111 | visible: saveDetailsItem.error != "" 112 | 113 | color: theme.errorColor 114 | text: saveDetailsItem.error 115 | horizontalAlignment: Text.AlignHCenter 116 | wrapMode: Text.WordWrap 117 | } 118 | } 119 | 120 | function resetInputFields() { 121 | root.state = "" 122 | saveDetailsItem.error = "" 123 | dishnameTextPlaceholderText.text = "Chicken" 124 | dishnameTextInput.text = "" 125 | tumbler.reset() 126 | nfcManager.stopDetecting() 127 | } 128 | } 129 | 130 | Item { 131 | id: saveTagItem 132 | anchors.fill: parent 133 | visible: false 134 | 135 | Text { 136 | id: noTagInRangeText 137 | 138 | width: parent.width * 0.8 139 | visible: !nfcManager.hasTagInRange && nfcManager.actionType === NFCManager.Writing 140 | 141 | anchors { 142 | top: parent.top 143 | bottom: pulsatingImage.top 144 | horizontalCenter: parent.horizontalCenter 145 | } 146 | 147 | color: theme.textColor 148 | text: "There is no NFC tag in range.\nTouch tag to save record" 149 | horizontalAlignment: Text.AlignHCenter 150 | verticalAlignment: Text.AlignVCenter 151 | wrapMode: Text.WordWrap 152 | } 153 | 154 | PulsatingImage { 155 | id: pulsatingImage 156 | width: parent.width * 0.5 157 | height: width 158 | 159 | anchors.centerIn: parent 160 | imageSource: "qrc:/assets/icons/saveTagWhite.png" 161 | } 162 | 163 | CustomButton { 164 | text: "Cancel" 165 | 166 | anchors { 167 | top: pulsatingImage.bottom 168 | topMargin: 50 169 | horizontalCenter: parent.horizontalCenter 170 | } 171 | 172 | onClicked: { 173 | saveDetailsItem.resetInputFields() 174 | } 175 | } 176 | } 177 | 178 | CustomPopup { 179 | id: popup 180 | 181 | width: parent.width 182 | height: parent.height 183 | 184 | onClosed: { 185 | if (isErrorPopup) { 186 | saveDetailsItem.resetInputFields() 187 | } else { 188 | root.saved() 189 | } 190 | } 191 | } 192 | 193 | onVisibleChanged: { 194 | saveDetailsItem.resetInputFields() 195 | } 196 | 197 | Connections { 198 | target: nfcManager 199 | enabled: root.visible 200 | 201 | function onNfcError(error) { 202 | popup.show(error, true) 203 | } 204 | 205 | function onWroteSuccessfully() { 206 | popup.show("NFC Tag Saved Successfully", false) 207 | } 208 | } 209 | 210 | function startSaving(dishname, seconds) { 211 | console.log("start saving") 212 | root.state = "saveTag" 213 | pulsatingImage.startAnimation() 214 | nfcManager.saveRecord(dishname, seconds) 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /qml/pages/TimerPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.12 2 | import QtQuick.Controls 2.12 3 | import QtMultimedia 5.12 4 | import com.scythestudio.nfc 1.0 5 | import "../" 6 | import "../controls" 7 | 8 | Item { 9 | id: root 10 | 11 | signal readButtonClicked 12 | signal saveButtonClicked 13 | 14 | state: "noTimer" 15 | states: [ 16 | State { 17 | name: "noTimer" 18 | PropertyChanges { 19 | target: noTimerItem 20 | visible: true 21 | } 22 | PropertyChanges { 23 | target: clockItem 24 | visible: false 25 | } 26 | PropertyChanges { 27 | target: alarmItem 28 | visible: false 29 | } 30 | }, 31 | State { 32 | name: "countdown" 33 | PropertyChanges { 34 | target: noTimerItem 35 | visible: false 36 | } 37 | PropertyChanges { 38 | target: clockItem 39 | visible: true 40 | } 41 | PropertyChanges { 42 | target: alarmItem 43 | visible: false 44 | } 45 | }, 46 | State { 47 | name: "alarm" 48 | PropertyChanges { 49 | target: noTimerItem 50 | visible: false 51 | } 52 | PropertyChanges { 53 | target: clockItem 54 | visible: false 55 | } 56 | PropertyChanges { 57 | target: alarmItem 58 | visible: true 59 | } 60 | } 61 | ] 62 | 63 | Item { 64 | id: noTimerItem 65 | anchors.fill: parent 66 | visible: true 67 | 68 | Column { 69 | width: parent.width * 0.8 70 | anchors.centerIn: parent 71 | 72 | spacing: 30 73 | 74 | Text { 75 | width: parent.width 76 | 77 | color: theme.textColor 78 | text: "There is no active timer. What do you wish to do?" 79 | horizontalAlignment: Text.AlignHCenter 80 | wrapMode: Text.WordWrap 81 | } 82 | 83 | Row { 84 | anchors.horizontalCenter: parent.horizontalCenter 85 | spacing: 20 86 | 87 | CustomButton { 88 | text: "Read Tag" 89 | 90 | onClicked: { 91 | root.readButtonClicked() 92 | } 93 | } 94 | 95 | CustomButton { 96 | text: "Save Tag" 97 | 98 | onClicked: { 99 | root.saveButtonClicked() 100 | } 101 | } 102 | } 103 | } 104 | } 105 | 106 | Item { 107 | id: clockItem 108 | 109 | anchors.fill: parent 110 | visible: false 111 | 112 | Column { 113 | width: parent.width * 0.8 114 | anchors { 115 | horizontalCenter: parent.horizontalCenter 116 | bottom: dishImageItem.top 117 | bottomMargin: 40 118 | } 119 | 120 | spacing: 30 121 | 122 | Text { 123 | id: dishInProgressText 124 | width: parent.width 125 | 126 | property string standardText: _.dishName + " in progress" 127 | 128 | color: theme.textColor 129 | text: standardText 130 | horizontalAlignment: Text.AlignHCenter 131 | wrapMode: Text.WordWrap 132 | 133 | Timer { 134 | repeat: true 135 | running: countdownTimer.running 136 | interval: countdownTimer.interval 137 | 138 | property int visibleDots: 0 139 | 140 | onTriggered: { 141 | if (visibleDots == 3) { 142 | dishInProgressText.text = dishInProgressText.standardText 143 | visibleDots = 0 144 | return 145 | } 146 | 147 | dishInProgressText.text += "." 148 | visibleDots++ 149 | } 150 | } 151 | } 152 | 153 | Text { 154 | width: parent.width 155 | 156 | color: theme.textColor 157 | font { 158 | pixelSize: dishInProgressText.font.pixelSize * 2 159 | bold: true 160 | } 161 | 162 | text: _.secondsAsHms() 163 | horizontalAlignment: Text.AlignHCenter 164 | wrapMode: Text.WordWrap 165 | } 166 | } 167 | 168 | Item { 169 | id: dishImageItem 170 | width: dishImage.maxSize 171 | height: width 172 | anchors.centerIn: parent 173 | 174 | Image { 175 | id: dishImage 176 | 177 | readonly property int standardSize: 100 178 | readonly property int maxSize: standardSize * 1.3 179 | 180 | width: standardSize 181 | height: width 182 | anchors.centerIn: parent 183 | 184 | source: "qrc:/assets/bake.png" 185 | 186 | SequentialAnimation on width { 187 | loops: Animation.Infinite 188 | running: countdownTimer.running 189 | 190 | PropertyAnimation { 191 | duration: 1000 192 | to: dishImage.standardSize 193 | } 194 | PropertyAnimation { 195 | duration: 1000 196 | to: dishImage.maxSize 197 | } 198 | } 199 | } 200 | } 201 | } 202 | 203 | Item { 204 | id: alarmItem 205 | anchors.fill: parent 206 | visible: false 207 | 208 | Column { 209 | width: parent.width * 0.8 210 | anchors.centerIn: parent 211 | 212 | spacing: 30 213 | 214 | Text { 215 | width: parent.width 216 | 217 | color: theme.textColor 218 | font { 219 | pixelSize: dishInProgressText.font.pixelSize * 2 220 | bold: true 221 | } 222 | text: _.dishName + " is ready!" 223 | horizontalAlignment: Text.AlignHCenter 224 | wrapMode: Text.WordWrap 225 | } 226 | 227 | CustomButton { 228 | anchors.horizontalCenter: parent.horizontalCenter 229 | text: "Dismiss alarm" 230 | 231 | onClicked: { 232 | console.log("alarm dismissed") 233 | alarmSoundEffect.stop() 234 | root.state = "noTimer" 235 | } 236 | } 237 | } 238 | } 239 | 240 | Timer { 241 | id: countdownTimer 242 | 243 | interval: 1000 244 | repeat: true 245 | running: _.seconds > 0 246 | 247 | onTriggered: { 248 | if (_.seconds > 0) { 249 | _.seconds-- 250 | 251 | if (_.seconds == 0) { 252 | console.log("ALARM") 253 | root.state = "alarm" 254 | alarmSoundEffect.play() 255 | } 256 | } 257 | } 258 | } 259 | 260 | SoundEffect { 261 | id: alarmSoundEffect 262 | 263 | loops: SoundEffect.Infinite 264 | source: "qrc:/assets/alarm.wav" 265 | } 266 | 267 | QtObject { 268 | id: _ 269 | 270 | property int seconds: 0 271 | property string dishName: "" 272 | 273 | function secondsAsHms() { 274 | var h = Math.floor(_.seconds / 3600) 275 | var m = Math.floor(_.seconds % 3600 / 60) 276 | var s = Math.floor(_.seconds % 3600 % 60) 277 | 278 | var hDisplay = h < 10 ? '0' + h : h 279 | var mDisplay = m < 10 ? '0' + m : m 280 | var sDisplay = s < 10 ? '0' + s : s 281 | 282 | return hDisplay + ':' + mDisplay + ':' + sDisplay 283 | } 284 | } 285 | 286 | onVisibleChanged: { 287 | if (visible) { 288 | // NFC detecting is not necessary on this page 289 | nfcManager.stopDetecting() 290 | } 291 | } 292 | 293 | function startCountdown(dishname, seconds) { 294 | _.dishName = dishname 295 | _.seconds = seconds 296 | root.state = "countdown" 297 | } 298 | } 299 | --------------------------------------------------------------------------------