From fe7bbed7c1a102e28c82c07b9ee901bf6c554af7 Mon Sep 17 00:00:00 2001 From: Anika Raemer Date: Tue, 12 Oct 2021 22:15:06 +0200 Subject: [PATCH 01/10] remove vlc and replace by basic QMediaPlayer --- .gitignore | 1 + LenaPi/LenaPi.pro | 11 +- LenaPi/LenaPi.pro.user | 172 +++++++------- LenaPi/controllers/MusicController.cpp | 72 ------ LenaPi/controllers/MusicController.h | 52 ----- LenaPi/controllers/MusicPlayer.cpp | 194 ++++++++++++++++ .../MusicPlayer.h} | 39 ++-- LenaPi/controllers/NavigationController.cpp | 10 +- LenaPi/controllers/NavigationController.h | 4 +- LenaPi/models/MusicModel.cpp | 218 ------------------ 10 files changed, 303 insertions(+), 470 deletions(-) delete mode 100644 LenaPi/controllers/MusicController.cpp delete mode 100644 LenaPi/controllers/MusicController.h create mode 100644 LenaPi/controllers/MusicPlayer.cpp rename LenaPi/{models/MusicModel.h => controllers/MusicPlayer.h} (76%) delete mode 100644 LenaPi/models/MusicModel.cpp diff --git a/.gitignore b/.gitignore index 56accb7..cf45d4e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ build-LenaPi-Desktop-Debug build-LenaPi-RasPi-Debug build-LenaPi-RasPi-Release LenaPi/LenaPi.pro.user.4.8-pre1 +musik diff --git a/LenaPi/LenaPi.pro b/LenaPi/LenaPi.pro index 5750661..baa2c3a 100644 --- a/LenaPi/LenaPi.pro +++ b/LenaPi/LenaPi.pro @@ -1,16 +1,15 @@ TEMPLATE = app -QT += qml quick +QT += qml quick multimedia CONFIG += c++11 -LIBS += -lVLCQtCore -lVLCQtQml +LIBS += SOURCES += main.cpp \ + controllers/MusicPlayer.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,12 +40,12 @@ else: unix:!android: target.path = /opt/$${TARGET}/bin !isEmpty(target.path): INSTALLS += target HEADERS += \ + controllers/MusicPlayer.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 diff --git a/LenaPi/LenaPi.pro.user b/LenaPi/LenaPi.pro.user index 430c2b1..bcdddb7 100644 --- a/LenaPi/LenaPi.pro.user +++ b/LenaPi/LenaPi.pro.user @@ -1,10 +1,10 @@ - + EnvironmentId - {76cb87c9-a04b-479b-87ff-1b3ddab073a7} + {0510cc5f-8237-43aa-9f4f-ce9a2d4944ec} ProjectExplorer.Project.ActiveTarget @@ -49,209 +49,206 @@ true true true + *.md, *.MD, Makefile false + true ProjectExplorer.Project.PluginSettings + + true + true + true + true + true + + + 0 + true true + Builtin.Questionable + + true + true + Builtin.DefaultTidyAndClazy + 2 + + + + true + ProjectExplorer.Project.Target.0 + Desktop Desktop Desktop - {f9938f31-357c-4785-aa6e-21a3feac2ebf} + {ebe72439-b95e-4a0f-b855-a910128856ec} 0 0 0 - /home/araemer/source/LenaPi/build-LenaPi-Desktop-Debug + 0 + /home/araemer/source/lenapi/build-LenaPi-Desktop-Debug + /home/araemer/source/lenapi/build-LenaPi-Desktop-Debug true - qmake - QtProjectManager.QMakeBuildStep - true false - false - false + true - Make - Qt4ProjectManager.MakeStep - - false - - - false 2 Build - + Build ProjectExplorer.BuildSteps.Build true - Make - Qt4ProjectManager.MakeStep - - true clean - - false 1 Clean - + Clean ProjectExplorer.BuildSteps.Clean 2 false + - Debug - + Debug Qt4ProjectManager.Qt4BuildConfiguration 2 - true + 0 - /home/ar/source/LenaPi/build-LenaPi-Desktop-Release + /home/araemer/source/lenapi/build-LenaPi-Desktop-Release + /home/araemer/source/lenapi/build-LenaPi-Desktop-Release true - qmake - QtProjectManager.QMakeBuildStep - false false - false - false + true - Make - Qt4ProjectManager.MakeStep - - false - - - false 2 Build - + Build ProjectExplorer.BuildSteps.Build true - Make - Qt4ProjectManager.MakeStep - - true clean - - false 1 Clean - + Clean ProjectExplorer.BuildSteps.Clean 2 false + - Release - + Release Qt4ProjectManager.Qt4BuildConfiguration 0 - true + 0 + 0 - /home/ar/source/LenaPi/build-LenaPi-Desktop-Profile + 0 + /home/araemer/source/lenapi/build-LenaPi-Desktop-Profile + /home/araemer/source/lenapi/build-LenaPi-Desktop-Profile true - qmake - QtProjectManager.QMakeBuildStep - true false - true - false + true - Make - Qt4ProjectManager.MakeStep - - false - - - false 2 Build - + Build ProjectExplorer.BuildSteps.Build true - Make - Qt4ProjectManager.MakeStep - - true clean - - false 1 Clean - + Clean ProjectExplorer.BuildSteps.Clean 2 false + - Profile - + Profile Qt4ProjectManager.Qt4BuildConfiguration 0 - true + 0 + 0 + 0 3 0 Deploy - + Deploy ProjectExplorer.BuildSteps.Deploy 1 - Deploy locally - + + false ProjectExplorer.DefaultDeployConfiguration 1 - + dwarf + + cpu-cycles + + + 250 + + -e + cpu-cycles + --call-graph + dwarf,4096 + -F + 250 + + -F + true + 4096 false false 1000 @@ -266,6 +263,7 @@ 0.01 10 true + kcachegrind 1 25 @@ -291,22 +289,16 @@ 13 14 + 2 - LenaPi - LenaPi2 - Qt4ProjectManager.Qt4RunConfiguration:/home/ar/source/LenaPi/LenaPi/LenaPi.pro - LenaPi.pro - - 3768 + Qt4ProjectManager.Qt4RunConfiguration:/home/araemer/source/lenapi/LenaPi/LenaPi.pro + /home/araemer/source/lenapi/LenaPi/LenaPi.pro false true true - false false true - - /home/araemer/source/LenaPi/build-LenaPi-Desktop-Debug 1 @@ -317,10 +309,10 @@ ProjectExplorer.Project.Updater.FileVersion - 20 + 22 Version - 20 + 22 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..df43e10 --- /dev/null +++ b/LenaPi/controllers/MusicPlayer.cpp @@ -0,0 +1,194 @@ +#include "MusicPlayer.h" +#include +#include + + + +MusicPlayer::MusicPlayer(QObject *parent) : QMediaPlayer(parent) +{ + /* nothing */ +} + +MusicPlayer::~MusicPlayer() +{ +} + +void MusicPlayer::init(NavigationItemModel *item) +{ + if(mCurrentItem == item){ + return; + } + mCurrentItem = item; + emit currentItemChanged(); + + reset(); + clearMediaList(); + + readMedia(mCurrentItem->getPath()); +} + +void MusicPlayer::navigateBack() +{ + emit navigateTo(mCurrentItem); +} + +void MusicPlayer::playPause() +{ + mIsPlaying = !mIsPlaying; + emit isPlayingChanged(); + if(mIsPlaying) + play(); + else + pause(); +} + +void MusicPlayer::stopMusic() +{ + if(mIsPlaying){ + mIsPlaying = false; + emit isPlayingChanged(); + + reset(); + + emit stop(); + } +} + +void MusicPlayer::playNext() +{ + //emit next(); + if(!mIsPlaying){ + mIsPlaying = true; + emit isPlayingChanged(); + } +} + +void MusicPlayer::playPrevious() +{ + //emit previous(); + if(!mIsPlaying){ + mIsPlaying = true; + emit isPlayingChanged(); + } +} + +NavigationItemModel *MusicPlayer::getCurrentItem() +{ + return mCurrentItem; +} + +bool MusicPlayer::isPlaying() const +{ + return mIsPlaying; +} + +bool MusicPlayer::hasNext() const +{ + return mHasNext; +} + +bool MusicPlayer::hasPrevious() const +{ + return mHasPrevious; +} + +void MusicPlayer::setAudioVolume(int newVolume) +{ + if(newVolume != mAudioVolume){ + if(newVolume > 100){ + mAudioVolume = 100; + } else if(newVolume < 0){ + mAudioVolume = 0; + } else { + mAudioVolume = newVolume; + } + emit audioVolumeChanged(mAudioVolume); + } +} + +double MusicPlayer::getProgress() const +{ + return mCurrentMediaItemProgress; +} + +QString MusicPlayer::getMediaTitle() const +{ + return mMediaTitle; +} + +QString MusicPlayer::getMediaLength() +{ + return timeToString(mCurrentMediaItemLength); +} + +QString MusicPlayer::getTime() +{ + return timeToString(mCurrentTime); +} + +void MusicPlayer::onTimeChanged(int time) +{ + mCurrentMediaItemProgress = (double) time / mCurrentMediaItemLength; + mCurrentTime = time; + emit progressChanged(); +} + +void MusicPlayer::onLengthChanged(int length) +{ + mCurrentMediaItemLength= length; + emit mediaLengthChanged(); +} + +void MusicPlayer::reset() +{ + mHasNext = false; + mHasPrevious = false; + emit hasNextChanged(); + emit hasPreviousChanged(); + + mCurrentMediaItemProgress = 0.0; + mCurrentTime = 0.0; + emit progressChanged(); + + mCurrentMediaItemLength = 0.0; + emit mediaLengthChanged(); +} + +void MusicPlayer::clearMediaList() +{ + 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({"*.mp3", "*.flac"}); + auto files = dir.entryInfoList(QDir::Files); + for(auto file:files){ + auto fileName = file.absoluteFilePath(); + auto fileUrl = QUrl::fromLocalFile(fileName); + qDebug() << fileName << fileUrl; + playList->addMedia(QMediaContent(fileUrl)); + } +} + +QString MusicPlayer::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; +} diff --git a/LenaPi/models/MusicModel.h b/LenaPi/controllers/MusicPlayer.h similarity index 76% rename from LenaPi/models/MusicModel.h rename to LenaPi/controllers/MusicPlayer.h index 764d79a..4d59071 100644 --- a/LenaPi/models/MusicModel.h +++ b/LenaPi/controllers/MusicPlayer.h @@ -1,12 +1,11 @@ -#ifndef MUSICMODEL_H -#define MUSICMODEL_H +#ifndef MUSICPLAYER_H +#define MUSICPLAYER_H #include - +#include #include -#include -class MusicModel : public QObject +class MusicPlayer : public QMediaPlayer { Q_OBJECT @@ -23,11 +22,6 @@ class MusicModel : public QObject signals: void navigateTo(NavigationItemModel *item); void currentItemChanged(); - void play(); - void pause(); - void stop(); - void previous(); - void next(); void hasPreviousChanged(); void hasNextChanged(); void isPlayingChanged(); @@ -37,20 +31,13 @@ signals: void audioVolumeChanged(int newVolume); public: - MusicModel(VlcInstance* instance, QObject *parent = Q_NULLPTR); - ~MusicModel(); + MusicPlayer(QObject *parent = Q_NULLPTR); + ~MusicPlayer(); 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; @@ -72,7 +59,12 @@ public: QString getTime(); public slots: - void onNextMediaSet(VlcMedia* media); + void navigateBack(); + void playPause(); + void stopMusic(); + void playNext(); + void playPrevious(); + void onTimeChanged(int time); void onLengthChanged(int length); @@ -80,7 +72,6 @@ private: void reset(); void clearMediaList(); void readMedia(const QString& path); - void setMediaTitle(VlcMedia* media); QString timeToString(int time); @@ -92,9 +83,7 @@ private: double mCurrentMediaItemProgress = 0; int mAudioVolume{50}; QString mMediaTitle = QString(""); - NavigationItemModel* mCurrentItem = Q_NULLPTR; - VlcMediaList* mMedia = Q_NULLPTR; - VlcInstance* mVlc = Q_NULLPTR; + NavigationItemModel* mCurrentItem = nullptr; }; -#endif // MUSICMODEL_H +#endif // MUSICPLAYER_H diff --git a/LenaPi/controllers/NavigationController.cpp b/LenaPi/controllers/NavigationController.cpp index 1eb5c59..0dc4a4f 100644 --- a/LenaPi/controllers/NavigationController.cpp +++ b/LenaPi/controllers/NavigationController.cpp @@ -6,15 +6,15 @@ #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); }); @@ -40,7 +40,6 @@ void NavigationController::setContext(QQmlContext *context) { mContext = context; setContextProperties(); - mMusicController->setContext(mContext); } void NavigationController::setContextProperties() @@ -48,6 +47,7 @@ void NavigationController::setContextProperties() if(!mContext) return; mContext->setContextProperty("navigationList", mNavList); mContext->setContextProperty("uiStateModel", mUiState); + mContext->setContextProperty("musicModel", mMediaPlayer); } void NavigationController::add(const QString &path, NavigationItemModel *parentItem) @@ -99,7 +99,7 @@ void NavigationController::onNavigationRequest() if(item->hasChildren()) mNavList->setModelItems(item->getChildren()); else { - mMusicController->initPlayer(item); + mMediaPlayer->init(item); mUiState->showMusicPlayer(); } } diff --git a/LenaPi/controllers/NavigationController.h b/LenaPi/controllers/NavigationController.h index 8761160..6f40c5b 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. @@ -59,7 +59,7 @@ private: NavigationListModel* mNavList; UiStateModel* mUiState; - MusicController* mMusicController; + MusicPlayer* mMediaPlayer; QString mRootPath = "."; 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(); -} From e9a031e6d39a55542ae3b8b8c86d98fd7debed15 Mon Sep 17 00:00:00 2001 From: Anika Raemer Date: Wed, 13 Oct 2021 13:01:19 +0200 Subject: [PATCH 02/10] volume control --- LenaPi/MusicPlayer.qml | 5 +---- LenaPi/VolumeSlider.qml | 4 ++-- LenaPi/controllers/MusicPlayer.cpp | 29 ++++++++------------------ LenaPi/controllers/MusicPlayer.h | 30 ++++++++++++++++++++------- LenaPi/main.cpp | 3 --- LenaPi/models/NavigationItemModel.cpp | 4 ++-- LenaPi/models/NavigationItemModel.h | 4 ++-- 7 files changed, 39 insertions(+), 40 deletions(-) diff --git a/LenaPi/MusicPlayer.qml b/LenaPi/MusicPlayer.qml index a34d213..78f0db4 100644 --- a/LenaPi/MusicPlayer.qml +++ b/LenaPi/MusicPlayer.qml @@ -45,7 +45,7 @@ Item{ anchors.centerIn: parent height: parent.height-10 width: height - source: musicModel.pCurrentItem.pImageSource + source: musicModel.pCoverImageSource fillMode: Image.PreserveAspectCrop layer.enabled: true layer.effect: OpacityMask{ @@ -64,9 +64,6 @@ 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 onValueChanged: { musicModel.pAudioVolume = value; diff --git a/LenaPi/VolumeSlider.qml b/LenaPi/VolumeSlider.qml index 1ca6c9b..1667df5 100644 --- a/LenaPi/VolumeSlider.qml +++ b/LenaPi/VolumeSlider.qml @@ -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/controllers/MusicPlayer.cpp b/LenaPi/controllers/MusicPlayer.cpp index df43e10..032dd13 100644 --- a/LenaPi/controllers/MusicPlayer.cpp +++ b/LenaPi/controllers/MusicPlayer.cpp @@ -6,11 +6,10 @@ MusicPlayer::MusicPlayer(QObject *parent) : QMediaPlayer(parent) { - /* nothing */ -} - -MusicPlayer::~MusicPlayer() -{ + // relay base class signal as NOTIFY doesn't seem to be able to handle it directly + connect(this, &QMediaPlayer::volumeChanged, this, &MusicPlayer::audioVolumeChanged); + // init audio + setAudioVolume(50); } void MusicPlayer::init(NavigationItemModel *item) @@ -19,7 +18,7 @@ void MusicPlayer::init(NavigationItemModel *item) return; } mCurrentItem = item; - emit currentItemChanged(); + emit coverImageSourceChanged(); reset(); clearMediaList(); @@ -72,11 +71,6 @@ void MusicPlayer::playPrevious() } } -NavigationItemModel *MusicPlayer::getCurrentItem() -{ - return mCurrentItem; -} - bool MusicPlayer::isPlaying() const { return mIsPlaying; @@ -94,15 +88,10 @@ bool MusicPlayer::hasPrevious() const void MusicPlayer::setAudioVolume(int newVolume) { - if(newVolume != mAudioVolume){ - if(newVolume > 100){ - mAudioVolume = 100; - } else if(newVolume < 0){ - mAudioVolume = 0; - } else { - mAudioVolume = newVolume; - } - emit audioVolumeChanged(mAudioVolume); + if(newVolume != volume()){ + qDebug() << volume() << newVolume; + setVolume(newVolume); + // signal audioVolumeChanged will be emitted automatically } } diff --git a/LenaPi/controllers/MusicPlayer.h b/LenaPi/controllers/MusicPlayer.h index 4d59071..8d36b64 100644 --- a/LenaPi/controllers/MusicPlayer.h +++ b/LenaPi/controllers/MusicPlayer.h @@ -5,11 +5,14 @@ #include #include +/** + * @brief MusicPlayer providing interface to QML and Navigation + */ class MusicPlayer : public QMediaPlayer { Q_OBJECT - Q_PROPERTY(QObject* pCurrentItem READ getCurrentItem NOTIFY currentItemChanged) + 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) @@ -21,7 +24,7 @@ class MusicPlayer : public QMediaPlayer signals: void navigateTo(NavigationItemModel *item); - void currentItemChanged(); + void coverImageSourceChanged(); void hasPreviousChanged(); void hasNextChanged(); void isPlayingChanged(); @@ -32,23 +35,37 @@ signals: public: MusicPlayer(QObject *parent = Q_NULLPTR); - ~MusicPlayer(); + ~MusicPlayer() =default; void init(NavigationItemModel* item); - NavigationItemModel *getCurrentItem(); + /** + * @brief Get path to media cover image. + * @return Path to media cover image. + */ + inline const QString& getCoverImageSource() const {return mCurrentItem->getImageSource();} bool isPlaying() const; bool hasNext() const; bool hasPrevious() const; - inline int getAudioVolume() const { return mAudioVolume; } /** - * @brief Set audio volume. Information is transferred to VlcAudio + * @brief Get audio volume. Range is in between 0 and 100 + * Relays info from QMediaPlayer::volume as Q_PROPERTY does seem to have + * trouble accessing base class functions directly. + */ + inline int getAudioVolume() const { return volume(); } + /** + * @brief Set audio volume. * @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. + * + * Relay info to QMediaPlayer::setVolume as Q_PROPERTY does seem to have + * trouble accessing base class functions directly. + * + * @todo save to config file? */ void setAudioVolume(int newVolume); @@ -81,7 +98,6 @@ private: int mCurrentMediaItemLength = 0; int mCurrentTime = 0; double mCurrentMediaItemProgress = 0; - int mAudioVolume{50}; QString mMediaTitle = QString(""); NavigationItemModel* mCurrentItem = nullptr; }; diff --git a/LenaPi/main.cpp b/LenaPi/main.cpp index e4633a4..dee97c8 100644 --- a/LenaPi/main.cpp +++ b/LenaPi/main.cpp @@ -15,9 +15,6 @@ 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 - */ QCommandLineParser parser; parser.setApplicationDescription("Lena's music app"); // Define a custom config file using -c or --config 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. From 2d985ca17d8e3743b08874f86abfb9b0187dcb8a Mon Sep 17 00:00:00 2001 From: Anika Raemer Date: Wed, 13 Oct 2021 13:47:16 +0200 Subject: [PATCH 03/10] fixed next/previous buttons, simplyfied volume --- LenaPi/MusicPlayer.qml | 4 +- LenaPi/controllers/MusicPlayer.cpp | 74 +++++++++++++----------------- LenaPi/controllers/MusicPlayer.h | 25 ---------- 3 files changed, 33 insertions(+), 70 deletions(-) diff --git a/LenaPi/MusicPlayer.qml b/LenaPi/MusicPlayer.qml index 78f0db4..d37e4c6 100644 --- a/LenaPi/MusicPlayer.qml +++ b/LenaPi/MusicPlayer.qml @@ -64,9 +64,9 @@ Item{ margins: container.margins topMargin: closeAppButton.visible ? 2*container.margins : container.margins } - value: musicModel.pAudioVolume + value: musicModel.volume onValueChanged: { - musicModel.pAudioVolume = value; + musicModel.volume = value; } } PlayerControlPannel { diff --git a/LenaPi/controllers/MusicPlayer.cpp b/LenaPi/controllers/MusicPlayer.cpp index 032dd13..350a03f 100644 --- a/LenaPi/controllers/MusicPlayer.cpp +++ b/LenaPi/controllers/MusicPlayer.cpp @@ -6,10 +6,16 @@ MusicPlayer::MusicPlayer(QObject *parent) : QMediaPlayer(parent) { - // relay base class signal as NOTIFY doesn't seem to be able to handle it directly - connect(this, &QMediaPlayer::volumeChanged, this, &MusicPlayer::audioVolumeChanged); // init audio - setAudioVolume(50); + /// @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(); + }); } void MusicPlayer::init(NavigationItemModel *item) @@ -33,66 +39,54 @@ void MusicPlayer::navigateBack() void MusicPlayer::playPause() { - mIsPlaying = !mIsPlaying; - emit isPlayingChanged(); - if(mIsPlaying) - play(); - else + if(isPlaying()) pause(); + else + play(); } void MusicPlayer::stopMusic() { - if(mIsPlaying){ - mIsPlaying = false; - emit isPlayingChanged(); - - reset(); - - emit stop(); + if(isPlaying()){ + reset(); + stop(); } } void MusicPlayer::playNext() { - //emit next(); - if(!mIsPlaying){ - mIsPlaying = true; - emit isPlayingChanged(); - } + if(!hasNext()) return; // checks if playlist exists + playlist()->next(); } void MusicPlayer::playPrevious() { - //emit previous(); - if(!mIsPlaying){ - mIsPlaying = true; - emit isPlayingChanged(); - } + if(!hasPrevious()) return; + playlist()->previous(); } bool MusicPlayer::isPlaying() const { - return mIsPlaying; + return state() == State::PlayingState; } bool MusicPlayer::hasNext() const { - return mHasNext; + 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 { - return mHasPrevious; -} - -void MusicPlayer::setAudioVolume(int newVolume) -{ - if(newVolume != volume()){ - qDebug() << volume() << newVolume; - setVolume(newVolume); - // signal audioVolumeChanged will be emitted automatically - } + 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 @@ -130,11 +124,6 @@ void MusicPlayer::onLengthChanged(int length) void MusicPlayer::reset() { - mHasNext = false; - mHasPrevious = false; - emit hasNextChanged(); - emit hasPreviousChanged(); - mCurrentMediaItemProgress = 0.0; mCurrentTime = 0.0; emit progressChanged(); @@ -168,7 +157,6 @@ void MusicPlayer::readMedia(const QString& path) for(auto file:files){ auto fileName = file.absoluteFilePath(); auto fileUrl = QUrl::fromLocalFile(fileName); - qDebug() << fileName << fileUrl; playList->addMedia(QMediaContent(fileUrl)); } } diff --git a/LenaPi/controllers/MusicPlayer.h b/LenaPi/controllers/MusicPlayer.h index 8d36b64..499a486 100644 --- a/LenaPi/controllers/MusicPlayer.h +++ b/LenaPi/controllers/MusicPlayer.h @@ -20,7 +20,6 @@ class MusicPlayer : public QMediaPlayer 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); @@ -31,7 +30,6 @@ signals: void progressChanged(); void mediaLengthChanged(); void mediaTitleChanged(); - void audioVolumeChanged(int newVolume); public: MusicPlayer(QObject *parent = Q_NULLPTR); @@ -50,25 +48,6 @@ public: bool hasNext() const; bool hasPrevious() const; - /** - * @brief Get audio volume. Range is in between 0 and 100 - * Relays info from QMediaPlayer::volume as Q_PROPERTY does seem to have - * trouble accessing base class functions directly. - */ - inline int getAudioVolume() const { return volume(); } - /** - * @brief Set audio volume. - * @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. - * - * Relay info to QMediaPlayer::setVolume as Q_PROPERTY does seem to have - * trouble accessing base class functions directly. - * - * @todo save to config file? - */ - void setAudioVolume(int newVolume); - double getProgress() const; QString getMediaTitle() const; @@ -91,10 +70,6 @@ private: void readMedia(const QString& path); QString timeToString(int time); - - bool mIsPlaying = false; - bool mHasNext = false; - bool mHasPrevious = false; int mCurrentMediaItemLength = 0; int mCurrentTime = 0; double mCurrentMediaItemProgress = 0; From 39a0356a518c1c712ddc7f3ca089baed9e4ea376 Mon Sep 17 00:00:00 2001 From: Anika Raemer Date: Wed, 13 Oct 2021 14:06:43 +0200 Subject: [PATCH 04/10] fixed progress --- LenaPi/controllers/MusicPlayer.cpp | 43 ++++++++++-------------------- LenaPi/controllers/MusicPlayer.h | 3 --- 2 files changed, 14 insertions(+), 32 deletions(-) diff --git a/LenaPi/controllers/MusicPlayer.cpp b/LenaPi/controllers/MusicPlayer.cpp index 350a03f..b592542 100644 --- a/LenaPi/controllers/MusicPlayer.cpp +++ b/LenaPi/controllers/MusicPlayer.cpp @@ -16,6 +16,13 @@ MusicPlayer::MusicPlayer(QObject *parent) : QMediaPlayer(parent) 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) @@ -26,7 +33,6 @@ void MusicPlayer::init(NavigationItemModel *item) mCurrentItem = item; emit coverImageSourceChanged(); - reset(); clearMediaList(); readMedia(mCurrentItem->getPath()); @@ -48,7 +54,6 @@ void MusicPlayer::playPause() void MusicPlayer::stopMusic() { if(isPlaying()){ - reset(); stop(); } } @@ -86,12 +91,15 @@ bool MusicPlayer::hasPrevious() const /* player inits with currentIndex -1 and hence previousIndex = last index * until started. Yet, previousButton should initially be disabled. */ - return previousIndex >= 0 && previousIndex < playlist()->currentIndex(); + return previousIndex >= 0 && previousIndex >= playlist()->currentIndex(); } double MusicPlayer::getProgress() const { - return mCurrentMediaItemProgress; + if(duration() <= 0 || position() < 0){ + return 0.0; + } + return (double)position()/duration(); } QString MusicPlayer::getMediaTitle() const @@ -101,35 +109,12 @@ QString MusicPlayer::getMediaTitle() const QString MusicPlayer::getMediaLength() { - return timeToString(mCurrentMediaItemLength); + return timeToString(duration()); } QString MusicPlayer::getTime() { - return timeToString(mCurrentTime); -} - -void MusicPlayer::onTimeChanged(int time) -{ - mCurrentMediaItemProgress = (double) time / mCurrentMediaItemLength; - mCurrentTime = time; - emit progressChanged(); -} - -void MusicPlayer::onLengthChanged(int length) -{ - mCurrentMediaItemLength= length; - emit mediaLengthChanged(); -} - -void MusicPlayer::reset() -{ - mCurrentMediaItemProgress = 0.0; - mCurrentTime = 0.0; - emit progressChanged(); - - mCurrentMediaItemLength = 0.0; - emit mediaLengthChanged(); + return timeToString(position()); } void MusicPlayer::clearMediaList() diff --git a/LenaPi/controllers/MusicPlayer.h b/LenaPi/controllers/MusicPlayer.h index 499a486..912d828 100644 --- a/LenaPi/controllers/MusicPlayer.h +++ b/LenaPi/controllers/MusicPlayer.h @@ -61,9 +61,6 @@ public slots: void playNext(); void playPrevious(); - void onTimeChanged(int time); - void onLengthChanged(int length); - private: void reset(); void clearMediaList(); From a483a380d624ca94eeeeab33d803f4811e6f91bc Mon Sep 17 00:00:00 2001 From: Anika Raemer Date: Wed, 13 Oct 2021 14:13:48 +0200 Subject: [PATCH 05/10] reset playlist to first track on stop --- LenaPi/controllers/MusicPlayer.cpp | 13 ++++++++++--- LenaPi/controllers/MusicPlayer.h | 13 +++++++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/LenaPi/controllers/MusicPlayer.cpp b/LenaPi/controllers/MusicPlayer.cpp index b592542..dbc4d5c 100644 --- a/LenaPi/controllers/MusicPlayer.cpp +++ b/LenaPi/controllers/MusicPlayer.cpp @@ -33,7 +33,7 @@ void MusicPlayer::init(NavigationItemModel *item) mCurrentItem = item; emit coverImageSourceChanged(); - clearMediaList(); + clearPlaylist(); readMedia(mCurrentItem->getPath()); } @@ -55,6 +55,7 @@ void MusicPlayer::stopMusic() { if(isPlaying()){ stop(); + resetPlaylistToFirstTrack(); } } @@ -66,10 +67,16 @@ void MusicPlayer::playNext() void MusicPlayer::playPrevious() { - if(!hasPrevious()) return; + 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; @@ -117,7 +124,7 @@ QString MusicPlayer::getTime() return timeToString(position()); } -void MusicPlayer::clearMediaList() +void MusicPlayer::clearPlaylist() { if(playlist()){ playlist()->clear(); diff --git a/LenaPi/controllers/MusicPlayer.h b/LenaPi/controllers/MusicPlayer.h index 912d828..9e68c03 100644 --- a/LenaPi/controllers/MusicPlayer.h +++ b/LenaPi/controllers/MusicPlayer.h @@ -62,9 +62,18 @@ public slots: void playPrevious(); private: - void reset(); - void clearMediaList(); + /** + * @brief Resets playlist to first track if playlist exists and is non-empty + */ + void resetPlaylistToFirstTrack(); + void clearPlaylist(); void readMedia(const QString& path); + /** + * @brief Transforms time in milliseconds into a string + * @param time Time in ms + * @return + * @todo use QDateTime + */ QString timeToString(int time); int mCurrentMediaItemLength = 0; From 75308b4423bcbfc6c4ec013dfe829193f873bcc9 Mon Sep 17 00:00:00 2001 From: Anika Raemer Date: Wed, 13 Oct 2021 15:06:09 +0200 Subject: [PATCH 06/10] fix previous button, single point of definition --- LenaPi/controllers/MusicPlayer.cpp | 30 +++---- LenaPi/controllers/MusicPlayer.h | 91 +++++++++++++++++---- LenaPi/controllers/NavigationController.cpp | 7 +- LenaPi/controllers/SettingsHandler.h | 6 ++ 4 files changed, 100 insertions(+), 34 deletions(-) diff --git a/LenaPi/controllers/MusicPlayer.cpp b/LenaPi/controllers/MusicPlayer.cpp index dbc4d5c..abbd9df 100644 --- a/LenaPi/controllers/MusicPlayer.cpp +++ b/LenaPi/controllers/MusicPlayer.cpp @@ -1,7 +1,7 @@ #include "MusicPlayer.h" #include #include - +#include MusicPlayer::MusicPlayer(QObject *parent) : QMediaPlayer(parent) @@ -98,7 +98,7 @@ bool MusicPlayer::hasPrevious() const /* player inits with currentIndex -1 and hence previousIndex = last index * until started. Yet, previousButton should initially be disabled. */ - return previousIndex >= 0 && previousIndex >= playlist()->currentIndex(); + return previousIndex >= 0 && previousIndex < playlist()->currentIndex(); } double MusicPlayer::getProgress() const @@ -111,17 +111,17 @@ double MusicPlayer::getProgress() const QString MusicPlayer::getMediaTitle() const { - return mMediaTitle; + return metaData("Title").toString(); } -QString MusicPlayer::getMediaLength() +QString MusicPlayer::getMediaDuration() const { - return timeToString(duration()); + return millisecondsToString(duration()); } -QString MusicPlayer::getTime() +QString MusicPlayer::getPosition() const { - return timeToString(position()); + return millisecondsToString(position()); } void MusicPlayer::clearPlaylist() @@ -144,7 +144,7 @@ void MusicPlayer::readMedia(const QString& path) } // add audio files to playlist - dir.setNameFilters({"*.mp3", "*.flac"}); + dir.setNameFilters(SettingsHandler::getAudioFileNameFilters()); auto files = dir.entryInfoList(QDir::Files); for(auto file:files){ auto fileName = file.absoluteFilePath(); @@ -153,11 +153,13 @@ void MusicPlayer::readMedia(const QString& path) } } -QString MusicPlayer::timeToString(int time) +QString MusicPlayer::millisecondsToString(int timeInMilliseconds) const { - 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; + 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 index 9e68c03..2b5e8c3 100644 --- a/LenaPi/controllers/MusicPlayer.h +++ b/LenaPi/controllers/MusicPlayer.h @@ -6,7 +6,10 @@ #include /** - * @brief MusicPlayer providing interface to QML and Navigation + * @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 { @@ -17,9 +20,9 @@ class MusicPlayer : public QMediaPlayer 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(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); @@ -29,12 +32,16 @@ signals: void isPlayingChanged(); void progressChanged(); void mediaLengthChanged(); - void mediaTitleChanged(); 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); /** @@ -42,23 +49,71 @@ public: * @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; - QString getMediaLength(); - QString getTime(); + /** + * @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: @@ -66,20 +121,22 @@ 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 Transforms time in milliseconds into a string + * @brief Format time in milliseconds acoording to [M]m:ss * @param time Time in ms - * @return - * @todo use QDateTime + * @return formateted string */ - QString timeToString(int time); + QString millisecondsToString(int timeInMilliseconds) const; - int mCurrentMediaItemLength = 0; - int mCurrentTime = 0; - double mCurrentMediaItemProgress = 0; - QString mMediaTitle = QString(""); NavigationItemModel* mCurrentItem = nullptr; }; diff --git a/LenaPi/controllers/NavigationController.cpp b/LenaPi/controllers/NavigationController.cpp index 0dc4a4f..ff2ad3c 100644 --- a/LenaPi/controllers/NavigationController.cpp +++ b/LenaPi/controllers/NavigationController.cpp @@ -1,4 +1,5 @@ #include "NavigationController.h" +#include #include #include @@ -75,18 +76,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; } 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(); From 9e7a55fc20056cc2201c13f9a04da5135a28ef70 Mon Sep 17 00:00:00 2001 From: Jan-Martin Raemer Date: Wed, 1 Jun 2022 20:31:47 +0200 Subject: [PATCH 07/10] AppSettings for android added debug output for Navigation as debugger does not work for Android yet --- LenaPi/LenaPi.pro | 5 +- LenaPi/LenaPi.pro.user | 357 +++++++++++++++----- LenaPi/Navigation.qml | 7 + LenaPi/android-files/AndroidManifest.xml | 84 +++++ LenaPi/controllers/NavigationController.cpp | 9 +- LenaPi/controllers/NavigationController.h | 16 + LenaPi/controllers/SettingsHandler.cpp | 16 +- LenaPi/models/UiStateModel.h | 1 + 8 files changed, 405 insertions(+), 90 deletions(-) create mode 100644 LenaPi/android-files/AndroidManifest.xml diff --git a/LenaPi/LenaPi.pro b/LenaPi/LenaPi.pro index baa2c3a..c21dbe6 100644 --- a/LenaPi/LenaPi.pro +++ b/LenaPi/LenaPi.pro @@ -3,7 +3,7 @@ TEMPLATE = app QT += qml quick multimedia CONFIG += c++11 LIBS += - +ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android-files SOURCES += main.cpp \ controllers/MusicPlayer.cpp \ models/NavigationListModel.cpp \ @@ -51,3 +51,6 @@ HEADERS += \ 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 bcdddb7..a1418e5 100644 --- a/LenaPi/LenaPi.pro.user +++ b/LenaPi/LenaPi.pro.user @@ -1,14 +1,14 @@ - + EnvironmentId - {0510cc5f-8237-43aa-9f4f-ce9a2d4944ec} + {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,6 +46,7 @@ 0 8 true + false 1 true true @@ -59,6 +61,7 @@ true + false true true true @@ -74,7 +77,7 @@ true true Builtin.DefaultTidyAndClazy - 2 + 4 @@ -85,30 +88,44 @@ ProjectExplorer.Project.Target.0 - Desktop - Desktop - Desktop - {ebe72439-b95e-4a0f-b855-a910128856ec} - 0 - 0 - 0 + 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/araemer/source/lenapi/build-LenaPi-Desktop-Debug - /home/araemer/source/lenapi/build-LenaPi-Desktop-Debug + /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 - 2 + + true + Copy application data + Qt4ProjectManager.AndroidPackageInstallationStep + + + android-31 + + true + Build Android APK + QmakeProjectManager.AndroidBuildApkStep + false + + 4 Build Build ProjectExplorer.BuildSteps.Build @@ -119,7 +136,7 @@ Qt4ProjectManager.MakeStep clean - 1 + 1 Clean Clean ProjectExplorer.BuildSteps.Clean @@ -127,6 +144,7 @@ 2 false + false Debug Qt4ProjectManager.Qt4BuildConfiguration @@ -134,21 +152,35 @@ 0 - /home/araemer/source/lenapi/build-LenaPi-Desktop-Release - /home/araemer/source/lenapi/build-LenaPi-Desktop-Release + /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 - 2 + + true + Copy application data + Qt4ProjectManager.AndroidPackageInstallationStep + + + android-31 + + true + Build Android APK + QmakeProjectManager.AndroidBuildApkStep + false + + 4 Build Build ProjectExplorer.BuildSteps.Build @@ -159,7 +191,7 @@ Qt4ProjectManager.MakeStep clean - 1 + 1 Clean Clean ProjectExplorer.BuildSteps.Clean @@ -167,6 +199,7 @@ 2 false + false Release Qt4ProjectManager.Qt4BuildConfiguration @@ -176,13 +209,12 @@ 0 - /home/araemer/source/lenapi/build-LenaPi-Desktop-Profile - /home/araemer/source/lenapi/build-LenaPi-Desktop-Profile + /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 @@ -190,7 +222,20 @@ true Qt4ProjectManager.MakeStep - 2 + + true + Copy application data + Qt4ProjectManager.AndroidPackageInstallationStep + + + android-31 + + true + Build Android APK + QmakeProjectManager.AndroidBuildApkStep + false + + 4 Build Build ProjectExplorer.BuildSteps.Build @@ -201,7 +246,7 @@ Qt4ProjectManager.MakeStep clean - 1 + 1 Clean Clean ProjectExplorer.BuildSteps.Clean @@ -209,6 +254,7 @@ 2 false + false Profile Qt4ProjectManager.Qt4BuildConfiguration @@ -217,10 +263,200 @@ 0 0 - 3 + 3 - 0 + + 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 + + LenaPi + 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.1 + + 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 @@ -230,82 +466,33 @@ false ProjectExplorer.DefaultDeployConfiguration - 1 + 1 dwarf cpu-cycles - - 250 - - -e - cpu-cycles - --call-graph - dwarf,4096 - -F - 250 - -F true - 4096 - false - false - 1000 - true - - false - false - false - false - true - 0.01 - 10 - true - kcachegrind - 1 - 25 - - 1 true - false - true - valgrind - - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 2 - Qt4ProjectManager.Qt4RunConfiguration:/home/araemer/source/lenapi/LenaPi/LenaPi.pro - /home/araemer/source/lenapi/LenaPi/LenaPi.pro + Qt4ProjectManager.Qt4RunConfiguration:/home/jmr/privat/src/LenaPi/LenaPi/LenaPi.pro + /home/jmr/privat/src/LenaPi/LenaPi/LenaPi.pro false true true false true - 1 + 1 ProjectExplorer.Project.TargetCount - 1 + 2 ProjectExplorer.Project.Updater.FileVersion diff --git a/LenaPi/Navigation.qml b/LenaPi/Navigation.qml index 5251493..cd5df5b 100644 --- a/LenaPi/Navigation.qml +++ b/LenaPi/Navigation.qml @@ -1,4 +1,5 @@ import QtQuick 2.0 +import QtQuick.Controls 2.4 /** * @brief Navigation view containing list view displaying artists or genres and albums @@ -39,6 +40,12 @@ Item { color: "#99ffffff" + Label{ + text: debug.pDebugOutput + anchors.centerIn: parent + visible: text !== "" + } + ListView{ id: circleList anchors.left: parent.left diff --git a/LenaPi/android-files/AndroidManifest.xml b/LenaPi/android-files/AndroidManifest.xml new file mode 100644 index 0000000..0236a61 --- /dev/null +++ b/LenaPi/android-files/AndroidManifest.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LenaPi/controllers/NavigationController.cpp b/LenaPi/controllers/NavigationController.cpp index ff2ad3c..0b04961 100644 --- a/LenaPi/controllers/NavigationController.cpp +++ b/LenaPi/controllers/NavigationController.cpp @@ -21,11 +21,17 @@ NavigationController::NavigationController(QObject *parent) : QObject(parent), }); } +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); @@ -49,6 +55,7 @@ void NavigationController::setContextProperties() mContext->setContextProperty("navigationList", mNavList); mContext->setContextProperty("uiStateModel", mUiState); mContext->setContextProperty("musicModel", mMediaPlayer); + mContext->setContextProperty("debug", this); } void NavigationController::add(const QString &path, NavigationItemModel *parentItem) diff --git a/LenaPi/controllers/NavigationController.h b/LenaPi/controllers/NavigationController.h index 6f40c5b..46fbdc2 100644 --- a/LenaPi/controllers/NavigationController.h +++ b/LenaPi/controllers/NavigationController.h @@ -15,6 +15,12 @@ class MusicPlayer; 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,6 +63,12 @@ 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; @@ -63,6 +77,8 @@ private: QString mRootPath = "."; + QString mDebugOutput; + QQmlContext* mContext = nullptr; private slots: 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/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. From bcdf3d94f299fd9ad2d763b9a210129481814d09 Mon Sep 17 00:00:00 2001 From: Jan-Martin Raemer Date: Fri, 3 Jun 2022 22:07:50 +0200 Subject: [PATCH 08/10] Scale all QML elements according to the current display size --- .gitignore | 4 +- LenaPi/LenaPi.pro | 2 + LenaPi/MediaProgress.qml | 47 +++++++------- LenaPi/MusicPlayer.qml | 4 +- LenaPi/MyScrollView.qml | 16 ++--- LenaPi/Navigation.qml | 10 +-- LenaPi/NavigationListDelegate.qml | 6 +- LenaPi/PlayerButtons.qml | 80 +++++++++++------------ LenaPi/PlayerControlPannel.qml | 38 +++++------ LenaPi/RoundImageButton.qml | 4 +- LenaPi/VolumeSlider.qml | 2 +- LenaPi/controllers/StyleController.cpp | 88 ++++++++++++++++++++++++++ LenaPi/controllers/StyleController.h | 78 +++++++++++++++++++++++ LenaPi/main.cpp | 4 ++ LenaPi/main.qml | 4 +- 15 files changed, 278 insertions(+), 109 deletions(-) create mode 100644 LenaPi/controllers/StyleController.cpp create mode 100644 LenaPi/controllers/StyleController.h diff --git a/.gitignore b/.gitignore index cf45d4e..ebd8b13 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +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/LenaPi.pro b/LenaPi/LenaPi.pro index c21dbe6..1bc3c7b 100644 --- a/LenaPi/LenaPi.pro +++ b/LenaPi/LenaPi.pro @@ -6,6 +6,7 @@ LIBS += ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android-files SOURCES += main.cpp \ controllers/MusicPlayer.cpp \ + controllers/StyleController.cpp \ models/NavigationListModel.cpp \ models/NavigationItemModel.cpp \ controllers/NavigationController.cpp \ @@ -41,6 +42,7 @@ else: unix:!android: target.path = /opt/$${TARGET}/bin HEADERS += \ controllers/MusicPlayer.h \ + controllers/StyleController.h \ models/MusicPlayer.h \ models/NavigationListModel.h \ models/NavigationItemModel.h \ 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/MusicPlayer.qml b/LenaPi/MusicPlayer.qml index d37e4c6..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,7 +43,7 @@ Item{ Image{ id: cover anchors.centerIn: parent - height: parent.height-10 + height: parent.height-StyleMargins.smallMargin width: height source: musicModel.pCoverImageSource fillMode: Image.PreserveAspectCrop 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 cd5df5b..8e6f283 100644 --- a/LenaPi/Navigation.qml +++ b/LenaPi/Navigation.qml @@ -6,7 +6,7 @@ import QtQuick.Controls 2.4 */ Item { id: container - property int margins: 20 + property int margins: StyleMargins.defaultMargin RoundImageButton{ id: back @@ -36,7 +36,7 @@ Item { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - height: 210 + height: StyleSizes.navigationListHeight color: "#99ffffff" @@ -51,12 +51,12 @@ Item { 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 1667df5..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" diff --git a/LenaPi/controllers/StyleController.cpp b/LenaPi/controllers/StyleController.cpp new file mode 100644 index 0000000..764818f --- /dev/null +++ b/LenaPi/controllers/StyleController.cpp @@ -0,0 +1,88 @@ +#include "StyleController.h" +#include +#include + +StyleController::StyleController(QObject *parent) + : QObject{parent}, mStyleSizes(new QQmlPropertyMap(this)), + mMargins(new QQmlPropertyMap(this)), mSpacings(new QQmlPropertyMap(this)), mPaddings(new QQmlPropertyMap(this)) +{ + // nothing +} + +void StyleController::calculateAndSetRatio() +{ + qreal refHeight = 480; + qreal refWidth = 800; + // Scales to fullscreen. No rescaling when changing window size + QRect rect = QGuiApplication::primaryScreen()->geometry(); + qreal height = qMax(rect.width(),rect.height()); + qreal width = qMin(rect.width(), rect.height()); + + mRatio = qMin(height/refHeight, width/refWidth); +} + +void StyleController::initStyleSizes() +{ + scaleAndInsert(mStyleSizes, "roundButtonDefaultSize", 65); + scaleAndInsert(mStyleSizes, "roundButtonBorderWidth", 2); + scaleAndInsert(mStyleSizes, "smallPlayerButtonSize", 60); + scaleAndInsert(mStyleSizes, "largePlayerButtonSize", 80); + + scaleAndInsert(mStyleSizes, "scrollHandleWidth", 16); + scaleAndInsert(mStyleSizes, "scrollHandleHeight", 8); + scaleAndInsert(mStyleSizes, "scrollHandleBorderWidth", 1); + + int progressBackgroundDefaultHeight = 10; + scaleAndInsert(mStyleSizes, "progressBackgroundDefaultHeight", progressBackgroundDefaultHeight); + scaleAndInsert(mStyleSizes, "progressBackgroundRadius", progressBackgroundDefaultHeight/2); + scaleAndInsert(mStyleSizes, "progressBarDefaultHeight", 8); + scaleAndInsert(mStyleSizes, "progressBackgroundBorderWidth", 1); + + scaleAndInsert(mStyleSizes, "navigationListHeight", 210); + scaleAndInsert(mStyleSizes, "navigationDelegateDefaultSize", 150); + +} + +void StyleController::initSpacings() +{ + scaleAndInsert(mSpacings, "defaultSpacing", 20); + scaleAndInsert(mSpacings, "smallSpacing", 10); + scaleAndInsert(mSpacings, "tinySpacing", 5); +} + +void StyleController::initMargins() +{ + scaleAndInsert(mMargins, "defaultMargin", 20); + scaleAndInsert(mMargins, "smallMargin", 10); + scaleAndInsert(mMargins, "tinyMargin", 5); + scaleAndInsert(mMargins, "scrollHandleMargins", 1); +} + +void StyleController::initPaddings() +{ + scaleAndInsert(mPaddings, "defaultPadding", 5); +} + +void StyleController::scaleAndInsert(QQmlPropertyMap *map, const QString &key, int value) +{ + map->insert(key, applyRatio(value)); +} + +int StyleController::applyRatio(int size) const +{ + return size*mRatio; +} + +void StyleController::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/StyleController.h b/LenaPi/controllers/StyleController.h new file mode 100644 index 0000000..aa4714f --- /dev/null +++ b/LenaPi/controllers/StyleController.h @@ -0,0 +1,78 @@ +#ifndef STYLECONTROLLER_H +#define STYLECONTROLLER_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 + */ +class StyleController : public QObject +{ + Q_OBJECT +public: + explicit StyleController(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 // STYLECONTROLLER_H diff --git a/LenaPi/main.cpp b/LenaPi/main.cpp index dee97c8..c1aafb2 100644 --- a/LenaPi/main.cpp +++ b/LenaPi/main.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "controllers/NavigationController.h" #include "MouseEventSpy.h" #include "EnergySaver.h" @@ -42,6 +43,9 @@ int main(int argc, char *argv[]) const auto settingsHandler = SettingsHandler::createSettingsHandlerAndFillWithDefaultsIfMissing(settings); + // init style + StyleController styleController; + styleController.init(engine.rootContext()); // init main app NavigationController navController; navController.setContext(engine.rootContext()); 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(); From 851b83a53a8df29a1dcbcf45762394e7ee4cf4a2 Mon Sep 17 00:00:00 2001 From: Jan-Martin Raemer Date: Fri, 3 Jun 2022 22:09:54 +0200 Subject: [PATCH 09/10] added todos --- LenaPi/controllers/StyleController.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/LenaPi/controllers/StyleController.h b/LenaPi/controllers/StyleController.h index aa4714f..4cc5ffd 100644 --- a/LenaPi/controllers/StyleController.h +++ b/LenaPi/controllers/StyleController.h @@ -16,6 +16,9 @@ * 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 StyleController : public QObject { From 1f12d9330046999451db42fe6ace0f47a5bda9af Mon Sep 17 00:00:00 2001 From: Jan-Martin Raemer Date: Sat, 16 Jul 2022 12:19:57 +0200 Subject: [PATCH 10/10] Make LenaPi usable on Android devices - Add app icon - prevend android device from shutting down cpu while playing music - fix scaling bug - fix energy saver for RasPi by reconnecting to music player - minor refactorings and renaming --- LenaPi/.qmake.stash | 24 ++ LenaPi/EnergySaver.cpp | 90 ++++++- LenaPi/EnergySaver.h | 64 ++++- LenaPi/LenaPi.pro | 12 +- LenaPi/LenaPi.pro.user | 251 +++++++++++++++++- LenaPi/MouseEventSpy.cpp | 2 +- LenaPi/android-files/AndroidManifest.xml | 4 +- .../android-files/res/drawable-hdpi/icon.png | Bin 0 -> 6858 bytes .../android-files/res/drawable-ldpi/icon.png | Bin 0 -> 2094 bytes .../android-files/res/drawable-mdpi/icon.png | Bin 0 -> 3637 bytes .../android-files/res/drawable-xhdpi/icon.png | Bin 0 -> 11054 bytes .../res/drawable-xxhdpi/icon.png | Bin 0 -> 22663 bytes .../res/drawable-xxxhdpi/icon.png | Bin 0 -> 38223 bytes LenaPi/controllers/NavigationController.cpp | 17 ++ LenaPi/controllers/NavigationController.h | 1 + ...{StyleController.cpp => StyleHandling.cpp} | 26 +- .../{StyleController.h => StyleHandling.h} | 10 +- LenaPi/main.cpp | 46 +++- LenaPi/resources/icon.jpeg | Bin 0 -> 37006 bytes 19 files changed, 499 insertions(+), 48 deletions(-) create mode 100644 LenaPi/.qmake.stash create mode 100644 LenaPi/android-files/res/drawable-hdpi/icon.png create mode 100644 LenaPi/android-files/res/drawable-ldpi/icon.png create mode 100644 LenaPi/android-files/res/drawable-mdpi/icon.png create mode 100644 LenaPi/android-files/res/drawable-xhdpi/icon.png create mode 100644 LenaPi/android-files/res/drawable-xxhdpi/icon.png create mode 100644 LenaPi/android-files/res/drawable-xxxhdpi/icon.png rename LenaPi/controllers/{StyleController.cpp => StyleHandling.cpp} (78%) rename LenaPi/controllers/{StyleController.h => StyleHandling.h} (93%) create mode 100644 LenaPi/resources/icon.jpeg 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 1bc3c7b..e603e36 100644 --- a/LenaPi/LenaPi.pro +++ b/LenaPi/LenaPi.pro @@ -1,12 +1,16 @@ TEMPLATE = app - +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 += -ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android-files SOURCES += main.cpp \ controllers/MusicPlayer.cpp \ - controllers/StyleController.cpp \ + controllers/StyleHandling.cpp \ models/NavigationListModel.cpp \ models/NavigationItemModel.cpp \ controllers/NavigationController.cpp \ @@ -42,7 +46,7 @@ else: unix:!android: target.path = /opt/$${TARGET}/bin HEADERS += \ controllers/MusicPlayer.h \ - controllers/StyleController.h \ + controllers/StyleHandling.h \ models/MusicPlayer.h \ models/NavigationListModel.h \ models/NavigationItemModel.h \ diff --git a/LenaPi/LenaPi.pro.user b/LenaPi/LenaPi.pro.user index a1418e5..73f8196 100644 --- a/LenaPi/LenaPi.pro.user +++ b/LenaPi/LenaPi.pro.user @@ -1,6 +1,6 @@ - + EnvironmentId @@ -87,6 +87,250 @@ ProjectExplorer.Project.Target.0 + + GenericLinuxOsType + armhf/raspi 10.0.1.146 + armhf/raspi 10.0.1.146 + {9f02fab2-7f72-4b24-bbd8-dbe33e863260} + 1 + 0 + 0 + + 0 + /home/jmr/privat/src/LenaPi/build-LenaPi-Unnamed-Debug + /home/jmr/privat/src/LenaPi/build-LenaPi-Unnamed-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 + + + /home/jmr/privat/src/LenaPi/build-LenaPi-Unnamed-Release + /home/jmr/privat/src/LenaPi/build-LenaPi-Unnamed-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 + /home/jmr/privat/src/LenaPi/build-LenaPi-Unnamed-Profile + /home/jmr/privat/src/LenaPi/build-LenaPi-Unnamed-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 + + 3 + + + + true + RemoteLinux.CheckForFreeDiskSpaceStep + + + + + / + 5242880 + + + + + true + RemoteLinux.KillAppStep + + + + + + + + + true + RemoteLinux.RsyncDeployStep + + + + + + + -av + + 3 + Deploy + Deploy + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + DeployToGenericLinux + + 1 + + true + true + true + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + + 2 + + 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 @@ -309,7 +553,6 @@ 0 - LenaPi Qt4ProjectManager.AndroidRunConfiguration:/home/jmr/privat/src/LenaPi/LenaPi/LenaPi.pro /home/jmr/privat/src/LenaPi/LenaPi/LenaPi.pro false @@ -321,7 +564,7 @@ - ProjectExplorer.Project.Target.1 + ProjectExplorer.Project.Target.2 Desktop desktop @@ -492,7 +735,7 @@ ProjectExplorer.Project.TargetCount - 2 + 3 ProjectExplorer.Project.Updater.FileVersion 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/android-files/AndroidManifest.xml b/LenaPi/android-files/AndroidManifest.xml index 0236a61..e0c143a 100644 --- a/LenaPi/android-files/AndroidManifest.xml +++ b/LenaPi/android-files/AndroidManifest.xml @@ -15,7 +15,7 @@ - + @@ -81,4 +81,6 @@ + + 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 0000000000000000000000000000000000000000..b22cf597623785f5d7d2e8ba534559700b3958e4 GIT binary patch literal 6858 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 Eg7~g8^8f$< literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7c9ca0fef6c2eb15300f8e36603563752d0e25ff GIT binary patch literal 2094 zcmV+}2+{Y6P)gO8j3^Xc6PVaX;SOd)&PPgnF5& zX2&?7YL=0T$HYu_RSdi$fL`*J%zTjzugX2@x`?D4`4sQCc-pOr&T(=HVZ5 z{7G`jM<`C{82BS2smXw+=```ES{CxHJMxYAnw zN*$Q_B)!(s!bd>gHgIv>(&Rnhat9cE(j`N3BtK0dp9kL0=$o>@z%9_d=JwXy$LRx* zrmhk7z>k000SaNLh0L01FZT01FZU(%pXi z000I_Nkl;N`tEMp@l-FiccUu z5LIePkx(B5BveExrHF5hRFxnaDjKN*wTgI&^g$yqX=wdO-C#F~WACol_P)Gc@B8lF zyZ4?md{{Se?3lI^^XXpAIlq}XGv}{^oO9U2h^X;_0D!pHvfU=^=^-GZ*I)R>Ml#;` z`9DAj2@nQ9f7jH^q@t>y0SSP6mQO$c2t-7Si;MjO1Nf;=VDIt~5CITT5Ue(vEX#WY zrrk%gH*}kMz20;>y%R`8K)R-X@b3$%QrqqlKMfQSx7Xg7iexgG&1Or*LN&2O2M>Q{ zC=uNlFCxn4^UKT2fk5EEfdc>laQBY&H=zU^2?2l*5by>p?Q9nOb+Y71WJ%P99b%W{v?4gf@r?jyL}Z5L*yEe_|x+^oT1;s-{J zf`Pa6uQ;zKV`(s8u~w_5Vn>4Pjz?XxSh~D$ec%55>10At>r!*G)#0crN?i~E2mrTk zRJl@e1iMP<73JJ-3LpIa$~)&;+uKEROISMU9qhh*J{z8qNBUb^1x;3s6Ys~GTU*1C zu*K!_c7XuA$&WVTAd(KLAfHw1sr^q*gN z@cuESlqgo%ax^cs1gGYr`I7wo=T7Ewo17>Z3>u8aY%)3I_xinFB;1-)0*bCBBI_Bs zQiv}t1wtX4#XNWJY(^7UZN+LeP~9M9*VQ&hsCTHd_nsF|ojm^3vmT!x5vrv!RBOZg zy6rX_-pCZ7G03!$%Y~CE-5^=*HUi47uRr&_hrJG4H|6|R6Eao=%xLB{G(GUGhlj?G z=yJ(wl)N6d+u3$QC2n3*1Rw;r%i(r9%auwho7HtDC)d7ow9jg_x1`cSZY{6ysZ_Zj zmd4v`QneB=8+|>ahynoZklNdbG!f1HXZFpv{u+;7zH`K~nyO(zvFA25LCTiPt|l>H z6E4)=`bZUViD+4?3Rq`Fx)M1t8+w8!hFpU&7%e{$mIKl@QGm%D|+Z#BmBmruX=(6_&S@!~|KQvT|FUxz8$g)%9+k|rZdKvrfX0C_{sC<-)3xkRF+PsA3F9; zS+1}~pk21Jd;}Cw07N2s<8ObOo_@cxGt{`oTMG~qiTKL$+{x#ExwT`r?T*7aXI!7T zH2sx(kDNXG2gVp|A*Izwz`iaLMDVNK)-G2MG6cN?y^}43ESXvryiCzAiiSOS2 YUniBultZM^@Bjb+07*qoM6N<$g2b`>(EtDd literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..57a20d1b0d9097d9f680d469f5fb81f975105ac8 GIT binary patch literal 3637 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 Hu0mjfV@vC& literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3ebfa9675f4ff79afe5b24d413235f6da82f3075 GIT binary patch literal 11054 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

