diff --git a/.gitignore b/.gitignore index 56accb7..ebd8b13 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ *.autosave -build-LenaPi-Desktop-Debug -build-LenaPi-RasPi-Debug -build-LenaPi-RasPi-Release +build-LenaPi* LenaPi/LenaPi.pro.user.4.8-pre1 +musik diff --git a/LenaPi/.qmake.stash b/LenaPi/.qmake.stash new file mode 100644 index 0000000..8046afc --- /dev/null +++ b/LenaPi/.qmake.stash @@ -0,0 +1,24 @@ +QMAKE_CXX.QT_COMPILER_STDCXX = 201402L +QMAKE_CXX.QMAKE_GCC_MAJOR_VERSION = 10 +QMAKE_CXX.QMAKE_GCC_MINOR_VERSION = 2 +QMAKE_CXX.QMAKE_GCC_PATCH_VERSION = 1 +QMAKE_CXX.COMPILER_MACROS = \ + QT_COMPILER_STDCXX \ + QMAKE_GCC_MAJOR_VERSION \ + QMAKE_GCC_MINOR_VERSION \ + QMAKE_GCC_PATCH_VERSION +QMAKE_CXX.INCDIRS = \ + /usr/arm-linux-gnueabihf/include/c++/10 \ + /usr/arm-linux-gnueabihf/include/c++/10/arm-linux-gnueabihf \ + /usr/arm-linux-gnueabihf/include/c++/10/backward \ + /usr/lib/gcc-cross/arm-linux-gnueabihf/10/include \ + /usr/arm-linux-gnueabihf/include \ + /usr/include/arm-linux-gnueabihf \ + /usr/include +QMAKE_CXX.LIBDIRS = \ + /usr/lib/gcc-cross/arm-linux-gnueabihf/10 \ + /usr/arm-linux-gnueabihf/lib \ + /lib/arm-linux-gnueabihf \ + /lib \ + /usr/lib/arm-linux-gnueabihf \ + /usr/lib diff --git a/LenaPi/EnergySaver.cpp b/LenaPi/EnergySaver.cpp index 556dcbc..da9db13 100644 --- a/LenaPi/EnergySaver.cpp +++ b/LenaPi/EnergySaver.cpp @@ -4,16 +4,35 @@ #include #include #include +EnergySaver::~EnergySaver() +{ +#ifdef ANDROID + releaseAndroidLock(); +#endif +} + +void EnergySaver::init() +{ + auto saver = instance(); + saver->mIsAutoShutDownEnabled = false; +#ifdef ANDROID + saver->initAdroidLocks(); +#endif +} void EnergySaver::init(int interval, const QString &shutdownScript) { + auto saver = instance(); + saver->mIsAutoShutDownEnabled = true; QFileInfo script(shutdownScript); if(script.exists()){ - auto saver = instance(); saver->setShutdownScript(shutdownScript); saver->initTimer(interval*1000); saver->restartTimer(); } +#ifdef ANDROID + saver->initAdroidLocks(); +#endif } @@ -21,15 +40,32 @@ void EnergySaver::init(int interval, const QString &shutdownScript) EnergySaver *EnergySaver::instance() { static EnergySaver* inst; - if (inst == nullptr) + if (!inst) { inst = new EnergySaver(); } return inst; } +void EnergySaver::deactivate() +{ + mIsActive = false; + mTimer.stop(); + setAndroidLock(); +} + +void EnergySaver::activate() +{ + mIsActive = true; + restartTimer(); + releaseAndroidLock(); +} + void EnergySaver::restartTimer() { + // Energy saver is currently deactivated -> Do NOT start timer + if(!mIsActive) return; + if(mTimer.isActive()){ mTimer.stop(); } @@ -49,9 +85,59 @@ void EnergySaver::setShutdownScript(const QString &shutdownScript) mShutdownScript = shutdownScript; } +void EnergySaver::initAdroidLocks() +{ +#ifdef ANDROID + QAndroidJniObject activity = QAndroidJniObject::callStaticObjectMethod("org/qtproject/qt5/android/QtNative", "activity", "()Landroid/app/Activity;"); + if ( activity.isValid() ) + { + QAndroidJniObject serviceName = QAndroidJniObject::getStaticObjectField("android/content/Context","POWER_SERVICE"); + if ( serviceName.isValid() ) + { + QAndroidJniObject powerMgr = activity.callObjectMethod("getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;",serviceName.object()); + if ( powerMgr.isValid() ) + { + jint levelAndFlags = QAndroidJniObject::getStaticField("android/os/PowerManager","SCREEN_DIM_WAKE_LOCK"); + + QAndroidJniObject tag = QAndroidJniObject::fromString( "My Tag" ); + + m_wakeLock = powerMgr.callObjectMethod("newWakeLock", "(ILjava/lang/String;)Landroid/os/PowerManager$WakeLock;", levelAndFlags,tag.object()); + } + } + } +#endif +} + +void EnergySaver::setAndroidLock() +{ +#ifdef ANDROID + if ( m_wakeLock.isValid() ) + { + m_wakeLock.callMethod("acquire", "()V"); + qDebug() << "Locked device, can't go to standby anymore"; + } + else + { + assert( false ); + } +#endif +} + +void EnergySaver::releaseAndroidLock() +{ +#ifdef ANDROID + if ( m_wakeLock.isValid() ) + { + m_wakeLock.callMethod("release", "()V"); + qDebug() << "Unlocked device, can now go to standby"; + } +#endif +} + void EnergySaver::onTimeout() { + if(!mIsAutoShutDownEnabled) return; std::cout << "Shutting down."; #ifndef _DEBUG QProcess p; diff --git a/LenaPi/EnergySaver.h b/LenaPi/EnergySaver.h index 3e14fe4..fbfd503 100644 --- a/LenaPi/EnergySaver.h +++ b/LenaPi/EnergySaver.h @@ -4,27 +4,39 @@ #include #include +#ifdef ANDROID +#include +#endif + /** * @brief Class handling energy saving options. * - * Shut down device if no mouse input is detected and music player - * has not been active or a certain time interval. + * On Android devices, it locks the cpu shutdown while active. On other devices, it + * will shut down device on timeout if the options are set accordingly and a shutdown script + * is provided. * - * @todo For now this does only work for Lena's RasPi, where the - * shutdown script is positioned in a ceratin hardcoded path. - * Enable/disable energy saving option, timeout and path of - * shutdown script via config + * In the context of the LenaPi application, it will prevent cpu shutdown on android devices while playing + * music. On other devices, it will shutdown the device if no music has been playing and no mouse input was + * detected for a certain time intervall. */ class EnergySaver : public QObject { Q_OBJECT protected: - explicit EnergySaver(QObject *parent = nullptr) : QObject(parent) {} + using QObject::QObject; public: + ~EnergySaver(); + /** + * @brief Create instance if necessary. The instance will have auto shutdown disabeld. + * + * Used to prevent sleep on android devices. + */ + static void init(); /** * @brief Create instance if necessary, configure it and start timer. + * @param enabledAutoShutdown Defines whether device will shutdown on inactivity using shutdownScript * @param interval Timer interval in seconds * @param shutdownScript Path to shutdown script file * @see EnergySaver::instance @@ -43,7 +55,19 @@ public: public slots: /** - * @brief Restart shutdown timer, e.g. because of music player activiti + * @brief Deactivate energy saver, e.g., as music is currently playing + * + * Sets locks on adroid devics and stops shutdown-timer. + */ + void deactivate(); + /** + * @brief Active energy saver + * + * Releases locks on android devices and restarts shutdown-timer if shutdown option is set. + */ + void activate(); + /** + * @brief Restart shutdown timer, e.g. because of music player activity */ void restartTimer(); @@ -54,8 +78,31 @@ private: */ void initTimer(int interval); void setShutdownScript(const QString& shutdownScript); + /** + * @brief Initializes locking or Android devices + */ + void initAdroidLocks(); + /** + * @brief Sets locks on Android devices to prevent sleep. + * + * Used to prevent cpu sleep as otherwise music will stop. + */ + void setAndroidLock(); + /** + * @brief Releases locks on Android devices to allow the device's cpu to go into sleep mode. + */ + void releaseAndroidLock(); QTimer mTimer; QString mShutdownScript; + bool mIsActive{false}; + bool mIsAutoShutDownEnabled{false}; + +#ifdef ANDROID + /** + * @brief Prevent energy saving for Android devices + */ + QAndroidJniObject m_wakeLock; +#endif private slots: /** @@ -63,6 +110,7 @@ private slots: */ void onTimeout(); + }; #endif // ENERGYSAVER_H diff --git a/LenaPi/LenaPi.pro b/LenaPi/LenaPi.pro index 5750661..e603e36 100644 --- a/LenaPi/LenaPi.pro +++ b/LenaPi/LenaPi.pro @@ -1,16 +1,20 @@ TEMPLATE = app - -QT += qml quick +linux:android { +QT += androidextras +ANDROID_PERMISSIONS += android.permission.WAKE_LOCK +ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android-files +QMAKE_CXXFLAGS += -DANDROID=1 +} +QT += qml quick multimedia CONFIG += c++11 -LIBS += -lVLCQtCore -lVLCQtQml - +LIBS += SOURCES += main.cpp \ + controllers/MusicPlayer.cpp \ + controllers/StyleHandling.cpp \ models/NavigationListModel.cpp \ models/NavigationItemModel.cpp \ controllers/NavigationController.cpp \ models/UiStateModel.cpp \ - controllers/MusicController.cpp \ - models/MusicModel.cpp \ MouseEventSpy.cpp \ EnergySaver.cpp \ controllers/SettingsHandler.cpp @@ -41,14 +45,18 @@ else: unix:!android: target.path = /opt/$${TARGET}/bin !isEmpty(target.path): INSTALLS += target HEADERS += \ + controllers/MusicPlayer.h \ + controllers/StyleHandling.h \ + models/MusicPlayer.h \ models/NavigationListModel.h \ models/NavigationItemModel.h \ controllers/NavigationController.h \ models/UiStateModel.h \ - controllers/MusicController.h \ - models/MusicModel.h \ MouseEventSpy.h \ EnergySaver.h \ controllers/SettingsHandler.h INCLUDEPATH+=/usr/local/include + +DISTFILES += \ + android-files/AndroidManifest.xml diff --git a/LenaPi/LenaPi.pro.user b/LenaPi/LenaPi.pro.user index 430c2b1..73f8196 100644 --- a/LenaPi/LenaPi.pro.user +++ b/LenaPi/LenaPi.pro.user @@ -1,14 +1,14 @@ - + EnvironmentId - {76cb87c9-a04b-479b-87ff-1b3ddab073a7} + {baaf8c13-9b67-4e08-b264-88275a728682} ProjectExplorer.Project.ActiveTarget - 0 + 0 ProjectExplorer.Project.EditorSettings @@ -28,7 +28,7 @@ QmlJSGlobal - 2 + 2 UTF-8 false 4 @@ -37,6 +37,7 @@ true true 1 + false true false 0 @@ -45,282 +46,703 @@ 0 8 true + false 1 true true true + *.md, *.MD, Makefile false + true ProjectExplorer.Project.PluginSettings + + true + false + true + true + true + true + + + 0 + true true + Builtin.Questionable + + true + true + Builtin.DefaultTidyAndClazy + 4 + + + + true + ProjectExplorer.Project.Target.0 - Desktop - Desktop - {f9938f31-357c-4785-aa6e-21a3feac2ebf} - 0 - 0 - 0 + GenericLinuxOsType + armhf/raspi 10.0.1.146 + armhf/raspi 10.0.1.146 + {9f02fab2-7f72-4b24-bbd8-dbe33e863260} + 1 + 0 + 0 - /home/araemer/source/LenaPi/build-LenaPi-Desktop-Debug + 0 + /home/jmr/privat/src/LenaPi/build-LenaPi-Unnamed-Debug + /home/jmr/privat/src/LenaPi/build-LenaPi-Unnamed-Debug true - qmake - QtProjectManager.QMakeBuildStep - true - false - false - false + true - Make - Qt4ProjectManager.MakeStep - - false - - - false - 2 + 2 Build - + Build ProjectExplorer.BuildSteps.Build true - Make - Qt4ProjectManager.MakeStep - - true clean - - false - 1 + 1 Clean - + Clean ProjectExplorer.BuildSteps.Clean 2 false + + false - Debug - + Debug Qt4ProjectManager.Qt4BuildConfiguration 2 - true - /home/ar/source/LenaPi/build-LenaPi-Desktop-Release + /home/jmr/privat/src/LenaPi/build-LenaPi-Unnamed-Release + /home/jmr/privat/src/LenaPi/build-LenaPi-Unnamed-Release true - qmake - QtProjectManager.QMakeBuildStep - false - false - false - false + true - Make - Qt4ProjectManager.MakeStep - - false - - - false - 2 + 2 Build - + Build ProjectExplorer.BuildSteps.Build true - Make - Qt4ProjectManager.MakeStep - - true clean - - false - 1 + 1 Clean - + Clean ProjectExplorer.BuildSteps.Clean 2 false + + false - Release - + Release Qt4ProjectManager.Qt4BuildConfiguration 0 - true - /home/ar/source/LenaPi/build-LenaPi-Desktop-Profile + 0 + /home/jmr/privat/src/LenaPi/build-LenaPi-Unnamed-Profile + /home/jmr/privat/src/LenaPi/build-LenaPi-Unnamed-Profile true - qmake - QtProjectManager.QMakeBuildStep - true - false - true - false + true - Make - Qt4ProjectManager.MakeStep - - false - - - false - 2 + 2 Build - + Build ProjectExplorer.BuildSteps.Build true - Make - Qt4ProjectManager.MakeStep - - true clean - - false - 1 + 1 Clean - + Clean ProjectExplorer.BuildSteps.Clean 2 false + + false - Profile - + Profile Qt4ProjectManager.Qt4BuildConfiguration 0 - true + 0 - 3 + 3 - 0 + + true + RemoteLinux.CheckForFreeDiskSpaceStep + + + + + / + 5242880 + + + + + true + RemoteLinux.KillAppStep + + + + + + + + + true + RemoteLinux.RsyncDeployStep + + + + + + + -av + + 3 Deploy - + Deploy ProjectExplorer.BuildSteps.Deploy 1 - Deploy locally - - ProjectExplorer.DefaultDeployConfiguration + + false + DeployToGenericLinux - 1 - + 1 - false - false - 1000 - + true true - - false - false - false - false - true - 0.01 - 10 - true - 1 - 25 - - 1 true - false - true - valgrind - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 2 - LenaPi - LenaPi2 - Qt4ProjectManager.Qt4RunConfiguration:/home/ar/source/LenaPi/LenaPi/LenaPi.pro - LenaPi.pro - - 3768 + ProjectExplorer.CustomExecutableRunConfiguration + + false + true + false + true + + + true + true + true + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + + 1 + + RemoteLinux.CustomRunConfig + + 1 + false + true + false + true + :0.0 + + 2 + + + + ProjectExplorer.Project.Target.1 + + Android.Device.Type + Android Qt 5.15.2 (android) Clang Multi-Abi + Android Qt 5.15.2 (android) Clang Multi-Abi + {ec9ac26e-ce2e-4aaf-b544-ccaec5b0b918} + 1 + 0 + 0 + + 0 + /home/jmr/privat/src/LenaPi/build-LenaPi-Android_Qt_5_15_2_android_Clang_Multi_Abi-Debug + /home/jmr/privat/src/LenaPi/build-LenaPi-Android_Qt_5_15_2_android_Clang_Multi_Abi-Debug + + + true + QtProjectManager.QMakeBuildStep + false + + armeabi-v7a + + + + true + Qt4ProjectManager.MakeStep + + + true + Copy application data + Qt4ProjectManager.AndroidPackageInstallationStep + + + android-31 + + true + Build Android APK + QmakeProjectManager.AndroidBuildApkStep + false + + 4 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + clean + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + 0 + + + /home/jmr/privat/src/LenaPi/build-LenaPi-Android_Qt_5_15_2_android_Clang_Multi_Abi-Release + /home/jmr/privat/src/LenaPi/build-LenaPi-Android_Qt_5_15_2_android_Clang_Multi_Abi-Release + + + true + QtProjectManager.QMakeBuildStep + false + + armeabi-v7a + + + + true + Qt4ProjectManager.MakeStep + + + true + Copy application data + Qt4ProjectManager.AndroidPackageInstallationStep + + + android-31 + + true + Build Android APK + QmakeProjectManager.AndroidBuildApkStep + false + + 4 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + clean + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Release + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 0 + 0 + + + 0 + /home/jmr/privat/src/LenaPi/build-LenaPi-Android_Qt_5_15_2_android_Clang_Multi_Abi-Profile + /home/jmr/privat/src/LenaPi/build-LenaPi-Android_Qt_5_15_2_android_Clang_Multi_Abi-Profile + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + + true + Copy application data + Qt4ProjectManager.AndroidPackageInstallationStep + + + android-31 + + true + Build Android APK + QmakeProjectManager.AndroidBuildApkStep + false + + 4 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + clean + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 0 + 0 + 0 + + 3 + + + + true + <b>Deploy to Android device</b> + Qt4ProjectManager.AndroidDeployQtStep + + 1 + Deploy + Deploy + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + Qt4ProjectManager.AndroidDeployConfiguration2 + + 1 + + + arm64-v8a + armeabi-v7a + armeabi + + 58009fc2 + 28 + + + dwarf + + cpu-cycles + + -F + true + true + true + + + + + + + + 0 + + Qt4ProjectManager.AndroidRunConfiguration:/home/jmr/privat/src/LenaPi/LenaPi/LenaPi.pro + /home/jmr/privat/src/LenaPi/LenaPi/LenaPi.pro + false + true + false + true + + 1 + + + + ProjectExplorer.Project.Target.2 + + Desktop + desktop + desktop + {73308c85-3272-4fe8-8c9f-0eb72285644f} + 1 + 0 + 0 + + 0 + /home/jmr/privat/src/LenaPi/build-LenaPi-desktop-Debug + /home/jmr/privat/src/LenaPi/build-LenaPi-desktop-Debug + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + clean + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + 0 + + + /home/jmr/privat/src/LenaPi/build-LenaPi-desktop-Release + /home/jmr/privat/src/LenaPi/build-LenaPi-desktop-Release + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + clean + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Release + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 0 + + + 0 + /home/jmr/privat/src/LenaPi/build-LenaPi-desktop-Profile + /home/jmr/privat/src/LenaPi/build-LenaPi-desktop-Profile + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + clean + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 0 + 0 + + 3 + + + 0 + Deploy + Deploy + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + ProjectExplorer.DefaultDeployConfiguration + + 1 + + dwarf + + cpu-cycles + + -F + true + true + true + + 2 + + Qt4ProjectManager.Qt4RunConfiguration:/home/jmr/privat/src/LenaPi/LenaPi/LenaPi.pro + /home/jmr/privat/src/LenaPi/LenaPi/LenaPi.pro false true true - false false true - - /home/araemer/source/LenaPi/build-LenaPi-Desktop-Debug - 1 + 1 ProjectExplorer.Project.TargetCount - 1 + 3 ProjectExplorer.Project.Updater.FileVersion - 20 + 22 Version - 20 + 22 diff --git a/LenaPi/MediaProgress.qml b/LenaPi/MediaProgress.qml index 5b6b441..d87746d 100644 --- a/LenaPi/MediaProgress.qml +++ b/LenaPi/MediaProgress.qml @@ -1,51 +1,52 @@ import QtQuick 2.0 import QtQuick.Controls 1.4 +import QtQuick.Layouts 1.3 import QtQuick.Controls.Styles 1.4 -Item { +ColumnLayout { id: container property var model - // manually set height - Component.onCompleted: height = childrenRect.height + spacing: StyleSpacings.tinySpacing - Label{ - anchors.left: progress.left - anchors.bottom: progress.top - font.pointSize: 9 - color: "grey" - text: model.pMediaTitle - } + RowLayout{ + Label{ + font.pointSize: 9 + color: "grey" + text: model.pMediaTitle + } + Item{ + // spacer + Layout.fillWidth: true + } - Label{ - anchors.right: progress.right - anchors.bottom: progress.top - font.pointSize: 9 - color: "grey" - text: model.pTime + " / " + model.pMediaLength + Label{ + font.pointSize: 9 + color: "grey" + text: model.pTime + " / " + model.pMediaLength + } } ProgressBar{ id: progress - anchors.top: parent.top - anchors.left: parent.left + Layout.fillWidth: true value: model.pProgress style:ProgressBarStyle { background: Rectangle { - radius: 5 + radius: StyleSizes.progressBackgroundRadius color: "white" border.color: "grey" - border.width: 1 + border.width: StyleSizes.progressBackgroundBorderWidth implicitWidth: container.width - implicitHeight: 10 + implicitHeight: StyleSizes.progressBackgroundDefaultHeight } progress: Rectangle { color: "blue" border.color: "blue" - radius: 5 - implicitHeight: 8 + radius: StyleSizes.progressBackgroundRadius + implicitHeight: StyleSizes.progressBarDefaultHeight } } } diff --git a/LenaPi/MouseEventSpy.cpp b/LenaPi/MouseEventSpy.cpp index 26eb916..95ade23 100644 --- a/LenaPi/MouseEventSpy.cpp +++ b/LenaPi/MouseEventSpy.cpp @@ -12,7 +12,7 @@ void MouseEventSpy::init() MouseEventSpy* MouseEventSpy::instance() { static MouseEventSpy* inst; - if (inst == nullptr) + if (!inst) { inst = new MouseEventSpy(); QGuiApplication* app = qGuiApp; diff --git a/LenaPi/MusicPlayer.qml b/LenaPi/MusicPlayer.qml index a34d213..15cf60b 100644 --- a/LenaPi/MusicPlayer.qml +++ b/LenaPi/MusicPlayer.qml @@ -5,7 +5,7 @@ import QtGraphicalEffects 1.0 Item{ id: container - property int margins: 20 + property int margins: StyleMargins.defaultMargin RoundImageButton{ id: backNavigation @@ -43,9 +43,9 @@ Item{ Image{ id: cover anchors.centerIn: parent - height: parent.height-10 + height: parent.height-StyleMargins.smallMargin width: height - source: musicModel.pCurrentItem.pImageSource + source: musicModel.pCoverImageSource fillMode: Image.PreserveAspectCrop layer.enabled: true layer.effect: OpacityMask{ @@ -64,12 +64,9 @@ Item{ margins: container.margins topMargin: closeAppButton.visible ? 2*container.margins : container.margins } - from: 34 // we cannot hear anything if lower than 35% - to: 100 - stepSize: 1 - value: musicModel.pAudioVolume + value: musicModel.volume onValueChanged: { - musicModel.pAudioVolume = value; + musicModel.volume = value; } } PlayerControlPannel { diff --git a/LenaPi/MyScrollView.qml b/LenaPi/MyScrollView.qml index 4d3e6e8..63245eb 100644 --- a/LenaPi/MyScrollView.qml +++ b/LenaPi/MyScrollView.qml @@ -6,24 +6,24 @@ ScrollView{ style: ScrollViewStyle{ transientScrollBars: true handle: Item { - implicitWidth: 16 - implicitHeight: 8 + implicitWidth: StyleSizes.scrollHandleWidth + implicitHeight: StyleSizes.scrollHandleHeight Rectangle { color: "blue" border.color: "black" radius: height/2 anchors.fill: parent - anchors.topMargin: 1 - anchors.leftMargin: 1 - anchors.rightMargin: 1 - anchors.bottomMargin: 1 + anchors.topMargin: StyleMargins.scrollHandleMargin + anchors.leftMargin: StyleMargins.scrollHandleMargin + anchors.rightMargin: StyleMargins.scrollHandleMargin + anchors.bottomMargin: StyleMargins.scrollHandleMargin } } scrollBarBackground: Rectangle{ color: "transparent" border.color: "grey" - border.width: 1 - height: 8 + border.width: StyleSizes.scrollHandleBorderWidth + height: StyleSizes.scrollHandleHeight radius: height/2 } decrementControl: Item{} diff --git a/LenaPi/Navigation.qml b/LenaPi/Navigation.qml index 5251493..8e6f283 100644 --- a/LenaPi/Navigation.qml +++ b/LenaPi/Navigation.qml @@ -1,11 +1,12 @@ import QtQuick 2.0 +import QtQuick.Controls 2.4 /** * @brief Navigation view containing list view displaying artists or genres and albums */ Item { id: container - property int margins: 20 + property int margins: StyleMargins.defaultMargin RoundImageButton{ id: back @@ -35,21 +36,27 @@ Item { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - height: 210 + height: StyleSizes.navigationListHeight color: "#99ffffff" + Label{ + text: debug.pDebugOutput + anchors.centerIn: parent + visible: text !== "" + } + ListView{ id: circleList anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: 20 + anchors.margins: StyleMargins.defaultMargin - height: parent.height - 40 + height: parent.height - 2*StyleMargins.defaultMargin model: navigationList.pModelItems - spacing: 20 + spacing: StyleSpacings.defaultSpacing orientation: ListView.Horizontal delegate: NavigationListDelegate{ id: delegate diff --git a/LenaPi/NavigationListDelegate.qml b/LenaPi/NavigationListDelegate.qml index 94b8919..aaff20a 100644 --- a/LenaPi/NavigationListDelegate.qml +++ b/LenaPi/NavigationListDelegate.qml @@ -14,11 +14,11 @@ ItemDelegate{ property alias imageSource: contentImage.source - padding: 5 + padding: StylePaddings.defaultPadding background: Rectangle{ id: background - implicitWidth: 150 + implicitWidth: StyleSizes.navigationDelegateDefaultSize implicitHeight: implicitWidth radius: container.isCircleDelegate ? height/2 : 0 color: "blue" @@ -26,7 +26,7 @@ ItemDelegate{ id: contentImage source: "qrc:/default_image" anchors.fill: parent - anchors.margins: 5 + anchors.margins: StyleMargins.tinyMargin fillMode: Image.PreserveAspectCrop layer.enabled: true diff --git a/LenaPi/PlayerButtons.qml b/LenaPi/PlayerButtons.qml index 8fe8b00..07caed0 100644 --- a/LenaPi/PlayerButtons.qml +++ b/LenaPi/PlayerButtons.qml @@ -1,62 +1,58 @@ import QtQuick 2.0 +import QtQuick.Layouts 1.3 -Item { +RowLayout{ id: container property var model - property var spacing: 20 - Row{ - id: buttons - anchors.centerIn: parent - spacing: container.spacing + property var spacing: StyleSpacings.defaultSpacing - RoundImageButton{ - id: previous - anchors.verticalCenter: parent.verticalCenter + RoundImageButton{ + id: previous + Layout.alignment: Qt.AlignVCenter - diameter: 60 - imageSource: "qrc:/icon_previous" + diameter: StyleSizes.smallPlayerButtonSize //60 + imageSource: "qrc:/icon_previous" - enabled: model.pHasPrevious + enabled: model.pHasPrevious - onClicked:{ - model.playPrevious(); - } + onClicked:{ + model.playPrevious(); } - RoundImageButton{ - id: playPause - anchors.verticalCenter: parent.verticalCenter - diameter: 80 - imageSource: model.pIsPlaying ? "qrc:/icon_pause" : "qrc:/icon_play" + } + RoundImageButton{ + id: playPause + Layout.alignment: Qt.AlignVCenter + diameter: StyleSizes.largePlayerButtonSize + imageSource: model.pIsPlaying ? "qrc:/icon_pause" : "qrc:/icon_play" - onClicked:{ - model.playPause(); - } + onClicked:{ + model.playPause(); } - RoundImageButton{ - id: stop - anchors.verticalCenter: parent.verticalCenter + } + RoundImageButton{ + id: stop + Layout.alignment: Qt.AlignVCenter - diameter: 60 - imageSource: "qrc:/icon_stop" + diameter: StyleSizes.smallPlayerButtonSize + imageSource: "qrc:/icon_stop" - enabled: model.pIsPlaying + enabled: model.pIsPlaying - onClicked:{ - model.stopMusic(); - } + onClicked:{ + model.stopMusic(); } - RoundImageButton{ - id: next - anchors.verticalCenter: parent.verticalCenter + } + RoundImageButton{ + id: next + Layout.alignment: Qt.AlignVCenter - diameter: 60 - imageSource: "qrc:/icon_next" + diameter: StyleSizes.smallPlayerButtonSize + imageSource: "qrc:/icon_next" - enabled: model.pHasNext + enabled: model.pHasNext - onClicked:{ - model.playNext(); - } + onClicked:{ + model.playNext(); } - } //Row + } } diff --git a/LenaPi/PlayerControlPannel.qml b/LenaPi/PlayerControlPannel.qml index e438945..9eb4ff2 100644 --- a/LenaPi/PlayerControlPannel.qml +++ b/LenaPi/PlayerControlPannel.qml @@ -1,4 +1,5 @@ import QtQuick 2.0 +import QtQuick.Layouts 1.3 Rectangle { id: container @@ -6,28 +7,29 @@ Rectangle { property int margins color: "#99ffffff" - height: 140 + height: content.height + 2* margins //StyleSizes.playerControlPanelHeight - MediaProgress{ - id: progress - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: container.margins - model: container.model - // might require height for labels to be shown on RasPi (Qt 5.10) - //2019-04-04: defined in MediaProgress.qml -> test on rasPi - } - - PlayerButtons{ - id: buttons + ColumnLayout{ + id: content + spacing: StyleSpacings.smallSpacing + anchors.margins: container.margins anchors.left: parent.left anchors.right: parent.right - anchors.top: progress.bottom - anchors.bottom: parent.bottom - model: container.model - spacing: 20 + MediaProgress{ + id: progress + + model: container.model + } + + PlayerButtons{ + id: buttons + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + Layout.fillHeight: true + model: container.model + spacing: StyleSpacings.defaultSpacing + } } } diff --git a/LenaPi/RoundImageButton.qml b/LenaPi/RoundImageButton.qml index 5334465..e2453c3 100644 --- a/LenaPi/RoundImageButton.qml +++ b/LenaPi/RoundImageButton.qml @@ -8,7 +8,7 @@ Button { id: container property alias imageSource: image.source // default button diameter -> default width, readonly - readonly property int defaultDiameter: 65 + readonly property int defaultDiameter: StyleSizes.roundButtonDefaultSize // button diameter -> width property int diameter: defaultDiameter // diameter of content image -> width @@ -16,7 +16,7 @@ Button { background: Rectangle{ - border.width: 2 + border.width: StyleSizes.roundButtonBorderWidth border.color: "grey" color: "white" diff --git a/LenaPi/VolumeSlider.qml b/LenaPi/VolumeSlider.qml index 1ca6c9b..6b51aca 100644 --- a/LenaPi/VolumeSlider.qml +++ b/LenaPi/VolumeSlider.qml @@ -7,7 +7,7 @@ ColumnLayout { property alias to: slider.to property alias stepSize: slider.stepSize property alias value: slider.value - spacing: 5 + spacing: StyleSpacings.tinySpacing RoundImageButton{ id: increaseButton imageSource: "qrc:///icon_increase_volume" @@ -18,9 +18,9 @@ ColumnLayout { Layout.fillHeight: true Layout.alignment: Qt.AlignHCenter orientation: Qt.Vertical - from: 34 // we cannot hear anything if lower than 35% + from: 0 to: 100 - stepSize: 1 + stepSize: 2 value: 50 } RoundImageButton{ diff --git a/LenaPi/android-files/AndroidManifest.xml b/LenaPi/android-files/AndroidManifest.xml new file mode 100644 index 0000000..e0c143a --- /dev/null +++ b/LenaPi/android-files/AndroidManifest.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LenaPi/android-files/res/drawable-hdpi/icon.png b/LenaPi/android-files/res/drawable-hdpi/icon.png new file mode 100644 index 0000000..b22cf59 Binary files /dev/null and b/LenaPi/android-files/res/drawable-hdpi/icon.png differ diff --git a/LenaPi/android-files/res/drawable-ldpi/icon.png b/LenaPi/android-files/res/drawable-ldpi/icon.png new file mode 100644 index 0000000..7c9ca0f Binary files /dev/null and b/LenaPi/android-files/res/drawable-ldpi/icon.png differ diff --git a/LenaPi/android-files/res/drawable-mdpi/icon.png b/LenaPi/android-files/res/drawable-mdpi/icon.png new file mode 100644 index 0000000..57a20d1 Binary files /dev/null and b/LenaPi/android-files/res/drawable-mdpi/icon.png differ diff --git a/LenaPi/android-files/res/drawable-xhdpi/icon.png b/LenaPi/android-files/res/drawable-xhdpi/icon.png new file mode 100644 index 0000000..3ebfa96 Binary files /dev/null and b/LenaPi/android-files/res/drawable-xhdpi/icon.png differ diff --git a/LenaPi/android-files/res/drawable-xxhdpi/icon.png b/LenaPi/android-files/res/drawable-xxhdpi/icon.png new file mode 100644 index 0000000..aaf15bf Binary files /dev/null and b/LenaPi/android-files/res/drawable-xxhdpi/icon.png differ diff --git a/LenaPi/android-files/res/drawable-xxxhdpi/icon.png b/LenaPi/android-files/res/drawable-xxxhdpi/icon.png new file mode 100644 index 0000000..c8cf6cb Binary files /dev/null and b/LenaPi/android-files/res/drawable-xxxhdpi/icon.png differ diff --git a/LenaPi/controllers/MusicController.cpp b/LenaPi/controllers/MusicController.cpp deleted file mode 100644 index 4b51a00..0000000 --- a/LenaPi/controllers/MusicController.cpp +++ /dev/null @@ -1,72 +0,0 @@ -#include "MusicController.h" - -#include -#include -#include -#include "EnergySaver.h" - -MusicController::MusicController(QObject *parent) : QObject(parent) -{ - mVlc = new VlcInstance(VlcCommon::args(), this); - mModel = new MusicModel(mVlc, this); - mPlayer = new VlcMediaListPlayer(mVlc); - mVlcAudio = new VlcAudio(mPlayer->mediaPlayer()); - - connect(mModel, &MusicModel::navigateTo, this, &MusicController::onNavigationRequest); - - //connect(mModel, &MusicModel::play, mPlayer, &VlcMediaListPlayer::play); - connect(mModel, &MusicModel::play, [this](){ - mVlcAudio->setVolume(mModel->getAudioVolume()); - mPlayer->play(); - }); - connect(mModel, &MusicModel::stop, mPlayer, &VlcMediaListPlayer::stop); - connect(mModel, &MusicModel::previous, mPlayer, &VlcMediaListPlayer::previous); - connect(mModel, &MusicModel::next, mPlayer, &VlcMediaListPlayer::next); - connect(mModel, &MusicModel::pause, mPlayer->mediaPlayer(), &VlcMediaPlayer::pause); - connect(mModel, &MusicModel::audioVolumeChanged, mVlcAudio, &VlcAudio::setVolume); - - connect(mPlayer, SIGNAL(nextItemSet(VlcMedia*)), mModel, SLOT(onNextMediaSet(VlcMedia*))); - connect(mPlayer->mediaPlayer(), &VlcMediaPlayer::lengthChanged, mModel, &MusicModel::onLengthChanged); - connect(mPlayer->mediaPlayer(), &VlcMediaPlayer::timeChanged, mModel, &MusicModel::onTimeChanged); - - // hand over player signals to energy saver in order to determine player activity. - connect(mPlayer->mediaPlayer(), &VlcMediaPlayer::timeChanged, EnergySaver::instance(), &EnergySaver::restartTimer); -} - -MusicController::~MusicController() -{ - mPlayer->deleteLater(); - mVlcAudio->deleteLater(); -} - -void MusicController::initPlayer(NavigationItemModel *item) -{ - if(item != mModel->getCurrentItem()){ - mPlayer->stop(); - mModel->init(item); - } - if(!mIsMediaListSet){ - mPlayer->setMediaList(mModel->getMedia()); - mIsMediaListSet = true; - } -} - -void MusicController::setContext(QQmlContext *context) -{ - mContext = context; - setContextProperties(); -} - -void MusicController::setContextProperties() -{ - if(!mContext) return; - mContext->setContextProperty("musicModel", mModel); -} - -void MusicController::onNavigationRequest(NavigationItemModel *item) -{ - if(mModel->isPlaying()) { - mModel->playPause(); - } - emit navigateTo(item); -} diff --git a/LenaPi/controllers/MusicController.h b/LenaPi/controllers/MusicController.h deleted file mode 100644 index 4bdf01f..0000000 --- a/LenaPi/controllers/MusicController.h +++ /dev/null @@ -1,52 +0,0 @@ -#ifndef MUSICCONTROLLER_H -#define MUSICCONTROLLER_H - -#include -#include -#include - -#include -#include -#include -#include - -class MusicModel; - -class MusicController : public QObject -{ - Q_OBJECT - -signals: - void navigateTo(NavigationItemModel* item); - -public: - MusicController(QObject *parent = Q_NULLPTR); - ~MusicController(); - - void initPlayer(NavigationItemModel* item); - - void setContext(QQmlContext* context); - -private: - void setContextProperties(); - - QQmlContext* mContext = Q_NULLPTR; - - MusicModel* mModel = Q_NULLPTR; - - VlcInstance* mVlc = Q_NULLPTR; - VlcMediaListPlayer* mPlayer = Q_NULLPTR; - VlcAudio* mVlcAudio = Q_NULLPTR; - - bool mIsMediaListSet = false; - - private slots: - /** - * @brief Stop player if necessary and forward signal navigatTo - * @param item target of navigation request - * @see navigateTo(NavigationItemModel* item); - */ - void onNavigationRequest(NavigationItemModel* item); -}; - -#endif // MUSICCONTROLLER_H diff --git a/LenaPi/controllers/MusicPlayer.cpp b/LenaPi/controllers/MusicPlayer.cpp new file mode 100644 index 0000000..abbd9df --- /dev/null +++ b/LenaPi/controllers/MusicPlayer.cpp @@ -0,0 +1,165 @@ +#include "MusicPlayer.h" +#include +#include +#include + + +MusicPlayer::MusicPlayer(QObject *parent) : QMediaPlayer(parent) +{ + // init audio + /// @todo remove magic number + setVolume(50); + // relay signal to qml + connect(this, &QMediaPlayer::stateChanged, this, &MusicPlayer::isPlayingChanged); + connect(this, &QMediaPlayer::currentMediaChanged, this, [this](const QMediaContent&){ + // notify qml to refresh enable state of next and previous button + emit hasNextChanged(); + emit hasPreviousChanged(); + }); + connect(this, &QMediaPlayer::positionChanged, this, [this](qint64){ + emit progressChanged(); + }); + connect(this, &QMediaPlayer::durationChanged, this, [this](qint64){ + emit progressChanged(); + emit mediaLengthChanged(); + }); +} + +void MusicPlayer::init(NavigationItemModel *item) +{ + if(mCurrentItem == item){ + return; + } + mCurrentItem = item; + emit coverImageSourceChanged(); + + clearPlaylist(); + + readMedia(mCurrentItem->getPath()); +} + +void MusicPlayer::navigateBack() +{ + emit navigateTo(mCurrentItem); +} + +void MusicPlayer::playPause() +{ + if(isPlaying()) + pause(); + else + play(); +} + +void MusicPlayer::stopMusic() +{ + if(isPlaying()){ + stop(); + resetPlaylistToFirstTrack(); + } +} + +void MusicPlayer::playNext() +{ + if(!hasNext()) return; // checks if playlist exists + playlist()->next(); +} + +void MusicPlayer::playPrevious() +{ + if(!hasPrevious()) return; // checks if playlist exists + playlist()->previous(); +} + +void MusicPlayer::resetPlaylistToFirstTrack() +{ + if(!playlist() && !playlist()->isEmpty()) return; + playlist()->setCurrentIndex(0); +} + +bool MusicPlayer::isPlaying() const +{ + return state() == State::PlayingState; +} + +bool MusicPlayer::hasNext() const +{ + if(!playlist()) return false; + const auto nextIndex = playlist()->nextIndex(); + /* player yields -1 as next index while playing last track + */ + return nextIndex >=0 && nextIndex < playlist()->mediaCount(); +} + +bool MusicPlayer::hasPrevious() const +{ + if(!playlist()) return false; + const auto previousIndex = playlist()->previousIndex(); + /* player inits with currentIndex -1 and hence previousIndex = last index + * until started. Yet, previousButton should initially be disabled. + */ + return previousIndex >= 0 && previousIndex < playlist()->currentIndex(); +} + +double MusicPlayer::getProgress() const +{ + if(duration() <= 0 || position() < 0){ + return 0.0; + } + return (double)position()/duration(); +} + +QString MusicPlayer::getMediaTitle() const +{ + return metaData("Title").toString(); +} + +QString MusicPlayer::getMediaDuration() const +{ + return millisecondsToString(duration()); +} + +QString MusicPlayer::getPosition() const +{ + return millisecondsToString(position()); +} + +void MusicPlayer::clearPlaylist() +{ + if(playlist()){ + playlist()->clear(); + } +} + +void MusicPlayer::readMedia(const QString& path) +{ + auto dir = QDir(path); + if(!dir.exists()) return; + + // create playlist if necessary + auto playList = playlist(); + if(!playList){ + playList = new QMediaPlaylist(this); + setPlaylist(playList); + } + + // add audio files to playlist + dir.setNameFilters(SettingsHandler::getAudioFileNameFilters()); + auto files = dir.entryInfoList(QDir::Files); + for(auto file:files){ + auto fileName = file.absoluteFilePath(); + auto fileUrl = QUrl::fromLocalFile(fileName); + playList->addMedia(QMediaContent(fileUrl)); + } +} + +QString MusicPlayer::millisecondsToString(int timeInMilliseconds) const +{ + int seconds = timeInMilliseconds/1000; // ms to secons + int minutes = seconds/60; //seconds to minutes + seconds %= 60; // seconds remaining after subtracting minutes + // format according to [M]M:ss + // at leading zero to seconds if necessary + QString secStr = (seconds < 10) ? "0"+QString::number(seconds) : QString::number(seconds); + return QString::number(minutes) + ":" + secStr; +} diff --git a/LenaPi/controllers/MusicPlayer.h b/LenaPi/controllers/MusicPlayer.h new file mode 100644 index 0000000..2b5e8c3 --- /dev/null +++ b/LenaPi/controllers/MusicPlayer.h @@ -0,0 +1,143 @@ +#ifndef MUSICPLAYER_H +#define MUSICPLAYER_H + +#include +#include +#include + +/** + * @brief MusicPlayer providing interface to QML and Navigation. + * + * Adds media from directory referenced by the current navigation item to + * playlist and provides interface to QML MusicPlayer. + */ +class MusicPlayer : public QMediaPlayer +{ + Q_OBJECT + + Q_PROPERTY(QUrl pCoverImageSource READ getCoverImageSource NOTIFY coverImageSourceChanged) + Q_PROPERTY(bool pHasNext READ hasNext NOTIFY hasNextChanged) + Q_PROPERTY(bool pHasPrevious READ hasPrevious NOTIFY hasPreviousChanged) + Q_PROPERTY(bool pIsPlaying READ isPlaying NOTIFY isPlayingChanged) + Q_PROPERTY(double pProgress READ getProgress NOTIFY progressChanged) + Q_PROPERTY(QString pMediaLength READ getMediaDuration NOTIFY mediaLengthChanged) + Q_PROPERTY(QString pTime READ getPosition NOTIFY progressChanged) + Q_PROPERTY(QString pMediaTitle READ getMediaTitle NOTIFY metaDataChanged) + +signals: + void navigateTo(NavigationItemModel *item); + void coverImageSourceChanged(); + void hasPreviousChanged(); + void hasNextChanged(); + void isPlayingChanged(); + void progressChanged(); + void mediaLengthChanged(); + +public: + MusicPlayer(QObject *parent = Q_NULLPTR); + ~MusicPlayer() =default; + + /** + * @brief Init playlist with media from directory referenced by item. + * @param item Navigation item representing a leaf + * @see void readMedia(const QString& path) + */ + void init(NavigationItemModel* item); + + /** + * @brief Get path to media cover image. + * @return Path to media cover image. + */ + inline const QString& getCoverImageSource() const {return mCurrentItem->getImageSource();} + /** + * @brief Indicates whether the media player is currently playing a track. + * @return true if media player is playing a track. + */ + bool isPlaying() const; + /** + * @brief Indicates whether there is a next item in the playlist. + * @return True if there is a next item in the playlist + * Does not wrap. + */ + bool hasNext() const; + /** + * @brief Indicates whether there is a previous item in the playlist. + * @return True if there is a previous item in the playlist + * Does not wrap. + */ + bool hasPrevious() const; + + /** + * @brief Get media progress as value in between 0 and 1 + * @return Media progress + * 0 corresponds to the beginning and 1 to the end of the current track. + */ + double getProgress() const; + + /** + * @brief Get current media title extracted from media meta data (id3 tags) + * @return Current media title + */ + QString getMediaTitle() const; + /** + * @brief Get duration of the current track + * @return Duration of the current track formatted as string + * @see QMediaPlayer::duration() + * @see millisecondsToString(int timeInMilliseconds) const + */ + QString getMediaDuration() const; + /** + * @brief Get current position within the track + * @return Postion within the track formatted as string + * @see QMediaPlayer::position() + * @see millisecondsToString(int timeInMilliseconds) const + */ + QString getPosition() const; + +public slots: + /** + * @brief Navigate back to navgation list + */ + void navigateBack(); + /** + * @brief Play or pause music depending on whether it is currently playing + */ + void playPause(); + /** + * @brief Stop music and reset track to first track in playlist + */ + void stopMusic(); + /** + * @brief Play next track in current playlist if available + */ + void playNext(); + /** + * @brief Replay previoustrack in current playlist if available + */ + void playPrevious(); + +private: + /** + * @brief Resets playlist to first track if playlist exists and is non-empty + */ + void resetPlaylistToFirstTrack(); + /** + * @brief Clear the current playlist + */ + void clearPlaylist(); + /** + * @brief Add all media from path to playlist + * @param path Path to media + */ + void readMedia(const QString& path); + /** + * @brief Format time in milliseconds acoording to [M]m:ss + * @param time Time in ms + * @return formateted string + */ + QString millisecondsToString(int timeInMilliseconds) const; + + NavigationItemModel* mCurrentItem = nullptr; +}; + +#endif // MUSICPLAYER_H diff --git a/LenaPi/controllers/NavigationController.cpp b/LenaPi/controllers/NavigationController.cpp index 1eb5c59..f3bb4ab 100644 --- a/LenaPi/controllers/NavigationController.cpp +++ b/LenaPi/controllers/NavigationController.cpp @@ -1,30 +1,41 @@ #include "NavigationController.h" +#include #include #include +#include #include #include #include -#include +#include NavigationController::NavigationController(QObject *parent) : QObject(parent), mRootItem(new NavigationItemModel(this)), mNavList(new NavigationListModel(this)), mUiState(new UiStateModel(this)), - mMusicController(new MusicController(this)) + mMediaPlayer(new MusicPlayer(this)) { - connect(mMusicController, &MusicController::navigateTo, [this](NavigationItemModel* item) { + connect(mMediaPlayer, &MusicPlayer::navigateTo, [this](NavigationItemModel* item) { mUiState->showNavigation(); mNavList->navigateTo(item); }); + /* Connect player state to energy saver to prevent device shutdown while playing music. + */ + connect(mMediaPlayer, &MusicPlayer::isPlayingChanged, this, &NavigationController::startOrStopEnergySaverDependingOnPlayerState); +} + +void NavigationController::setDebugOutput(const QString& text) +{ + mDebugOutput = text; + emit debugOutputChanged(); } void NavigationController::init(const QString &rootPath) { auto rootDir = QDir(rootPath); if(!rootDir.exists()) return; - mRootPath = rootPath; + mRootPath = rootPath; add(mRootPath, mRootItem); @@ -40,7 +51,6 @@ void NavigationController::setContext(QQmlContext *context) { mContext = context; setContextProperties(); - mMusicController->setContext(mContext); } void NavigationController::setContextProperties() @@ -48,6 +58,8 @@ void NavigationController::setContextProperties() if(!mContext) return; mContext->setContextProperty("navigationList", mNavList); mContext->setContextProperty("uiStateModel", mUiState); + mContext->setContextProperty("musicModel", mMediaPlayer); + mContext->setContextProperty("debug", this); } void NavigationController::add(const QString &path, NavigationItemModel *parentItem) @@ -75,18 +87,18 @@ bool NavigationController::checkContent(const QString &path) { bool valid = false; auto dir =QDir(path); + // directory must either contain subdirectories or media files auto subDirsNames = dir.entryList(QDir::AllDirs); if(subDirsNames.length() > 0) { valid = true; } else { + dir.setNameFilters(SettingsHandler::getAudioFileNameFilters()); auto fileNames = dir.entryList(QDir::Files); int numAudio = 0; for(auto file:fileNames){ - if(file.endsWith(".flac") || file.endsWith(".mp3")){ numAudio++; - } } - if(numAudio > 0) valid = true; + valid = numAudio > 0; } return valid; } @@ -99,7 +111,20 @@ void NavigationController::onNavigationRequest() if(item->hasChildren()) mNavList->setModelItems(item->getChildren()); else { - mMusicController->initPlayer(item); + mMediaPlayer->init(item); mUiState->showMusicPlayer(); } } + +void NavigationController::startOrStopEnergySaverDependingOnPlayerState() +{ + auto* energySaver = EnergySaver::instance(); + assert(energySaver); + if(energySaver){ + if(mMediaPlayer->isPlaying()){ + energySaver->deactivate(); + } else { + energySaver->activate(); + } + } +} diff --git a/LenaPi/controllers/NavigationController.h b/LenaPi/controllers/NavigationController.h index 8761160..0459f3d 100644 --- a/LenaPi/controllers/NavigationController.h +++ b/LenaPi/controllers/NavigationController.h @@ -7,7 +7,7 @@ class NavigationItemModel; class NavigationListModel; class UiStateModel; -class MusicController; +class MusicPlayer; /** * @brief Main controller controlling ui state, navigation and music player. @@ -15,6 +15,12 @@ class MusicController; class NavigationController : public QObject { Q_OBJECT + + Q_PROPERTY(QString pDebugOutput READ getDebugOutput NOTIFY debugOutputChanged) + +signals: + void debugOutputChanged(); + public: explicit NavigationController(QObject *parent = nullptr); @@ -37,6 +43,8 @@ public: */ void setContext(QQmlContext* context); + QString getDebugOutput() const { return mDebugOutput; } + private: /** * @brief Register models in context @@ -55,14 +63,22 @@ private: */ bool checkContent(const QString& path); + /** + * @brief Set Debug Output. Necessary as Android Debugger does not work yet + * @param text Output to show in ui + */ + void setDebugOutput(const QString& text); + NavigationItemModel* mRootItem; NavigationListModel* mNavList; UiStateModel* mUiState; - MusicController* mMusicController; + MusicPlayer* mMediaPlayer; QString mRootPath = "."; + QString mDebugOutput; + QQmlContext* mContext = nullptr; private slots: @@ -70,6 +86,7 @@ private slots: * @brief Either show subdirectories or music player depending on current directory type */ void onNavigationRequest(); + void startOrStopEnergySaverDependingOnPlayerState(); }; #endif // NAVIGATIONCONTROLLER_H diff --git a/LenaPi/controllers/SettingsHandler.cpp b/LenaPi/controllers/SettingsHandler.cpp index 9dd1373..a0f6edb 100644 --- a/LenaPi/controllers/SettingsHandler.cpp +++ b/LenaPi/controllers/SettingsHandler.cpp @@ -1,4 +1,5 @@ #include "SettingsHandler.h" +#include constexpr const char* const rootPath = "rootPath"; constexpr const char* const profile = "profile"; @@ -59,11 +60,20 @@ QString SettingsHandler::getShutdownScript() const void SettingsHandler::initDefaults() { - mDefaults.insert(rootPath, "/home/ar/source/lenaMusic/"); - mDefaults.insert(profile, "RasPiTouch"); - mDefaults.insert(enableEnergySaver, true); + mDefaults.insert(rootPath, QStandardPaths::MusicLocation); + mDefaults.insert(enableEnergySaver, false); mDefaults.insert(timeout, 60); mDefaults.insert(shutdownScript, "/usr/local/sbin/do_shutdown.sh"); + mDefaults.insert(profile, "RasPiTouch"); + // @todo add profile Android? Or simply scale ui for RasPi + // mDefaults.insert(profile, "Android"); + +// Defaults for LenaPi +// mDefaults.insert(rootPath, "/home/ar/source/lenaMusic/"); +// mDefaults.insert(profile, "RasPiTouch"); +// mDefaults.insert(enableEnergySaver, true); +// mDefaults.insert(timeout, 60); +// mDefaults.insert(shutdownScript, "/usr/local/sbin/do_shutdown.sh"); } diff --git a/LenaPi/controllers/SettingsHandler.h b/LenaPi/controllers/SettingsHandler.h index 83b9868..2b7cb8d 100644 --- a/LenaPi/controllers/SettingsHandler.h +++ b/LenaPi/controllers/SettingsHandler.h @@ -17,6 +17,12 @@ public: static SettingsHandler* createSettingsHandlerAndFillWithDefaultsIfMissing(QSettings* settings); + /** + * @brief Provides a name filter for QDir that can be used to filter all audio files with valid formats. + * @return name filter for QDir + */ + static QStringList getAudioFileNameFilters() {return {"*.mp3", "*.flac"};} + inline void setSettings(QSettings* settings) { mSettings = settings;} inline QSettings* getSettings() const { return mSettings; } void fillWithDefaultIfMissing(); diff --git a/LenaPi/controllers/StyleHandling.cpp b/LenaPi/controllers/StyleHandling.cpp new file mode 100644 index 0000000..19d4419 --- /dev/null +++ b/LenaPi/controllers/StyleHandling.cpp @@ -0,0 +1,90 @@ +#include "StyleHandling.h" +#include +#include +#include + +StyleHandling::StyleHandling(QObject *parent) + : QObject{parent}, mStyleSizes(new QQmlPropertyMap(this)), + mMargins(new QQmlPropertyMap(this)), mSpacings(new QQmlPropertyMap(this)), mPaddings(new QQmlPropertyMap(this)) +{ + // nothing +} + +void StyleHandling::calculateAndSetRatio() +{ + qreal refHeight = 480; + qreal refWidth = 800; + // Scales to fullscreen. No rescaling when changing window size + QRect rect = QGuiApplication::primaryScreen()->geometry(); + qreal height = qMin(rect.width(),rect.height()); + qreal width = qMax(rect.width(), rect.height()); + + mRatio = qMin(height/refHeight, width/refWidth); + qDebug() << "mRation=" << mRatio<< "sizes="<insert(key, applyRatio(value)); +} + +int StyleHandling::applyRatio(int size) const +{ + return size*mRatio; +} + +void StyleHandling::init(QQmlContext *context) +{ + calculateAndSetRatio(); + initStyleSizes(); + initMargins(); + initSpacings(); + initPaddings(); + + context->setContextProperty("StyleSizes", mStyleSizes); + context->setContextProperty("StyleSpacings", mSpacings); + context->setContextProperty("StyleMargins", mMargins); + context->setContextProperty("StylePaddings", mPaddings); +} diff --git a/LenaPi/controllers/StyleHandling.h b/LenaPi/controllers/StyleHandling.h new file mode 100644 index 0000000..2128b05 --- /dev/null +++ b/LenaPi/controllers/StyleHandling.h @@ -0,0 +1,81 @@ +#ifndef STYLEHANDLING_H +#define STYLEHANDLING_H + +#include +#include +#include + +/** + * @brief Contains all style sizes, margins and paddings used throughout the application + * + * Based on a reference screen size, maps for scaled margins, paddings and spacings are created and + * registered in the QML context. The values stored in the maps can then be used from QML. + * Map names: StyleSizes, StylePaddings, StyleSpacings, StyleMargins + * + * Always use values from these maps in QML! Never use magic numbers! They will NOT scale when + * the application is run on a different screen. + * + * See https://doc.qt.io/qt-5/scalability.html + * + * @todo scale fonts as well? + * @todo use dpi for scaling as app is very small on Android? + */ +class StyleHandling : public QObject +{ + Q_OBJECT +public: + explicit StyleHandling(QObject *parent = nullptr); + + /** + * @brief Calculates ratio, initializes all maps and registers them in QML context + * @param context Context to register Maps in + */ + void init(QQmlContext* context); + +private: + /** + * @brief Calculates ratio from reference screen size and size of the current primary screen and inits member mRatio + */ + void calculateAndSetRatio(); + /** + * @brief Initializes map containing all sizes (e.g., button or delegate sizes) + */ + void initStyleSizes(); + /** + * @brief Initializes map containing all spacings + */ + void initSpacings(); + /** + * @brief Initializes map containing all margins + */ + void initMargins(); + /** + * @brief Initializes map containing all paddings + */ + void initPaddings(); + /** + * @brief Scales the value with calculated ratio and inserts it into the given map under the given key + * @param map Map to insert key-value pair into + * @param key Key used in the map. This is the name used to retrieve the value in QML + * @param value Unscaled value + * @see mRatio + * + * In QML the value with the key "defaulSpacing" in the Map "StyleSpacings" is accessed by StyleSpacings.defaultSpacing + */ + void scaleAndInsert(QQmlPropertyMap* map, const QString& key, int value); + /** + * @brief Applys ratio to given size + * @param size Size to be scaled + * @return scaled size + * @see mRatio + */ + int applyRatio(int size) const; + + qreal mRatio = 1.0; + QQmlPropertyMap* mStyleSizes = nullptr; + QQmlPropertyMap* mMargins = nullptr; + QQmlPropertyMap* mSpacings = nullptr; + QQmlPropertyMap* mPaddings = nullptr; +}; + +#endif // STYLEHANDLING_H diff --git a/LenaPi/main.cpp b/LenaPi/main.cpp index e4633a4..b125dc7 100644 --- a/LenaPi/main.cpp +++ b/LenaPi/main.cpp @@ -4,20 +4,20 @@ #include #include #include -#include "controllers/NavigationController.h" +#include +#include +#include #include "MouseEventSpy.h" #include "EnergySaver.h" -#include "controllers/SettingsHandler.h" int main(int argc, char *argv[]) { - QGuiApplication app(argc, argv); QQmlApplicationEngine engine; - /* Add command line parser to specify a custom config file - * https://doc.qt.io/qt-5/qcommandlineparser.html - */ + /**************************************************************************** + * Configure and parse commandline arguments + ****************************************************************************/ QCommandLineParser parser; parser.setApplicationDescription("Lena's music app"); // Define a custom config file using -c or --config @@ -27,6 +27,12 @@ int main(int argc, char *argv[]) // process commandline arguments parser.process(app); + /**************************************************************************** + * Find and read settings + * If a config file is handed over via commandline arguments, it is preferred. + * Otherwise, the config in the standard location is used. If none exists yet, + * a default config is created. + ****************************************************************************/ QSettings* settings = nullptr; if(!parser.value(configOption).isEmpty()){ // config was handed over via commandline argument. Use this config if file exists. @@ -37,20 +43,35 @@ int main(int argc, char *argv[]) } } if(!settings){ - // default config + // create config from default location settings = new QSettings(QSettings::Scope::UserScope, "MaleyanaSoft", "LenaPi"); } - /* Read Settings */ + // Read Settings const auto settingsHandler = SettingsHandler::createSettingsHandlerAndFillWithDefaultsIfMissing(settings); - // init main app + /**************************************************************************** + * init style + * Sets default sizes for ui elements. The element size is scaled according + * to the device's display size. + ****************************************************************************/ + StyleHandling styleHandler; + styleHandler.init(engine.rootContext()); + + /**************************************************************************** + * init main app + ****************************************************************************/ NavigationController navController; navController.setContext(engine.rootContext()); navController.init(settingsHandler->getRootPath()); navController.setUiProfile(settingsHandler->getProfile()); + /**************************************************************************** + * init energy saver + * Prevents sleep on android devices and shuts down other device if inactive + * (no music or mouse events) for a certain time intervall + ****************************************************************************/ if(settingsHandler->isEnergySaverEnabled()){ /* install MouseEventSpy and energy saver used for auto shut down of device * if not used for a predefined time. @@ -59,9 +80,13 @@ int main(int argc, char *argv[]) EnergySaver::init(settingsHandler->getEnergySaverTimeout(), settingsHandler->getShutdownScript()); QObject::connect(MouseEventSpy::instance(), &MouseEventSpy::mouseEventDetected, EnergySaver::instance(), &EnergySaver::restartTimer); + } else { + EnergySaver::init(); } - // load GUI + /**************************************************************************** + * load view + ****************************************************************************/ engine.load(QUrl("qrc:/main.qml")); return app.exec(); diff --git a/LenaPi/main.qml b/LenaPi/main.qml index 7d26c1b..c796e43 100644 --- a/LenaPi/main.qml +++ b/LenaPi/main.qml @@ -4,8 +4,8 @@ import QtQuick.Controls 2.4 Window { visible: true - width: 800 - height: 480 + // width: 800 + //height: 480 title: "LenaPi 1.2" Component.onCompleted: showMaximized(); diff --git a/LenaPi/models/MusicModel.cpp b/LenaPi/models/MusicModel.cpp deleted file mode 100644 index 9383ea4..0000000 --- a/LenaPi/models/MusicModel.cpp +++ /dev/null @@ -1,218 +0,0 @@ -#include "MusicModel.h" -#include -#include -#include - -MusicModel::MusicModel(VlcInstance* instance, QObject *parent) : QObject(parent), - mVlc(instance), mMedia(new VlcMediaList(instance)) -{ - /* nothing */ -} - -MusicModel::~MusicModel() -{ - // do not delete! will cause segmentation fault - //if(mMedia) - // mMedia->deleteLater(); -} - -void MusicModel::init(NavigationItemModel *item) -{ - if(mCurrentItem == item){ - return; - } - mCurrentItem = item; - emit currentItemChanged(); - - reset(); - clearMediaList(); - - readMedia(mCurrentItem->getPath()); - - setMediaTitle(mMedia->at(0)); -} - -void MusicModel::navigateBack() -{ - emit navigateTo(mCurrentItem); -} - -void MusicModel::playPause() -{ - mIsPlaying = !mIsPlaying; - emit isPlayingChanged(); - if(mIsPlaying) - emit play(); - else - emit pause(); -} - -void MusicModel::stopMusic() -{ - if(mIsPlaying){ - mIsPlaying = false; - emit isPlayingChanged(); - - reset(); - - emit stop(); - } -} - -void MusicModel::playNext() -{ - emit next(); - if(!mIsPlaying){ - mIsPlaying = true; - emit isPlayingChanged(); - } -} - -void MusicModel::playPrevious() -{ - emit previous(); - if(!mIsPlaying){ - mIsPlaying = true; - emit isPlayingChanged(); - } -} - -NavigationItemModel *MusicModel::getCurrentItem() -{ - return mCurrentItem; -} - -VlcMediaList *MusicModel::getMedia() -{ - return mMedia; -} - -bool MusicModel::isPlaying() const -{ - return mIsPlaying; -} - -bool MusicModel::hasNext() const -{ - return mHasNext; -} - -bool MusicModel::hasPrevious() const -{ - return mHasPrevious; -} - -void MusicModel::setAudioVolume(int newVolume) -{ - if(newVolume != mAudioVolume){ - if(newVolume > 100){ - mAudioVolume = 100; - } else if(newVolume < 0){ - mAudioVolume = 0; - } else { - mAudioVolume = newVolume; - } - emit audioVolumeChanged(mAudioVolume); - } -} - -double MusicModel::getProgress() const -{ - return mCurrentMediaItemProgress; -} - -QString MusicModel::getMediaTitle() const -{ - return mMediaTitle; -} - -QString MusicModel::getMediaLength() -{ - return timeToString(mCurrentMediaItemLength); -} - -QString MusicModel::getTime() -{ - return timeToString(mCurrentTime); -} - -void MusicModel::onNextMediaSet(VlcMedia *media) -{ - setMediaTitle(media); - - mHasNext = true; - mHasPrevious = true; - if(mMedia->at(0) == media){ - mHasPrevious = false; - } - if(mMedia->at(mMedia->count()-1) == media){ - mHasNext = false; - } - emit hasPreviousChanged(); - emit hasNextChanged(); -} - -void MusicModel::onTimeChanged(int time) -{ - mCurrentMediaItemProgress = (double) time / mCurrentMediaItemLength; - mCurrentTime = time; - emit progressChanged(); -} - -void MusicModel::onLengthChanged(int length) -{ - mCurrentMediaItemLength= length; - emit mediaLengthChanged(); -} - -void MusicModel::reset() -{ - mHasNext = false; - mHasPrevious = false; - emit hasNextChanged(); - emit hasPreviousChanged(); - - mCurrentMediaItemProgress = 0.0; - mCurrentTime = 0.0; - emit progressChanged(); - - mCurrentMediaItemLength = 0.0; - emit mediaLengthChanged(); -} - -void MusicModel::clearMediaList() -{ - while(mMedia->count() > 0){ - mMedia->removeMedia(0); - } -} - -void MusicModel::readMedia(const QString& path) -{ - auto dir = QDir(path); - if(!dir.exists()) return; - - auto fileNames = dir.entryList(QDir::Files); - for(auto file:fileNames){ - if(file.endsWith(".flac") || file.endsWith(".mp3")){ - mMedia->addMedia(new VlcMedia(dir.filePath(file), true, mVlc)); - } - } -} - -QString MusicModel::timeToString(int time) -{ - int sec = time/1000; - int min = sec/60; - sec = sec-min*60; - QString secStr = (sec < 10) ? "0"+QString::number(sec) : QString::number(sec); - return QString::number(min) + ":" + secStr; -} - -void MusicModel::setMediaTitle(VlcMedia *media) -{ - auto list = media->currentLocation().split("/"); - auto title = list.at(list.count() -1 ); - mMediaTitle = title.left(title.lastIndexOf(".")); - emit mediaTitleChanged(); -} diff --git a/LenaPi/models/MusicModel.h b/LenaPi/models/MusicModel.h deleted file mode 100644 index 764d79a..0000000 --- a/LenaPi/models/MusicModel.h +++ /dev/null @@ -1,100 +0,0 @@ -#ifndef MUSICMODEL_H -#define MUSICMODEL_H - -#include - -#include -#include - -class MusicModel : public QObject -{ - Q_OBJECT - - Q_PROPERTY(QObject* pCurrentItem READ getCurrentItem NOTIFY currentItemChanged) - Q_PROPERTY(bool pHasNext READ hasNext NOTIFY hasNextChanged) - Q_PROPERTY(bool pHasPrevious READ hasPrevious NOTIFY hasPreviousChanged) - Q_PROPERTY(bool pIsPlaying READ isPlaying NOTIFY isPlayingChanged) - Q_PROPERTY(double pProgress READ getProgress NOTIFY progressChanged) - Q_PROPERTY(QString pMediaLength READ getMediaLength NOTIFY mediaLengthChanged) - Q_PROPERTY(QString pTime READ getTime NOTIFY progressChanged) - Q_PROPERTY(QString pMediaTitle READ getMediaTitle NOTIFY mediaTitleChanged) - Q_PROPERTY(int pAudioVolume READ getAudioVolume WRITE setAudioVolume NOTIFY audioVolumeChanged) - -signals: - void navigateTo(NavigationItemModel *item); - void currentItemChanged(); - void play(); - void pause(); - void stop(); - void previous(); - void next(); - void hasPreviousChanged(); - void hasNextChanged(); - void isPlayingChanged(); - void progressChanged(); - void mediaLengthChanged(); - void mediaTitleChanged(); - void audioVolumeChanged(int newVolume); - -public: - MusicModel(VlcInstance* instance, QObject *parent = Q_NULLPTR); - ~MusicModel(); - - void init(NavigationItemModel* item); - - Q_INVOKABLE void navigateBack(); - Q_INVOKABLE void playPause(); - Q_INVOKABLE void stopMusic(); - Q_INVOKABLE void playNext(); - Q_INVOKABLE void playPrevious(); - - NavigationItemModel *getCurrentItem(); - - VlcMediaList *getMedia(); - - bool isPlaying() const; - bool hasNext() const; - bool hasPrevious() const; - - inline int getAudioVolume() const { return mAudioVolume; } - /** - * @brief Set audio volume. Information is transferred to VlcAudio - * @param newVolume value between 0 and 100 (audio level in percent) - * Ensures that volume is inbetween 0 and 100. If this range is exceeded, - * the volume is set to the lowest and highest allowed value, respectively. - */ - void setAudioVolume(int newVolume); - - double getProgress() const; - - QString getMediaTitle() const; - QString getMediaLength(); - QString getTime(); - -public slots: - void onNextMediaSet(VlcMedia* media); - void onTimeChanged(int time); - void onLengthChanged(int length); - -private: - void reset(); - void clearMediaList(); - void readMedia(const QString& path); - void setMediaTitle(VlcMedia* media); - QString timeToString(int time); - - - bool mIsPlaying = false; - bool mHasNext = false; - bool mHasPrevious = false; - int mCurrentMediaItemLength = 0; - int mCurrentTime = 0; - double mCurrentMediaItemProgress = 0; - int mAudioVolume{50}; - QString mMediaTitle = QString(""); - NavigationItemModel* mCurrentItem = Q_NULLPTR; - VlcMediaList* mMedia = Q_NULLPTR; - VlcInstance* mVlc = Q_NULLPTR; -}; - -#endif // MUSICMODEL_H diff --git a/LenaPi/models/NavigationItemModel.cpp b/LenaPi/models/NavigationItemModel.cpp index d540425..c4287b3 100644 --- a/LenaPi/models/NavigationItemModel.cpp +++ b/LenaPi/models/NavigationItemModel.cpp @@ -10,12 +10,12 @@ NavigationItemModel::NavigationItemModel(QObject *parent) : QObject(parent), qRegisterMetaType("NavigationItemModel*"); } -QString NavigationItemModel::getImageSource() const +const QString& NavigationItemModel::getImageSource() const { return mImageSource; } -QString NavigationItemModel::getPath() const +const QString& NavigationItemModel::getPath() const { return mPath; } diff --git a/LenaPi/models/NavigationItemModel.h b/LenaPi/models/NavigationItemModel.h index afb17b2..0bd7133 100644 --- a/LenaPi/models/NavigationItemModel.h +++ b/LenaPi/models/NavigationItemModel.h @@ -36,13 +36,13 @@ public: * represented by this item. If no such image is found, a default image is displayed * on the delegate. */ - QString getImageSource() const; + const QString &getImageSource() const; /** * @brief Get path to folder represented by this navigation item. * @return Path to folder */ - QString getPath() const; + const QString& getPath() const; /** * @brief Set folder path and set image source displayed on delegate. * @param path Path to directory that is represented by this navigation item. diff --git a/LenaPi/models/UiStateModel.h b/LenaPi/models/UiStateModel.h index d7eff7c..bcd352d 100644 --- a/LenaPi/models/UiStateModel.h +++ b/LenaPi/models/UiStateModel.h @@ -4,6 +4,7 @@ #include #include #include +#include /** * @brief Handles state of UI by providing the qml source. diff --git a/LenaPi/resources/icon.jpeg b/LenaPi/resources/icon.jpeg new file mode 100644 index 0000000..42118e0 Binary files /dev/null and b/LenaPi/resources/icon.jpeg differ