6ndlMRBrzIn%v9q#mX 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

Vga2m!Dsh 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 #include +#include #include #include @@ -19,6 +20,9 @@ NavigationController::NavigationController(QObject *parent) : QObject(parent), 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) @@ -111,3 +115,16 @@ void NavigationController::onNavigationRequest() 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 46fbdc2..0459f3d 100644 --- a/LenaPi/controllers/NavigationController.h +++ b/LenaPi/controllers/NavigationController.h @@ -86,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/StyleController.cpp b/LenaPi/controllers/StyleHandling.cpp similarity index 78% rename from LenaPi/controllers/StyleController.cpp rename to LenaPi/controllers/StyleHandling.cpp index 764818f..19d4419 100644 --- a/LenaPi/controllers/StyleController.cpp +++ b/LenaPi/controllers/StyleHandling.cpp @@ -1,27 +1,29 @@ -#include "StyleController.h" +#include "StyleHandling.h" #include #include +#include -StyleController::StyleController(QObject *parent) +StyleHandling::StyleHandling(QObject *parent) : QObject{parent}, mStyleSizes(new QQmlPropertyMap(this)), mMargins(new QQmlPropertyMap(this)), mSpacings(new QQmlPropertyMap(this)), mPaddings(new QQmlPropertyMap(this)) { // nothing } -void StyleController::calculateAndSetRatio() +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 = qMax(rect.width(),rect.height()); - qreal width = qMin(rect.width(), rect.height()); + 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 StyleController::applyRatio(int size) const +int StyleHandling::applyRatio(int size) const { return size*mRatio; } -void StyleController::init(QQmlContext *context) +void StyleHandling::init(QQmlContext *context) { calculateAndSetRatio(); initStyleSizes(); diff --git a/LenaPi/controllers/StyleController.h b/LenaPi/controllers/StyleHandling.h similarity index 93% rename from LenaPi/controllers/StyleController.h rename to LenaPi/controllers/StyleHandling.h index 4cc5ffd..2128b05 100644 --- a/LenaPi/controllers/StyleController.h +++ b/LenaPi/controllers/StyleHandling.h @@ -1,5 +1,5 @@ -#ifndef STYLECONTROLLER_H -#define STYLECONTROLLER_H +#ifndef STYLEHANDLING_H +#define STYLEHANDLING_H #include #include @@ -20,11 +20,11 @@ * @todo scale fonts as well? * @todo use dpi for scaling as app is very small on Android? */ -class StyleController : public QObject +class StyleHandling : public QObject { Q_OBJECT public: - explicit StyleController(QObject *parent = nullptr); + explicit StyleHandling(QObject *parent = nullptr); /** * @brief Calculates ratio, initializes all maps and registers them in QML context @@ -78,4 +78,4 @@ private: QQmlPropertyMap* mPaddings = nullptr; }; -#endif // STYLECONTROLLER_H +#endif // STYLEHANDLING_H diff --git a/LenaPi/main.cpp b/LenaPi/main.cpp index c1aafb2..b125dc7 100644 --- a/LenaPi/main.cpp +++ b/LenaPi/main.cpp @@ -4,18 +4,20 @@ #include #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; + /**************************************************************************** + * Configure and parse commandline arguments + ****************************************************************************/ QCommandLineParser parser; parser.setApplicationDescription("Lena's music app"); // Define a custom config file using -c or --config @@ -25,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. @@ -35,23 +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 style - StyleController styleController; - styleController.init(engine.rootContext()); - // 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. @@ -60,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/resources/icon.jpeg b/LenaPi/resources/icon.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..42118e0672b7213164855b4b8ee5b6abc3883873 GIT binary patch literal 37006 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 QhmTePR8DvO9e+3f53}EQ3IG5A literal 0 HcmV?d00001