mirror of
https://github.com/belsabbagh/dotfiles.git
synced 2026-04-11 09:36:46 +00:00
quickshell and hyprland additions
This commit is contained in:
1
.config/quickshell/caelestia/plugin/CMakeLists.txt
Normal file
1
.config/quickshell/caelestia/plugin/CMakeLists.txt
Normal file
@@ -0,0 +1 @@
|
||||
add_subdirectory(src/Caelestia)
|
||||
@@ -0,0 +1,62 @@
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Qml Gui Quick Concurrent Sql Network DBus)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
|
||||
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
|
||||
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
|
||||
pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET)
|
||||
if(NOT Cava_FOUND)
|
||||
pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED)
|
||||
endif()
|
||||
|
||||
set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml")
|
||||
qt_standard_project_setup(REQUIRES 6.9)
|
||||
|
||||
function(qml_module arg_TARGET)
|
||||
cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;LIBRARIES")
|
||||
|
||||
qt_add_qml_module(${arg_TARGET}
|
||||
URI ${arg_URI}
|
||||
VERSION ${VERSION}
|
||||
SOURCES ${arg_SOURCES}
|
||||
)
|
||||
|
||||
qt_query_qml_module(${arg_TARGET}
|
||||
URI module_uri
|
||||
VERSION module_version
|
||||
PLUGIN_TARGET module_plugin_target
|
||||
TARGET_PATH module_target_path
|
||||
QMLDIR module_qmldir
|
||||
TYPEINFO module_typeinfo
|
||||
)
|
||||
|
||||
message(STATUS "Created QML module ${module_uri}, version ${module_version}")
|
||||
|
||||
set(module_dir "${INSTALL_QMLDIR}/${module_target_path}")
|
||||
install(TARGETS ${arg_TARGET} LIBRARY DESTINATION "${module_dir}" RUNTIME DESTINATION "${module_dir}")
|
||||
install(TARGETS "${module_plugin_target}" LIBRARY DESTINATION "${module_dir}" RUNTIME DESTINATION "${module_dir}")
|
||||
install(FILES "${module_qmldir}" DESTINATION "${module_dir}")
|
||||
install(FILES "${module_typeinfo}" DESTINATION "${module_dir}")
|
||||
|
||||
target_link_libraries(${arg_TARGET} PRIVATE Qt::Core Qt::Qml ${arg_LIBRARIES})
|
||||
endfunction()
|
||||
|
||||
qml_module(caelestia
|
||||
URI Caelestia
|
||||
SOURCES
|
||||
cutils.hpp cutils.cpp
|
||||
qalculator.hpp qalculator.cpp
|
||||
appdb.hpp appdb.cpp
|
||||
requests.hpp requests.cpp
|
||||
toaster.hpp toaster.cpp
|
||||
imageanalyser.hpp imageanalyser.cpp
|
||||
LIBRARIES
|
||||
Qt::Gui
|
||||
Qt::Quick
|
||||
Qt::Concurrent
|
||||
Qt::Sql
|
||||
PkgConfig::Qalculate
|
||||
)
|
||||
|
||||
add_subdirectory(Internal)
|
||||
add_subdirectory(Models)
|
||||
add_subdirectory(Services)
|
||||
@@ -0,0 +1,15 @@
|
||||
qml_module(caelestia-internal
|
||||
URI Caelestia.Internal
|
||||
SOURCES
|
||||
cachingimagemanager.hpp cachingimagemanager.cpp
|
||||
circularindicatormanager.hpp circularindicatormanager.cpp
|
||||
hyprdevices.hpp hyprdevices.cpp
|
||||
hyprextras.hpp hyprextras.cpp
|
||||
logindmanager.hpp logindmanager.cpp
|
||||
LIBRARIES
|
||||
Qt::Gui
|
||||
Qt::Quick
|
||||
Qt::Concurrent
|
||||
Qt::Network
|
||||
Qt::DBus
|
||||
)
|
||||
@@ -0,0 +1,223 @@
|
||||
#include "cachingimagemanager.hpp"
|
||||
|
||||
#include <QtQuick/qquickwindow.h>
|
||||
#include <qcryptographichash.h>
|
||||
#include <qdir.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qfuturewatcher.h>
|
||||
#include <qimagereader.h>
|
||||
#include <qpainter.h>
|
||||
#include <qtconcurrentrun.h>
|
||||
|
||||
namespace caelestia::internal {
|
||||
|
||||
qreal CachingImageManager::effectiveScale() const {
|
||||
if (m_item && m_item->window()) {
|
||||
return m_item->window()->devicePixelRatio();
|
||||
}
|
||||
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
QSize CachingImageManager::effectiveSize() const {
|
||||
if (!m_item) {
|
||||
return QSize();
|
||||
}
|
||||
|
||||
const qreal scale = effectiveScale();
|
||||
const QSize size = QSizeF(m_item->width() * scale, m_item->height() * scale).toSize();
|
||||
m_item->setProperty("sourceSize", size);
|
||||
return size;
|
||||
}
|
||||
|
||||
QQuickItem* CachingImageManager::item() const {
|
||||
return m_item;
|
||||
}
|
||||
|
||||
void CachingImageManager::setItem(QQuickItem* item) {
|
||||
if (m_item == item) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_widthConn) {
|
||||
disconnect(m_widthConn);
|
||||
}
|
||||
if (m_heightConn) {
|
||||
disconnect(m_heightConn);
|
||||
}
|
||||
|
||||
m_item = item;
|
||||
emit itemChanged();
|
||||
|
||||
if (item) {
|
||||
m_widthConn = connect(item, &QQuickItem::widthChanged, this, [this]() {
|
||||
updateSource();
|
||||
});
|
||||
m_heightConn = connect(item, &QQuickItem::heightChanged, this, [this]() {
|
||||
updateSource();
|
||||
});
|
||||
updateSource();
|
||||
}
|
||||
}
|
||||
|
||||
QUrl CachingImageManager::cacheDir() const {
|
||||
return m_cacheDir;
|
||||
}
|
||||
|
||||
void CachingImageManager::setCacheDir(const QUrl& cacheDir) {
|
||||
if (m_cacheDir == cacheDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_cacheDir = cacheDir;
|
||||
if (!m_cacheDir.path().endsWith("/")) {
|
||||
m_cacheDir.setPath(m_cacheDir.path() + "/");
|
||||
}
|
||||
emit cacheDirChanged();
|
||||
}
|
||||
|
||||
QString CachingImageManager::path() const {
|
||||
return m_path;
|
||||
}
|
||||
|
||||
void CachingImageManager::setPath(const QString& path) {
|
||||
if (m_path == path) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_path = path;
|
||||
emit pathChanged();
|
||||
|
||||
if (!path.isEmpty()) {
|
||||
updateSource(path);
|
||||
}
|
||||
}
|
||||
|
||||
void CachingImageManager::updateSource() {
|
||||
updateSource(m_path);
|
||||
}
|
||||
|
||||
void CachingImageManager::updateSource(const QString& path) {
|
||||
if (path.isEmpty() || path == m_shaPath) {
|
||||
// Path is empty or already calculating sha for path
|
||||
return;
|
||||
}
|
||||
|
||||
m_shaPath = path;
|
||||
|
||||
const auto future = QtConcurrent::run(&CachingImageManager::sha256sum, path);
|
||||
|
||||
const auto watcher = new QFutureWatcher<QString>(this);
|
||||
|
||||
connect(watcher, &QFutureWatcher<QString>::finished, this, [watcher, path, this]() {
|
||||
if (m_path != path) {
|
||||
// Object is destroyed or path has changed, ignore
|
||||
watcher->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
const QSize size = effectiveSize();
|
||||
|
||||
if (!m_item || !size.width() || !size.height()) {
|
||||
watcher->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
const QString fillMode = m_item->property("fillMode").toString();
|
||||
// clang-format off
|
||||
const QString filename = QString("%1@%2x%3-%4.png")
|
||||
.arg(watcher->result()).arg(size.width()).arg(size.height())
|
||||
.arg(fillMode == "PreserveAspectCrop" ? "crop" : fillMode == "PreserveAspectFit" ? "fit" : "stretch");
|
||||
// clang-format on
|
||||
|
||||
const QUrl cache = m_cacheDir.resolved(QUrl(filename));
|
||||
if (m_cachePath == cache) {
|
||||
watcher->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
m_cachePath = cache;
|
||||
emit cachePathChanged();
|
||||
|
||||
if (!cache.isLocalFile()) {
|
||||
qWarning() << "CachingImageManager::updateSource: cachePath" << cache << "is not a local file";
|
||||
watcher->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
const QImageReader reader(cache.toLocalFile());
|
||||
if (reader.canRead()) {
|
||||
m_item->setProperty("source", cache);
|
||||
} else {
|
||||
m_item->setProperty("source", QUrl::fromLocalFile(path));
|
||||
createCache(path, cache.toLocalFile(), fillMode, size);
|
||||
}
|
||||
|
||||
// Clear current running sha if same
|
||||
if (m_shaPath == path) {
|
||||
m_shaPath = QString();
|
||||
}
|
||||
|
||||
watcher->deleteLater();
|
||||
});
|
||||
|
||||
watcher->setFuture(future);
|
||||
}
|
||||
|
||||
QUrl CachingImageManager::cachePath() const {
|
||||
return m_cachePath;
|
||||
}
|
||||
|
||||
void CachingImageManager::createCache(
|
||||
const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const {
|
||||
QThreadPool::globalInstance()->start([path, cache, fillMode, size] {
|
||||
QImage image(path);
|
||||
|
||||
if (image.isNull()) {
|
||||
qWarning() << "CachingImageManager::createCache: failed to read" << path;
|
||||
return;
|
||||
}
|
||||
|
||||
image.convertTo(QImage::Format_ARGB32);
|
||||
|
||||
if (fillMode == "PreserveAspectCrop") {
|
||||
image = image.scaled(size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
|
||||
} else if (fillMode == "PreserveAspectFit") {
|
||||
image = image.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
} else {
|
||||
image = image.scaled(size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
if (fillMode == "PreserveAspectCrop" || fillMode == "PreserveAspectFit") {
|
||||
QImage canvas(size, QImage::Format_ARGB32);
|
||||
canvas.fill(Qt::transparent);
|
||||
|
||||
QPainter painter(&canvas);
|
||||
painter.drawImage((size.width() - image.width()) / 2, (size.height() - image.height()) / 2, image);
|
||||
painter.end();
|
||||
|
||||
image = canvas;
|
||||
}
|
||||
|
||||
const QString parent = QFileInfo(cache).absolutePath();
|
||||
if (!QDir().mkpath(parent) || !image.save(cache)) {
|
||||
qWarning() << "CachingImageManager::createCache: failed to save to" << cache;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QString CachingImageManager::sha256sum(const QString& path) {
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
qWarning() << "CachingImageManager::sha256sum: failed to open" << path;
|
||||
return "";
|
||||
}
|
||||
|
||||
QCryptographicHash hash(QCryptographicHash::Sha256);
|
||||
hash.addData(&file);
|
||||
file.close();
|
||||
|
||||
return hash.result().toHex();
|
||||
}
|
||||
|
||||
} // namespace caelestia::internal
|
||||
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
|
||||
#include <QtQuick/qquickitem.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace caelestia::internal {
|
||||
|
||||
class CachingImageManager : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(QQuickItem* item READ item WRITE setItem NOTIFY itemChanged REQUIRED)
|
||||
Q_PROPERTY(QUrl cacheDir READ cacheDir WRITE setCacheDir NOTIFY cacheDirChanged REQUIRED)
|
||||
|
||||
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
|
||||
Q_PROPERTY(QUrl cachePath READ cachePath NOTIFY cachePathChanged)
|
||||
|
||||
public:
|
||||
explicit CachingImageManager(QObject* parent = nullptr)
|
||||
: QObject(parent)
|
||||
, m_item(nullptr) {}
|
||||
|
||||
[[nodiscard]] QQuickItem* item() const;
|
||||
void setItem(QQuickItem* item);
|
||||
|
||||
[[nodiscard]] QUrl cacheDir() const;
|
||||
void setCacheDir(const QUrl& cacheDir);
|
||||
|
||||
[[nodiscard]] QString path() const;
|
||||
void setPath(const QString& path);
|
||||
|
||||
[[nodiscard]] QUrl cachePath() const;
|
||||
|
||||
Q_INVOKABLE void updateSource();
|
||||
Q_INVOKABLE void updateSource(const QString& path);
|
||||
|
||||
signals:
|
||||
void itemChanged();
|
||||
void cacheDirChanged();
|
||||
|
||||
void pathChanged();
|
||||
void cachePathChanged();
|
||||
void usingCacheChanged();
|
||||
|
||||
private:
|
||||
QString m_shaPath;
|
||||
|
||||
QQuickItem* m_item;
|
||||
QUrl m_cacheDir;
|
||||
|
||||
QString m_path;
|
||||
QUrl m_cachePath;
|
||||
|
||||
QMetaObject::Connection m_widthConn;
|
||||
QMetaObject::Connection m_heightConn;
|
||||
|
||||
[[nodiscard]] qreal effectiveScale() const;
|
||||
[[nodiscard]] QSize effectiveSize() const;
|
||||
|
||||
void createCache(const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const;
|
||||
[[nodiscard]] static QString sha256sum(const QString& path);
|
||||
};
|
||||
|
||||
} // namespace caelestia::internal
|
||||
@@ -0,0 +1,211 @@
|
||||
#include "circularindicatormanager.hpp"
|
||||
#include <qeasingcurve.h>
|
||||
#include <qpoint.h>
|
||||
|
||||
namespace {
|
||||
|
||||
namespace advance {
|
||||
|
||||
constexpr qint32 TOTAL_CYCLES = 4;
|
||||
constexpr qint32 TOTAL_DURATION_IN_MS = 5400;
|
||||
constexpr qint32 DURATION_TO_EXPAND_IN_MS = 667;
|
||||
constexpr qint32 DURATION_TO_COLLAPSE_IN_MS = 667;
|
||||
constexpr qint32 DURATION_TO_COMPLETE_END_IN_MS = 333;
|
||||
constexpr qint32 TAIL_DEGREES_OFFSET = -20;
|
||||
constexpr qint32 EXTRA_DEGREES_PER_CYCLE = 250;
|
||||
constexpr qint32 CONSTANT_ROTATION_DEGREES = 1520;
|
||||
|
||||
constexpr std::array<qint32, TOTAL_CYCLES> DELAY_TO_EXPAND_IN_MS = { 0, 1350, 2700, 4050 };
|
||||
constexpr std::array<qint32, TOTAL_CYCLES> DELAY_TO_COLLAPSE_IN_MS = { 667, 2017, 3367, 4717 };
|
||||
|
||||
} // namespace advance
|
||||
|
||||
namespace retreat {
|
||||
|
||||
constexpr qint32 TOTAL_DURATION_IN_MS = 6000;
|
||||
constexpr qint32 DURATION_SPIN_IN_MS = 500;
|
||||
constexpr qint32 DURATION_GROW_ACTIVE_IN_MS = 3000;
|
||||
constexpr qint32 DURATION_SHRINK_ACTIVE_IN_MS = 3000;
|
||||
constexpr std::array DELAY_SPINS_IN_MS = { 0, 1500, 3000, 4500 };
|
||||
constexpr qint32 DELAY_GROW_ACTIVE_IN_MS = 0;
|
||||
constexpr qint32 DELAY_SHRINK_ACTIVE_IN_MS = 3000;
|
||||
constexpr qint32 DURATION_TO_COMPLETE_END_IN_MS = 500;
|
||||
|
||||
// Constants for animation values.
|
||||
|
||||
// The total degrees that a constant rotation goes by.
|
||||
constexpr qint32 CONSTANT_ROTATION_DEGREES = 1080;
|
||||
// Despite of the constant rotation, there are also 5 extra rotations the entire animation. The
|
||||
// total degrees that each extra rotation goes by.
|
||||
constexpr qint32 SPIN_ROTATION_DEGREES = 90;
|
||||
constexpr std::array<qreal, 2> END_FRACTION_RANGE = { 0.10, 0.87 };
|
||||
|
||||
} // namespace retreat
|
||||
|
||||
inline qreal getFractionInRange(qreal playtime, qreal start, qreal duration) {
|
||||
const auto fraction = (playtime - start) / duration;
|
||||
return std::clamp(fraction, 0.0, 1.0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace caelestia::internal {
|
||||
|
||||
CircularIndicatorManager::CircularIndicatorManager(QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_type(IndeterminateAnimationType::Advance)
|
||||
, m_curve(QEasingCurve(QEasingCurve::BezierSpline))
|
||||
, m_progress(0)
|
||||
, m_startFraction(0)
|
||||
, m_endFraction(0)
|
||||
, m_rotation(0)
|
||||
, m_completeEndProgress(0) {
|
||||
// Fast out slow in
|
||||
m_curve.addCubicBezierSegment({ 0.4, 0.0 }, { 0.2, 1.0 }, { 1.0, 1.0 });
|
||||
}
|
||||
|
||||
qreal CircularIndicatorManager::startFraction() const {
|
||||
return m_startFraction;
|
||||
}
|
||||
|
||||
qreal CircularIndicatorManager::endFraction() const {
|
||||
return m_endFraction;
|
||||
}
|
||||
|
||||
qreal CircularIndicatorManager::rotation() const {
|
||||
return m_rotation;
|
||||
}
|
||||
|
||||
qreal CircularIndicatorManager::progress() const {
|
||||
return m_progress;
|
||||
}
|
||||
|
||||
void CircularIndicatorManager::setProgress(qreal progress) {
|
||||
update(progress);
|
||||
}
|
||||
|
||||
qreal CircularIndicatorManager::duration() const {
|
||||
if (m_type == IndeterminateAnimationType::Advance) {
|
||||
return advance::TOTAL_DURATION_IN_MS;
|
||||
} else {
|
||||
return retreat::TOTAL_DURATION_IN_MS;
|
||||
}
|
||||
}
|
||||
|
||||
qreal CircularIndicatorManager::completeEndDuration() const {
|
||||
if (m_type == IndeterminateAnimationType::Advance) {
|
||||
return advance::DURATION_TO_COMPLETE_END_IN_MS;
|
||||
} else {
|
||||
return retreat::DURATION_TO_COMPLETE_END_IN_MS;
|
||||
}
|
||||
}
|
||||
|
||||
CircularIndicatorManager::IndeterminateAnimationType CircularIndicatorManager::indeterminateAnimationType() const {
|
||||
return m_type;
|
||||
}
|
||||
|
||||
void CircularIndicatorManager::setIndeterminateAnimationType(IndeterminateAnimationType t) {
|
||||
if (m_type != t) {
|
||||
m_type = t;
|
||||
emit indeterminateAnimationTypeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
qreal CircularIndicatorManager::completeEndProgress() const {
|
||||
return m_completeEndProgress;
|
||||
}
|
||||
|
||||
void CircularIndicatorManager::setCompleteEndProgress(qreal progress) {
|
||||
if (qFuzzyCompare(m_completeEndProgress + 1.0, progress + 1.0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_completeEndProgress = progress;
|
||||
emit completeEndProgressChanged();
|
||||
|
||||
update(m_progress);
|
||||
}
|
||||
|
||||
void CircularIndicatorManager::update(qreal progress) {
|
||||
if (qFuzzyCompare(m_progress + 1.0, progress + 1.0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_type == IndeterminateAnimationType::Advance) {
|
||||
updateAdvance(progress);
|
||||
} else {
|
||||
updateRetreat(progress);
|
||||
}
|
||||
|
||||
m_progress = progress;
|
||||
emit progressChanged();
|
||||
}
|
||||
|
||||
void CircularIndicatorManager::updateRetreat(qreal progress) {
|
||||
using namespace retreat;
|
||||
const auto playtime = progress * TOTAL_DURATION_IN_MS;
|
||||
|
||||
// Constant rotation.
|
||||
const qreal constantRotation = CONSTANT_ROTATION_DEGREES * progress;
|
||||
// Extra rotation for the faster spinning.
|
||||
qreal spinRotation = 0;
|
||||
for (const int spinDelay : DELAY_SPINS_IN_MS) {
|
||||
spinRotation += m_curve.valueForProgress(getFractionInRange(playtime, spinDelay, DURATION_SPIN_IN_MS)) *
|
||||
SPIN_ROTATION_DEGREES;
|
||||
}
|
||||
m_rotation = constantRotation + spinRotation;
|
||||
emit rotationChanged();
|
||||
|
||||
// Grow active indicator.
|
||||
qreal fraction =
|
||||
m_curve.valueForProgress(getFractionInRange(playtime, DELAY_GROW_ACTIVE_IN_MS, DURATION_GROW_ACTIVE_IN_MS));
|
||||
fraction -=
|
||||
m_curve.valueForProgress(getFractionInRange(playtime, DELAY_SHRINK_ACTIVE_IN_MS, DURATION_SHRINK_ACTIVE_IN_MS));
|
||||
|
||||
if (!qFuzzyIsNull(m_startFraction)) {
|
||||
m_startFraction = 0.0;
|
||||
emit startFractionChanged();
|
||||
}
|
||||
const auto oldEndFrac = m_endFraction;
|
||||
m_endFraction = std::lerp(END_FRACTION_RANGE[0], END_FRACTION_RANGE[1], fraction);
|
||||
|
||||
// Completing animation.
|
||||
if (m_completeEndProgress > 0) {
|
||||
m_endFraction *= 1 - m_completeEndProgress;
|
||||
}
|
||||
|
||||
if (!qFuzzyCompare(m_endFraction + 1.0, oldEndFrac + 1.0)) {
|
||||
emit endFractionChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void CircularIndicatorManager::updateAdvance(qreal progress) {
|
||||
using namespace advance;
|
||||
const auto playtime = progress * TOTAL_DURATION_IN_MS;
|
||||
|
||||
// Adds constant rotation to segment positions.
|
||||
m_startFraction = CONSTANT_ROTATION_DEGREES * progress + TAIL_DEGREES_OFFSET;
|
||||
m_endFraction = CONSTANT_ROTATION_DEGREES * progress;
|
||||
|
||||
// Adds cycle specific rotation to segment positions.
|
||||
for (size_t cycleIndex = 0; cycleIndex < TOTAL_CYCLES; ++cycleIndex) {
|
||||
// While expanding.
|
||||
qreal fraction = getFractionInRange(playtime, DELAY_TO_EXPAND_IN_MS[cycleIndex], DURATION_TO_EXPAND_IN_MS);
|
||||
m_endFraction += m_curve.valueForProgress(fraction) * EXTRA_DEGREES_PER_CYCLE;
|
||||
|
||||
// While collapsing.
|
||||
fraction = getFractionInRange(playtime, DELAY_TO_COLLAPSE_IN_MS[cycleIndex], DURATION_TO_COLLAPSE_IN_MS);
|
||||
m_startFraction += m_curve.valueForProgress(fraction) * EXTRA_DEGREES_PER_CYCLE;
|
||||
}
|
||||
|
||||
// Closes the gap between head and tail for complete end.
|
||||
m_startFraction += (m_endFraction - m_startFraction) * m_completeEndProgress;
|
||||
|
||||
m_startFraction /= 360.0;
|
||||
m_endFraction /= 360.0;
|
||||
|
||||
emit startFractionChanged();
|
||||
emit endFractionChanged();
|
||||
}
|
||||
|
||||
} // namespace caelestia::internal
|
||||
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include <qeasingcurve.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace caelestia::internal {
|
||||
|
||||
class CircularIndicatorManager : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(qreal startFraction READ startFraction NOTIFY startFractionChanged)
|
||||
Q_PROPERTY(qreal endFraction READ endFraction NOTIFY endFractionChanged)
|
||||
Q_PROPERTY(qreal rotation READ rotation NOTIFY rotationChanged)
|
||||
Q_PROPERTY(qreal progress READ progress WRITE setProgress NOTIFY progressChanged)
|
||||
Q_PROPERTY(qreal completeEndProgress READ completeEndProgress WRITE setCompleteEndProgress NOTIFY
|
||||
completeEndProgressChanged)
|
||||
Q_PROPERTY(qreal duration READ duration NOTIFY indeterminateAnimationTypeChanged)
|
||||
Q_PROPERTY(qreal completeEndDuration READ completeEndDuration NOTIFY indeterminateAnimationTypeChanged)
|
||||
Q_PROPERTY(IndeterminateAnimationType indeterminateAnimationType READ indeterminateAnimationType WRITE
|
||||
setIndeterminateAnimationType NOTIFY indeterminateAnimationTypeChanged)
|
||||
|
||||
public:
|
||||
explicit CircularIndicatorManager(QObject* parent = nullptr);
|
||||
|
||||
enum IndeterminateAnimationType {
|
||||
Advance = 0,
|
||||
Retreat
|
||||
};
|
||||
Q_ENUM(IndeterminateAnimationType)
|
||||
|
||||
[[nodiscard]] qreal startFraction() const;
|
||||
[[nodiscard]] qreal endFraction() const;
|
||||
[[nodiscard]] qreal rotation() const;
|
||||
|
||||
[[nodiscard]] qreal progress() const;
|
||||
void setProgress(qreal progress);
|
||||
|
||||
[[nodiscard]] qreal completeEndProgress() const;
|
||||
void setCompleteEndProgress(qreal progress);
|
||||
|
||||
[[nodiscard]] qreal duration() const;
|
||||
[[nodiscard]] qreal completeEndDuration() const;
|
||||
|
||||
[[nodiscard]] IndeterminateAnimationType indeterminateAnimationType() const;
|
||||
void setIndeterminateAnimationType(IndeterminateAnimationType t);
|
||||
|
||||
signals:
|
||||
void startFractionChanged();
|
||||
void endFractionChanged();
|
||||
void rotationChanged();
|
||||
void progressChanged();
|
||||
void completeEndProgressChanged();
|
||||
void indeterminateAnimationTypeChanged();
|
||||
|
||||
private:
|
||||
IndeterminateAnimationType m_type;
|
||||
QEasingCurve m_curve;
|
||||
|
||||
qreal m_progress;
|
||||
qreal m_startFraction;
|
||||
qreal m_endFraction;
|
||||
qreal m_rotation;
|
||||
qreal m_completeEndProgress;
|
||||
|
||||
void update(qreal progress);
|
||||
void updateAdvance(qreal progress);
|
||||
void updateRetreat(qreal progress);
|
||||
};
|
||||
|
||||
} // namespace caelestia::internal
|
||||
@@ -0,0 +1,134 @@
|
||||
#include "hyprdevices.hpp"
|
||||
|
||||
#include <qjsonarray.h>
|
||||
|
||||
namespace caelestia::internal::hypr {
|
||||
|
||||
HyprKeyboard::HyprKeyboard(QJsonObject ipcObject, QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_lastIpcObject(ipcObject) {}
|
||||
|
||||
QVariantHash HyprKeyboard::lastIpcObject() const {
|
||||
return m_lastIpcObject.toVariantHash();
|
||||
}
|
||||
|
||||
QString HyprKeyboard::address() const {
|
||||
return m_lastIpcObject.value("address").toString();
|
||||
}
|
||||
|
||||
QString HyprKeyboard::name() const {
|
||||
return m_lastIpcObject.value("name").toString();
|
||||
}
|
||||
|
||||
QString HyprKeyboard::layout() const {
|
||||
return m_lastIpcObject.value("layout").toString();
|
||||
}
|
||||
|
||||
QString HyprKeyboard::activeKeymap() const {
|
||||
return m_lastIpcObject.value("active_keymap").toString();
|
||||
}
|
||||
|
||||
bool HyprKeyboard::capsLock() const {
|
||||
return m_lastIpcObject.value("capsLock").toBool();
|
||||
}
|
||||
|
||||
bool HyprKeyboard::numLock() const {
|
||||
return m_lastIpcObject.value("numLock").toBool();
|
||||
}
|
||||
|
||||
bool HyprKeyboard::main() const {
|
||||
return m_lastIpcObject.value("main").toBool();
|
||||
}
|
||||
|
||||
bool HyprKeyboard::updateLastIpcObject(QJsonObject object) {
|
||||
if (m_lastIpcObject == object) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto last = m_lastIpcObject;
|
||||
|
||||
m_lastIpcObject = object;
|
||||
emit lastIpcObjectChanged();
|
||||
|
||||
bool dirty = false;
|
||||
if (last.value("address") != object.value("address")) {
|
||||
dirty = true;
|
||||
emit addressChanged();
|
||||
}
|
||||
if (last.value("name") != object.value("name")) {
|
||||
dirty = true;
|
||||
emit nameChanged();
|
||||
}
|
||||
if (last.value("layout") != object.value("layout")) {
|
||||
dirty = true;
|
||||
emit layoutChanged();
|
||||
}
|
||||
if (last.value("active_keymap") != object.value("active_keymap")) {
|
||||
dirty = true;
|
||||
emit activeKeymapChanged();
|
||||
}
|
||||
if (last.value("capsLock") != object.value("capsLock")) {
|
||||
dirty = true;
|
||||
emit capsLockChanged();
|
||||
}
|
||||
if (last.value("numLock") != object.value("numLock")) {
|
||||
dirty = true;
|
||||
emit numLockChanged();
|
||||
}
|
||||
if (last.value("main") != object.value("main")) {
|
||||
dirty = true;
|
||||
emit mainChanged();
|
||||
}
|
||||
return dirty;
|
||||
}
|
||||
|
||||
HyprDevices::HyprDevices(QObject* parent)
|
||||
: QObject(parent) {}
|
||||
|
||||
QQmlListProperty<HyprKeyboard> HyprDevices::keyboards() {
|
||||
return QQmlListProperty<HyprKeyboard>(this, &m_keyboards);
|
||||
}
|
||||
|
||||
bool HyprDevices::updateLastIpcObject(QJsonObject object) {
|
||||
const auto val = object.value("keyboards").toArray();
|
||||
bool dirty = false;
|
||||
|
||||
for (auto it = m_keyboards.begin(); it != m_keyboards.end();) {
|
||||
auto* const keyboard = *it;
|
||||
const auto inNewValues = std::any_of(val.begin(), val.end(), [keyboard](const QJsonValue& o) {
|
||||
return o.toObject().value("address").toString() == keyboard->address();
|
||||
});
|
||||
|
||||
if (!inNewValues) {
|
||||
dirty = true;
|
||||
it = m_keyboards.erase(it);
|
||||
keyboard->deleteLater();
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& o : val) {
|
||||
const auto obj = o.toObject();
|
||||
const auto addr = obj.value("address").toString();
|
||||
|
||||
auto it = std::find_if(m_keyboards.begin(), m_keyboards.end(), [addr](const HyprKeyboard* kb) {
|
||||
return kb->address() == addr;
|
||||
});
|
||||
|
||||
if (it != m_keyboards.end()) {
|
||||
dirty |= (*it)->updateLastIpcObject(obj);
|
||||
} else {
|
||||
dirty = true;
|
||||
m_keyboards << new HyprKeyboard(obj, this);
|
||||
}
|
||||
}
|
||||
|
||||
if (dirty) {
|
||||
emit keyboardsChanged();
|
||||
}
|
||||
|
||||
return dirty;
|
||||
}
|
||||
|
||||
} // namespace caelestia::internal::hypr
|
||||
@@ -0,0 +1,74 @@
|
||||
#pragma once
|
||||
|
||||
#include <qjsonobject.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qqmllist.h>
|
||||
|
||||
namespace caelestia::internal::hypr {
|
||||
|
||||
class HyprKeyboard : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("HyprKeyboard instances can only be retrieved from a HyprDevices")
|
||||
|
||||
Q_PROPERTY(QVariantHash lastIpcObject READ lastIpcObject NOTIFY lastIpcObjectChanged)
|
||||
Q_PROPERTY(QString address READ address NOTIFY addressChanged)
|
||||
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
|
||||
Q_PROPERTY(QString layout READ layout NOTIFY layoutChanged)
|
||||
Q_PROPERTY(QString activeKeymap READ activeKeymap NOTIFY activeKeymapChanged)
|
||||
Q_PROPERTY(bool capsLock READ capsLock NOTIFY capsLockChanged)
|
||||
Q_PROPERTY(bool numLock READ numLock NOTIFY numLockChanged)
|
||||
Q_PROPERTY(bool main READ main NOTIFY mainChanged)
|
||||
|
||||
public:
|
||||
explicit HyprKeyboard(QJsonObject ipcObject, QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] QVariantHash lastIpcObject() const;
|
||||
[[nodiscard]] QString address() const;
|
||||
[[nodiscard]] QString name() const;
|
||||
[[nodiscard]] QString layout() const;
|
||||
[[nodiscard]] QString activeKeymap() const;
|
||||
[[nodiscard]] bool capsLock() const;
|
||||
[[nodiscard]] bool numLock() const;
|
||||
[[nodiscard]] bool main() const;
|
||||
|
||||
bool updateLastIpcObject(QJsonObject object);
|
||||
|
||||
signals:
|
||||
void lastIpcObjectChanged();
|
||||
void addressChanged();
|
||||
void nameChanged();
|
||||
void layoutChanged();
|
||||
void activeKeymapChanged();
|
||||
void capsLockChanged();
|
||||
void numLockChanged();
|
||||
void mainChanged();
|
||||
|
||||
private:
|
||||
QJsonObject m_lastIpcObject;
|
||||
};
|
||||
|
||||
class HyprDevices : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("HyprDevices instances can only be retrieved from a HyprExtras")
|
||||
|
||||
Q_PROPERTY(
|
||||
QQmlListProperty<caelestia::internal::hypr::HyprKeyboard> keyboards READ keyboards NOTIFY keyboardsChanged)
|
||||
|
||||
public:
|
||||
explicit HyprDevices(QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] QQmlListProperty<HyprKeyboard> keyboards();
|
||||
|
||||
bool updateLastIpcObject(QJsonObject object);
|
||||
|
||||
signals:
|
||||
void keyboardsChanged();
|
||||
|
||||
private:
|
||||
QList<HyprKeyboard*> m_keyboards;
|
||||
};
|
||||
|
||||
} // namespace caelestia::internal::hypr
|
||||
@@ -0,0 +1,217 @@
|
||||
#include "hyprextras.hpp"
|
||||
|
||||
#include <qdir.h>
|
||||
#include <qjsonarray.h>
|
||||
#include <qlocalsocket.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
namespace caelestia::internal::hypr {
|
||||
|
||||
HyprExtras::HyprExtras(QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_requestSocket("")
|
||||
, m_eventSocket("")
|
||||
, m_socket(nullptr)
|
||||
, m_socketValid(false)
|
||||
, m_devices(new HyprDevices(this)) {
|
||||
const auto his = qEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE");
|
||||
if (his.isEmpty()) {
|
||||
qWarning()
|
||||
<< "HyprExtras::HyprExtras: $HYPRLAND_INSTANCE_SIGNATURE is unset. Unable to connect to Hyprland socket.";
|
||||
return;
|
||||
}
|
||||
|
||||
auto hyprDir = QString("%1/hypr/%2").arg(qEnvironmentVariable("XDG_RUNTIME_DIR"), his);
|
||||
if (!QDir(hyprDir).exists()) {
|
||||
hyprDir = "/tmp/hypr/" + his;
|
||||
|
||||
if (!QDir(hyprDir).exists()) {
|
||||
qWarning() << "HyprExtras::HyprExtras: Hyprland socket directory does not exist. Unable to connect to "
|
||||
"Hyprland socket.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_requestSocket = hyprDir + "/.socket.sock";
|
||||
m_eventSocket = hyprDir + "/.socket2.sock";
|
||||
|
||||
refreshOptions();
|
||||
refreshDevices();
|
||||
|
||||
m_socket = new QLocalSocket(this);
|
||||
|
||||
QObject::connect(m_socket, &QLocalSocket::errorOccurred, this, &HyprExtras::socketError);
|
||||
QObject::connect(m_socket, &QLocalSocket::stateChanged, this, &HyprExtras::socketStateChanged);
|
||||
QObject::connect(m_socket, &QLocalSocket::readyRead, this, &HyprExtras::readEvent);
|
||||
|
||||
m_socket->connectToServer(m_eventSocket, QLocalSocket::ReadOnly);
|
||||
}
|
||||
|
||||
QVariantHash HyprExtras::options() const {
|
||||
return m_options;
|
||||
}
|
||||
|
||||
HyprDevices* HyprExtras::devices() const {
|
||||
return m_devices;
|
||||
}
|
||||
|
||||
void HyprExtras::message(const QString& message) {
|
||||
if (message.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
makeRequest(message, [](bool success, const QByteArray& res) {
|
||||
if (!success) {
|
||||
qWarning() << "HyprExtras::message: request error:" << QString::fromUtf8(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void HyprExtras::batchMessage(const QStringList& messages) {
|
||||
if (messages.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
makeRequest("[[BATCH]]" + messages.join(";"), [](bool success, const QByteArray& res) {
|
||||
if (!success) {
|
||||
qWarning() << "HyprExtras::batchMessage: request error:" << QString::fromUtf8(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void HyprExtras::applyOptions(const QVariantHash& options) {
|
||||
if (options.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString request = "[[BATCH]]";
|
||||
for (auto it = options.constBegin(); it != options.constEnd(); ++it) {
|
||||
request += QString("keyword %1 %2;").arg(it.key(), it.value().toString());
|
||||
}
|
||||
|
||||
makeRequest(request, [this](bool success, const QByteArray& res) {
|
||||
if (success) {
|
||||
refreshOptions();
|
||||
} else {
|
||||
qWarning() << "HyprExtras::applyOptions: request error" << QString::fromUtf8(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void HyprExtras::refreshOptions() {
|
||||
if (!m_optionsRefresh.isNull()) {
|
||||
m_optionsRefresh->close();
|
||||
}
|
||||
|
||||
m_optionsRefresh = makeRequestJson("descriptions", [this](bool success, const QJsonDocument& response) {
|
||||
m_optionsRefresh.reset();
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto options = response.array();
|
||||
bool dirty = false;
|
||||
|
||||
for (const auto& o : std::as_const(options)) {
|
||||
const auto obj = o.toObject();
|
||||
const auto key = obj.value("value").toString();
|
||||
const auto value = obj.value("data").toObject().value("current").toVariant();
|
||||
if (m_options.value(key) != value) {
|
||||
dirty = true;
|
||||
m_options.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (dirty) {
|
||||
emit optionsChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void HyprExtras::refreshDevices() {
|
||||
if (!m_devicesRefresh.isNull()) {
|
||||
m_devicesRefresh->close();
|
||||
}
|
||||
|
||||
m_devicesRefresh = makeRequestJson("devices", [this](bool success, const QJsonDocument& response) {
|
||||
m_devicesRefresh.reset();
|
||||
if (success) {
|
||||
m_devices->updateLastIpcObject(response.object());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void HyprExtras::socketError(QLocalSocket::LocalSocketError error) const {
|
||||
if (!m_socketValid) {
|
||||
qWarning() << "HyprExtras::socketError: unable to connect to Hyprland event socket:" << error;
|
||||
} else {
|
||||
qWarning() << "HyprExtras::socketError: Hyprland event socket error:" << error;
|
||||
}
|
||||
}
|
||||
|
||||
void HyprExtras::socketStateChanged(QLocalSocket::LocalSocketState state) {
|
||||
if (state == QLocalSocket::UnconnectedState && m_socketValid) {
|
||||
qWarning() << "HyprExtras::socketStateChanged: Hyprland event socket disconnected.";
|
||||
}
|
||||
|
||||
m_socketValid = state == QLocalSocket::ConnectedState;
|
||||
}
|
||||
|
||||
void HyprExtras::readEvent() {
|
||||
while (true) {
|
||||
auto rawEvent = m_socket->readLine();
|
||||
if (rawEvent.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
rawEvent.truncate(rawEvent.length() - 1); // Remove trailing \n
|
||||
const auto event = QByteArrayView(rawEvent.data(), rawEvent.indexOf(">>"));
|
||||
handleEvent(QString::fromUtf8(event));
|
||||
}
|
||||
}
|
||||
|
||||
void HyprExtras::handleEvent(const QString& event) {
|
||||
if (event == "configreloaded") {
|
||||
refreshOptions();
|
||||
} else if (event == "activelayout") {
|
||||
refreshDevices();
|
||||
}
|
||||
}
|
||||
|
||||
HyprExtras::SocketPtr HyprExtras::makeRequestJson(
|
||||
const QString& request, const std::function<void(bool, QJsonDocument)>& callback) {
|
||||
return makeRequest("j/" + request, [callback](bool success, const QByteArray& response) {
|
||||
callback(success, QJsonDocument::fromJson(response));
|
||||
});
|
||||
}
|
||||
|
||||
HyprExtras::SocketPtr HyprExtras::makeRequest(
|
||||
const QString& request, const std::function<void(bool, QByteArray)>& callback) {
|
||||
if (m_requestSocket.isEmpty()) {
|
||||
return SocketPtr();
|
||||
}
|
||||
|
||||
auto socket = SocketPtr::create(this);
|
||||
|
||||
QObject::connect(socket.data(), &QLocalSocket::connected, this, [=, this]() {
|
||||
QObject::connect(socket.data(), &QLocalSocket::readyRead, this, [socket, callback]() {
|
||||
const auto response = socket->readAll();
|
||||
callback(true, std::move(response));
|
||||
socket->close();
|
||||
});
|
||||
|
||||
socket->write(request.toUtf8());
|
||||
socket->flush();
|
||||
});
|
||||
|
||||
QObject::connect(socket.data(), &QLocalSocket::errorOccurred, this, [=](QLocalSocket::LocalSocketError err) {
|
||||
qWarning() << "HyprExtras::makeRequest: error making request:" << err << "| request:" << request;
|
||||
callback(false, {});
|
||||
socket->close();
|
||||
});
|
||||
|
||||
socket->connectToServer(m_requestSocket);
|
||||
|
||||
return socket;
|
||||
}
|
||||
|
||||
} // namespace caelestia::internal::hypr
|
||||
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include "hyprdevices.hpp"
|
||||
#include <qlocalsocket.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace caelestia::internal::hypr {
|
||||
|
||||
class HyprExtras : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(QVariantHash options READ options NOTIFY optionsChanged)
|
||||
Q_PROPERTY(caelestia::internal::hypr::HyprDevices* devices READ devices CONSTANT)
|
||||
|
||||
public:
|
||||
explicit HyprExtras(QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] QVariantHash options() const;
|
||||
[[nodiscard]] HyprDevices* devices() const;
|
||||
|
||||
Q_INVOKABLE void message(const QString& message);
|
||||
Q_INVOKABLE void batchMessage(const QStringList& messages);
|
||||
Q_INVOKABLE void applyOptions(const QVariantHash& options);
|
||||
|
||||
Q_INVOKABLE void refreshOptions();
|
||||
Q_INVOKABLE void refreshDevices();
|
||||
|
||||
signals:
|
||||
void optionsChanged();
|
||||
|
||||
private:
|
||||
using SocketPtr = QSharedPointer<QLocalSocket>;
|
||||
|
||||
QString m_requestSocket;
|
||||
QString m_eventSocket;
|
||||
QLocalSocket* m_socket;
|
||||
bool m_socketValid;
|
||||
|
||||
QVariantHash m_options;
|
||||
HyprDevices* const m_devices;
|
||||
|
||||
SocketPtr m_optionsRefresh;
|
||||
SocketPtr m_devicesRefresh;
|
||||
|
||||
void socketError(QLocalSocket::LocalSocketError error) const;
|
||||
void socketStateChanged(QLocalSocket::LocalSocketState state);
|
||||
void readEvent();
|
||||
void handleEvent(const QString& event);
|
||||
|
||||
SocketPtr makeRequestJson(const QString& request, const std::function<void(bool, QJsonDocument)>& callback);
|
||||
SocketPtr makeRequest(const QString& request, const std::function<void(bool, QByteArray)>& callback);
|
||||
};
|
||||
|
||||
} // namespace caelestia::internal::hypr
|
||||
@@ -0,0 +1,65 @@
|
||||
#include "logindmanager.hpp"
|
||||
|
||||
#include <QtDBus/qdbusconnection.h>
|
||||
#include <QtDBus/qdbuserror.h>
|
||||
#include <QtDBus/qdbusinterface.h>
|
||||
#include <QtDBus/qdbusreply.h>
|
||||
|
||||
namespace caelestia::internal {
|
||||
|
||||
LogindManager::LogindManager(QObject* parent)
|
||||
: QObject(parent) {
|
||||
auto bus = QDBusConnection::systemBus();
|
||||
if (!bus.isConnected()) {
|
||||
qWarning() << "LogindManager::LogindManager: failed to connect to system bus:" << bus.lastError().message();
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok = bus.connect("org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager",
|
||||
"PrepareForSleep", this, SLOT(handlePrepareForSleep(bool)));
|
||||
|
||||
if (!ok) {
|
||||
qWarning() << "LogindManager::LogindManager: failed to connect to PrepareForSleep signal:"
|
||||
<< bus.lastError().message();
|
||||
}
|
||||
|
||||
QDBusInterface login1("org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", bus);
|
||||
const QDBusReply<QDBusObjectPath> reply = login1.call("GetSession", "auto");
|
||||
if (!reply.isValid()) {
|
||||
qWarning() << "LogindManager::LogindManager: failed to get session path";
|
||||
return;
|
||||
}
|
||||
const auto sessionPath = reply.value().path();
|
||||
|
||||
ok = bus.connect("org.freedesktop.login1", sessionPath, "org.freedesktop.login1.Session", "Lock", this,
|
||||
SLOT(handleLockRequested()));
|
||||
|
||||
if (!ok) {
|
||||
qWarning() << "LogindManager::LogindManager: failed to connect to Lock signal:" << bus.lastError().message();
|
||||
}
|
||||
|
||||
ok = bus.connect("org.freedesktop.login1", sessionPath, "org.freedesktop.login1.Session", "Unlock", this,
|
||||
SLOT(handleUnlockRequested()));
|
||||
|
||||
if (!ok) {
|
||||
qWarning() << "LogindManager::LogindManager: failed to connect to Unlock signal:" << bus.lastError().message();
|
||||
}
|
||||
}
|
||||
|
||||
void LogindManager::handlePrepareForSleep(bool sleep) {
|
||||
if (sleep) {
|
||||
emit aboutToSleep();
|
||||
} else {
|
||||
emit resumed();
|
||||
}
|
||||
}
|
||||
|
||||
void LogindManager::handleLockRequested() {
|
||||
emit lockRequested();
|
||||
}
|
||||
|
||||
void LogindManager::handleUnlockRequested() {
|
||||
emit unlockRequested();
|
||||
}
|
||||
|
||||
} // namespace caelestia::internal
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace caelestia::internal {
|
||||
|
||||
class LogindManager : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
public:
|
||||
explicit LogindManager(QObject* parent = nullptr);
|
||||
|
||||
signals:
|
||||
void aboutToSleep();
|
||||
void resumed();
|
||||
void lockRequested();
|
||||
void unlockRequested();
|
||||
|
||||
private slots:
|
||||
void handlePrepareForSleep(bool sleep);
|
||||
void handleLockRequested();
|
||||
void handleUnlockRequested();
|
||||
};
|
||||
|
||||
} // namespace caelestia::internal
|
||||
@@ -0,0 +1,8 @@
|
||||
qml_module(caelestia-models
|
||||
URI Caelestia.Models
|
||||
SOURCES
|
||||
filesystemmodel.hpp filesystemmodel.cpp
|
||||
LIBRARIES
|
||||
Qt::Gui
|
||||
Qt::Concurrent
|
||||
)
|
||||
@@ -0,0 +1,479 @@
|
||||
#include "filesystemmodel.hpp"
|
||||
|
||||
#include <qdiriterator.h>
|
||||
#include <qfuturewatcher.h>
|
||||
#include <qtconcurrentrun.h>
|
||||
|
||||
namespace caelestia::models {
|
||||
|
||||
FileSystemEntry::FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_fileInfo(path)
|
||||
, m_path(path)
|
||||
, m_relativePath(relativePath)
|
||||
, m_isImageInitialised(false)
|
||||
, m_mimeTypeInitialised(false) {}
|
||||
|
||||
QString FileSystemEntry::path() const {
|
||||
return m_path;
|
||||
};
|
||||
|
||||
QString FileSystemEntry::relativePath() const {
|
||||
return m_relativePath;
|
||||
};
|
||||
|
||||
QString FileSystemEntry::name() const {
|
||||
return m_fileInfo.fileName();
|
||||
};
|
||||
|
||||
QString FileSystemEntry::baseName() const {
|
||||
return m_fileInfo.baseName();
|
||||
};
|
||||
|
||||
QString FileSystemEntry::parentDir() const {
|
||||
return m_fileInfo.absolutePath();
|
||||
};
|
||||
|
||||
QString FileSystemEntry::suffix() const {
|
||||
return m_fileInfo.completeSuffix();
|
||||
};
|
||||
|
||||
qint64 FileSystemEntry::size() const {
|
||||
return m_fileInfo.size();
|
||||
};
|
||||
|
||||
bool FileSystemEntry::isDir() const {
|
||||
return m_fileInfo.isDir();
|
||||
};
|
||||
|
||||
bool FileSystemEntry::isImage() const {
|
||||
if (!m_isImageInitialised) {
|
||||
QImageReader reader(m_path);
|
||||
m_isImage = reader.canRead();
|
||||
m_isImageInitialised = true;
|
||||
}
|
||||
return m_isImage;
|
||||
}
|
||||
|
||||
QString FileSystemEntry::mimeType() const {
|
||||
if (!m_mimeTypeInitialised) {
|
||||
const QMimeDatabase db;
|
||||
m_mimeType = db.mimeTypeForFile(m_path).name();
|
||||
m_mimeTypeInitialised = true;
|
||||
}
|
||||
return m_mimeType;
|
||||
}
|
||||
|
||||
void FileSystemEntry::updateRelativePath(const QDir& dir) {
|
||||
const auto relPath = dir.relativeFilePath(m_path);
|
||||
if (m_relativePath != relPath) {
|
||||
m_relativePath = relPath;
|
||||
emit relativePathChanged();
|
||||
}
|
||||
}
|
||||
|
||||
FileSystemModel::FileSystemModel(QObject* parent)
|
||||
: QAbstractListModel(parent)
|
||||
, m_recursive(false)
|
||||
, m_watchChanges(true)
|
||||
, m_showHidden(false)
|
||||
, m_filter(NoFilter) {
|
||||
connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::watchDirIfRecursive);
|
||||
connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::updateEntriesForDir);
|
||||
}
|
||||
|
||||
int FileSystemModel::rowCount(const QModelIndex& parent) const {
|
||||
if (parent != QModelIndex()) {
|
||||
return 0;
|
||||
}
|
||||
return static_cast<int>(m_entries.size());
|
||||
}
|
||||
|
||||
QVariant FileSystemModel::data(const QModelIndex& index, int role) const {
|
||||
if (role != Qt::UserRole || !index.isValid() || index.row() >= m_entries.size()) {
|
||||
return QVariant();
|
||||
}
|
||||
return QVariant::fromValue(m_entries.at(index.row()));
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> FileSystemModel::roleNames() const {
|
||||
return { { Qt::UserRole, "modelData" } };
|
||||
}
|
||||
|
||||
QString FileSystemModel::path() const {
|
||||
return m_path;
|
||||
}
|
||||
|
||||
void FileSystemModel::setPath(const QString& path) {
|
||||
if (m_path == path) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_path = path;
|
||||
emit pathChanged();
|
||||
|
||||
m_dir.setPath(m_path);
|
||||
|
||||
for (const auto& entry : std::as_const(m_entries)) {
|
||||
entry->updateRelativePath(m_dir);
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
bool FileSystemModel::recursive() const {
|
||||
return m_recursive;
|
||||
}
|
||||
|
||||
void FileSystemModel::setRecursive(bool recursive) {
|
||||
if (m_recursive == recursive) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_recursive = recursive;
|
||||
emit recursiveChanged();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
bool FileSystemModel::watchChanges() const {
|
||||
return m_watchChanges;
|
||||
}
|
||||
|
||||
void FileSystemModel::setWatchChanges(bool watchChanges) {
|
||||
if (m_watchChanges == watchChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_watchChanges = watchChanges;
|
||||
emit watchChangesChanged();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
bool FileSystemModel::showHidden() const {
|
||||
return m_showHidden;
|
||||
}
|
||||
|
||||
void FileSystemModel::setShowHidden(bool showHidden) {
|
||||
if (m_showHidden == showHidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_showHidden = showHidden;
|
||||
emit showHiddenChanged();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
bool FileSystemModel::sortReverse() const {
|
||||
return m_sortReverse;
|
||||
}
|
||||
|
||||
void FileSystemModel::setSortReverse(bool sortReverse) {
|
||||
if (m_sortReverse == sortReverse) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_sortReverse = sortReverse;
|
||||
emit sortReverseChanged();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
FileSystemModel::Filter FileSystemModel::filter() const {
|
||||
return m_filter;
|
||||
}
|
||||
|
||||
void FileSystemModel::setFilter(Filter filter) {
|
||||
if (m_filter == filter) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_filter = filter;
|
||||
emit filterChanged();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
QStringList FileSystemModel::nameFilters() const {
|
||||
return m_nameFilters;
|
||||
}
|
||||
|
||||
void FileSystemModel::setNameFilters(const QStringList& nameFilters) {
|
||||
if (m_nameFilters == nameFilters) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_nameFilters = nameFilters;
|
||||
emit nameFiltersChanged();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
QQmlListProperty<FileSystemEntry> FileSystemModel::entries() {
|
||||
return QQmlListProperty<FileSystemEntry>(this, &m_entries);
|
||||
}
|
||||
|
||||
void FileSystemModel::watchDirIfRecursive(const QString& path) {
|
||||
if (m_recursive && m_watchChanges) {
|
||||
const auto currentDir = m_dir;
|
||||
const bool showHidden = m_showHidden;
|
||||
const auto future = QtConcurrent::run([showHidden, path]() {
|
||||
QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot;
|
||||
if (showHidden) {
|
||||
filters |= QDir::Hidden;
|
||||
}
|
||||
|
||||
QDirIterator iter(path, filters, QDirIterator::Subdirectories);
|
||||
QStringList dirs;
|
||||
while (iter.hasNext()) {
|
||||
dirs << iter.next();
|
||||
}
|
||||
return dirs;
|
||||
});
|
||||
const auto watcher = new QFutureWatcher<QStringList>(this);
|
||||
connect(watcher, &QFutureWatcher<QStringList>::finished, this, [currentDir, showHidden, watcher, this]() {
|
||||
const auto paths = watcher->result();
|
||||
if (currentDir == m_dir && showHidden == m_showHidden && !paths.isEmpty()) {
|
||||
// Ignore if dir or showHidden has changed
|
||||
m_watcher.addPaths(paths);
|
||||
}
|
||||
watcher->deleteLater();
|
||||
});
|
||||
watcher->setFuture(future);
|
||||
}
|
||||
}
|
||||
|
||||
void FileSystemModel::update() {
|
||||
updateWatcher();
|
||||
updateEntries();
|
||||
}
|
||||
|
||||
void FileSystemModel::updateWatcher() {
|
||||
if (!m_watcher.directories().isEmpty()) {
|
||||
m_watcher.removePaths(m_watcher.directories());
|
||||
}
|
||||
|
||||
if (!m_watchChanges || m_path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_watcher.addPath(m_path);
|
||||
watchDirIfRecursive(m_path);
|
||||
}
|
||||
|
||||
void FileSystemModel::updateEntries() {
|
||||
if (m_path.isEmpty()) {
|
||||
if (!m_entries.isEmpty()) {
|
||||
beginResetModel();
|
||||
qDeleteAll(m_entries);
|
||||
m_entries.clear();
|
||||
endResetModel();
|
||||
emit entriesChanged();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& future : m_futures) {
|
||||
future.cancel();
|
||||
}
|
||||
m_futures.clear();
|
||||
|
||||
updateEntriesForDir(m_path);
|
||||
}
|
||||
|
||||
void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
||||
const auto recursive = m_recursive;
|
||||
const auto showHidden = m_showHidden;
|
||||
const auto filter = m_filter;
|
||||
const auto nameFilters = m_nameFilters;
|
||||
|
||||
QSet<QString> oldPaths;
|
||||
for (const auto& entry : std::as_const(m_entries)) {
|
||||
oldPaths << entry->path();
|
||||
}
|
||||
|
||||
const auto future = QtConcurrent::run([=](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) {
|
||||
const auto flags = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags;
|
||||
|
||||
std::optional<QDirIterator> iter;
|
||||
|
||||
if (filter == Images) {
|
||||
QStringList extraNameFilters = nameFilters;
|
||||
const auto formats = QImageReader::supportedImageFormats();
|
||||
for (const auto& format : formats) {
|
||||
extraNameFilters << "*." + format;
|
||||
}
|
||||
|
||||
QDir::Filters filters = QDir::Files;
|
||||
if (showHidden) {
|
||||
filters |= QDir::Hidden;
|
||||
}
|
||||
|
||||
iter.emplace(dir, extraNameFilters, filters, flags);
|
||||
} else {
|
||||
QDir::Filters filters;
|
||||
|
||||
if (filter == Files) {
|
||||
filters = QDir::Files;
|
||||
} else if (filter == Dirs) {
|
||||
filters = QDir::Dirs | QDir::NoDotAndDotDot;
|
||||
} else {
|
||||
filters = QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot;
|
||||
}
|
||||
|
||||
if (showHidden) {
|
||||
filters |= QDir::Hidden;
|
||||
}
|
||||
|
||||
if (nameFilters.isEmpty()) {
|
||||
iter.emplace(dir, filters, flags);
|
||||
} else {
|
||||
iter.emplace(dir, nameFilters, filters, flags);
|
||||
}
|
||||
}
|
||||
|
||||
QSet<QString> newPaths;
|
||||
while (iter->hasNext()) {
|
||||
if (promise.isCanceled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString path = iter->next();
|
||||
|
||||
if (filter == Images) {
|
||||
QImageReader reader(path);
|
||||
if (!reader.canRead()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
newPaths.insert(path);
|
||||
}
|
||||
|
||||
if (promise.isCanceled() || newPaths == oldPaths) {
|
||||
return;
|
||||
}
|
||||
|
||||
promise.addResult(qMakePair(oldPaths - newPaths, newPaths - oldPaths));
|
||||
});
|
||||
|
||||
if (m_futures.contains(dir)) {
|
||||
m_futures[dir].cancel();
|
||||
}
|
||||
m_futures.insert(dir, future);
|
||||
|
||||
const auto watcher = new QFutureWatcher<QPair<QSet<QString>, QSet<QString>>>(this);
|
||||
|
||||
connect(watcher, &QFutureWatcher<QPair<QSet<QString>, QSet<QString>>>::finished, this, [dir, watcher, this]() {
|
||||
m_futures.remove(dir);
|
||||
|
||||
if (!watcher->future().isResultReadyAt(0)) {
|
||||
watcher->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto result = watcher->result();
|
||||
applyChanges(result.first, result.second);
|
||||
|
||||
watcher->deleteLater();
|
||||
});
|
||||
|
||||
watcher->setFuture(future);
|
||||
}
|
||||
|
||||
void FileSystemModel::applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths) {
|
||||
QList<int> removedIndices;
|
||||
for (int i = 0; i < m_entries.size(); ++i) {
|
||||
if (removedPaths.contains(m_entries[i]->path())) {
|
||||
removedIndices << i;
|
||||
}
|
||||
}
|
||||
std::sort(removedIndices.begin(), removedIndices.end(), std::greater<int>());
|
||||
|
||||
// Batch remove old entries
|
||||
int start = -1;
|
||||
int end = -1;
|
||||
for (int idx : std::as_const(removedIndices)) {
|
||||
if (start == -1) {
|
||||
start = idx;
|
||||
end = idx;
|
||||
} else if (idx == end - 1) {
|
||||
end = idx;
|
||||
} else {
|
||||
beginRemoveRows(QModelIndex(), end, start);
|
||||
for (int i = start; i >= end; --i) {
|
||||
m_entries.takeAt(i)->deleteLater();
|
||||
}
|
||||
endRemoveRows();
|
||||
|
||||
start = idx;
|
||||
end = idx;
|
||||
}
|
||||
}
|
||||
if (start != -1) {
|
||||
beginRemoveRows(QModelIndex(), end, start);
|
||||
for (int i = start; i >= end; --i) {
|
||||
m_entries.takeAt(i)->deleteLater();
|
||||
}
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
// Create new entries
|
||||
QList<FileSystemEntry*> newEntries;
|
||||
for (const auto& path : addedPaths) {
|
||||
newEntries << new FileSystemEntry(path, m_dir.relativeFilePath(path), this);
|
||||
}
|
||||
std::sort(newEntries.begin(), newEntries.end(), [this](const FileSystemEntry* a, const FileSystemEntry* b) {
|
||||
return compareEntries(a, b);
|
||||
});
|
||||
|
||||
// Batch insert new entries
|
||||
int insertStart = -1;
|
||||
QList<FileSystemEntry*> batchItems;
|
||||
for (const auto& entry : std::as_const(newEntries)) {
|
||||
const auto it = std::lower_bound(
|
||||
m_entries.begin(), m_entries.end(), entry, [this](const FileSystemEntry* a, const FileSystemEntry* b) {
|
||||
return compareEntries(a, b);
|
||||
});
|
||||
const auto row = static_cast<int>(it - m_entries.begin());
|
||||
|
||||
if (insertStart == -1) {
|
||||
insertStart = row;
|
||||
batchItems << entry;
|
||||
} else if (row == insertStart + batchItems.size()) {
|
||||
batchItems << entry;
|
||||
} else {
|
||||
beginInsertRows(QModelIndex(), insertStart, insertStart + static_cast<int>(batchItems.size()) - 1);
|
||||
for (int i = 0; i < batchItems.size(); ++i) {
|
||||
m_entries.insert(insertStart + i, batchItems[i]);
|
||||
}
|
||||
endInsertRows();
|
||||
|
||||
insertStart = row;
|
||||
batchItems.clear();
|
||||
batchItems << entry;
|
||||
}
|
||||
}
|
||||
if (!batchItems.isEmpty()) {
|
||||
beginInsertRows(QModelIndex(), insertStart, insertStart + static_cast<int>(batchItems.size()) - 1);
|
||||
for (int i = 0; i < batchItems.size(); ++i) {
|
||||
m_entries.insert(insertStart + i, batchItems[i]);
|
||||
}
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
emit entriesChanged();
|
||||
}
|
||||
|
||||
bool FileSystemModel::compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) const {
|
||||
if (a->isDir() != b->isDir()) {
|
||||
return m_sortReverse ^ a->isDir();
|
||||
}
|
||||
const auto cmp = a->relativePath().localeAwareCompare(b->relativePath());
|
||||
return m_sortReverse ? cmp > 0 : cmp < 0;
|
||||
}
|
||||
|
||||
} // namespace caelestia::models
|
||||
@@ -0,0 +1,148 @@
|
||||
#pragma once
|
||||
|
||||
#include <qabstractitemmodel.h>
|
||||
#include <qdir.h>
|
||||
#include <qfilesystemwatcher.h>
|
||||
#include <qfuture.h>
|
||||
#include <qimagereader.h>
|
||||
#include <qmimedatabase.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qqmllist.h>
|
||||
|
||||
namespace caelestia::models {
|
||||
|
||||
class FileSystemEntry : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("FileSystemEntry instances can only be retrieved from a FileSystemModel")
|
||||
|
||||
Q_PROPERTY(QString path READ path CONSTANT)
|
||||
Q_PROPERTY(QString relativePath READ relativePath NOTIFY relativePathChanged)
|
||||
Q_PROPERTY(QString name READ name CONSTANT)
|
||||
Q_PROPERTY(QString baseName READ baseName CONSTANT)
|
||||
Q_PROPERTY(QString parentDir READ parentDir CONSTANT)
|
||||
Q_PROPERTY(QString suffix READ suffix CONSTANT)
|
||||
Q_PROPERTY(qint64 size READ size CONSTANT)
|
||||
Q_PROPERTY(bool isDir READ isDir CONSTANT)
|
||||
Q_PROPERTY(bool isImage READ isImage CONSTANT)
|
||||
Q_PROPERTY(QString mimeType READ mimeType CONSTANT)
|
||||
|
||||
public:
|
||||
explicit FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] QString path() const;
|
||||
[[nodiscard]] QString relativePath() const;
|
||||
[[nodiscard]] QString name() const;
|
||||
[[nodiscard]] QString baseName() const;
|
||||
[[nodiscard]] QString parentDir() const;
|
||||
[[nodiscard]] QString suffix() const;
|
||||
[[nodiscard]] qint64 size() const;
|
||||
[[nodiscard]] bool isDir() const;
|
||||
[[nodiscard]] bool isImage() const;
|
||||
[[nodiscard]] QString mimeType() const;
|
||||
|
||||
void updateRelativePath(const QDir& dir);
|
||||
|
||||
signals:
|
||||
void relativePathChanged();
|
||||
|
||||
private:
|
||||
const QFileInfo m_fileInfo;
|
||||
|
||||
const QString m_path;
|
||||
QString m_relativePath;
|
||||
|
||||
mutable bool m_isImage;
|
||||
mutable bool m_isImageInitialised;
|
||||
|
||||
mutable QString m_mimeType;
|
||||
mutable bool m_mimeTypeInitialised;
|
||||
};
|
||||
|
||||
class FileSystemModel : public QAbstractListModel {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
|
||||
Q_PROPERTY(bool recursive READ recursive WRITE setRecursive NOTIFY recursiveChanged)
|
||||
Q_PROPERTY(bool watchChanges READ watchChanges WRITE setWatchChanges NOTIFY watchChangesChanged)
|
||||
Q_PROPERTY(bool showHidden READ showHidden WRITE setShowHidden NOTIFY showHiddenChanged)
|
||||
Q_PROPERTY(bool sortReverse READ sortReverse WRITE setSortReverse NOTIFY sortReverseChanged)
|
||||
Q_PROPERTY(Filter filter READ filter WRITE setFilter NOTIFY filterChanged)
|
||||
Q_PROPERTY(QStringList nameFilters READ nameFilters WRITE setNameFilters NOTIFY nameFiltersChanged)
|
||||
|
||||
Q_PROPERTY(QQmlListProperty<caelestia::models::FileSystemEntry> entries READ entries NOTIFY entriesChanged)
|
||||
|
||||
public:
|
||||
enum Filter {
|
||||
NoFilter,
|
||||
Images,
|
||||
Files,
|
||||
Dirs
|
||||
};
|
||||
Q_ENUM(Filter)
|
||||
|
||||
explicit FileSystemModel(QObject* parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
[[nodiscard]] QString path() const;
|
||||
void setPath(const QString& path);
|
||||
|
||||
[[nodiscard]] bool recursive() const;
|
||||
void setRecursive(bool recursive);
|
||||
|
||||
[[nodiscard]] bool watchChanges() const;
|
||||
void setWatchChanges(bool watchChanges);
|
||||
|
||||
[[nodiscard]] bool showHidden() const;
|
||||
void setShowHidden(bool showHidden);
|
||||
|
||||
[[nodiscard]] bool sortReverse() const;
|
||||
void setSortReverse(bool sortReverse);
|
||||
|
||||
[[nodiscard]] Filter filter() const;
|
||||
void setFilter(Filter filter);
|
||||
|
||||
[[nodiscard]] QStringList nameFilters() const;
|
||||
void setNameFilters(const QStringList& nameFilters);
|
||||
|
||||
[[nodiscard]] QQmlListProperty<FileSystemEntry> entries();
|
||||
|
||||
signals:
|
||||
void pathChanged();
|
||||
void recursiveChanged();
|
||||
void watchChangesChanged();
|
||||
void showHiddenChanged();
|
||||
void sortReverseChanged();
|
||||
void filterChanged();
|
||||
void nameFiltersChanged();
|
||||
void entriesChanged();
|
||||
|
||||
private:
|
||||
QDir m_dir;
|
||||
QFileSystemWatcher m_watcher;
|
||||
QList<FileSystemEntry*> m_entries;
|
||||
QHash<QString, QFuture<QPair<QSet<QString>, QSet<QString>>>> m_futures;
|
||||
|
||||
QString m_path;
|
||||
bool m_recursive;
|
||||
bool m_watchChanges;
|
||||
bool m_showHidden;
|
||||
bool m_sortReverse;
|
||||
Filter m_filter;
|
||||
QStringList m_nameFilters;
|
||||
|
||||
void watchDirIfRecursive(const QString& path);
|
||||
void update();
|
||||
void updateWatcher();
|
||||
void updateEntries();
|
||||
void updateEntriesForDir(const QString& dir);
|
||||
void applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths);
|
||||
[[nodiscard]] bool compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) const;
|
||||
};
|
||||
|
||||
} // namespace caelestia::models
|
||||
@@ -0,0 +1,14 @@
|
||||
qml_module(caelestia-services
|
||||
URI Caelestia.Services
|
||||
SOURCES
|
||||
service.hpp service.cpp
|
||||
serviceref.hpp serviceref.cpp
|
||||
beattracker.hpp beattracker.cpp
|
||||
audiocollector.hpp audiocollector.cpp
|
||||
audioprovider.hpp audioprovider.cpp
|
||||
cavaprovider.hpp cavaprovider.cpp
|
||||
LIBRARIES
|
||||
PkgConfig::Pipewire
|
||||
PkgConfig::Aubio
|
||||
PkgConfig::Cava
|
||||
)
|
||||
@@ -0,0 +1,246 @@
|
||||
#include "audiocollector.hpp"
|
||||
|
||||
#include "service.hpp"
|
||||
#include <algorithm>
|
||||
#include <pipewire/pipewire.h>
|
||||
#include <qdebug.h>
|
||||
#include <qmutex.h>
|
||||
#include <spa/param/audio/format-utils.h>
|
||||
#include <spa/param/latency-utils.h>
|
||||
#include <stop_token>
|
||||
#include <vector>
|
||||
|
||||
namespace caelestia::services {
|
||||
|
||||
PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector)
|
||||
: m_loop(nullptr)
|
||||
, m_stream(nullptr)
|
||||
, m_timer(nullptr)
|
||||
, m_idle(true)
|
||||
, m_token(token)
|
||||
, m_collector(collector) {
|
||||
pw_init(nullptr, nullptr);
|
||||
|
||||
m_loop = pw_main_loop_new(nullptr);
|
||||
if (!m_loop) {
|
||||
qWarning() << "PipeWireWorker::init: failed to create PipeWire main loop";
|
||||
pw_deinit();
|
||||
return;
|
||||
}
|
||||
|
||||
timespec timeout = { 0, 10 * SPA_NSEC_PER_MSEC };
|
||||
m_timer = pw_loop_add_timer(pw_main_loop_get_loop(m_loop), handleTimeout, this);
|
||||
pw_loop_update_timer(pw_main_loop_get_loop(m_loop), m_timer, &timeout, &timeout, false);
|
||||
|
||||
auto props = pw_properties_new(
|
||||
PW_KEY_MEDIA_TYPE, "Audio", PW_KEY_MEDIA_CATEGORY, "Capture", PW_KEY_MEDIA_ROLE, "Music", nullptr);
|
||||
pw_properties_set(props, PW_KEY_STREAM_CAPTURE_SINK, "true");
|
||||
pw_properties_setf(
|
||||
props, PW_KEY_NODE_LATENCY, "%u/%u", nextPowerOf2(512 * ac::SAMPLE_RATE / 48000), ac::SAMPLE_RATE);
|
||||
pw_properties_set(props, PW_KEY_NODE_PASSIVE, "true");
|
||||
pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
|
||||
pw_properties_set(props, PW_KEY_STREAM_DONT_REMIX, "false");
|
||||
pw_properties_set(props, "channelmix.upmix", "true");
|
||||
|
||||
std::vector<uint8_t> buffer(ac::CHUNK_SIZE);
|
||||
spa_pod_builder b;
|
||||
spa_pod_builder_init(&b, buffer.data(), static_cast<quint32>(buffer.size()));
|
||||
|
||||
spa_audio_info_raw info{};
|
||||
info.format = SPA_AUDIO_FORMAT_S16;
|
||||
info.rate = ac::SAMPLE_RATE;
|
||||
info.channels = 1;
|
||||
|
||||
const spa_pod* params[1];
|
||||
params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info);
|
||||
|
||||
pw_stream_events events{};
|
||||
events.state_changed = [](void* data, pw_stream_state, pw_stream_state state, const char*) {
|
||||
auto* self = static_cast<PipeWireWorker*>(data);
|
||||
self->streamStateChanged(state);
|
||||
};
|
||||
events.process = [](void* data) {
|
||||
auto* self = static_cast<PipeWireWorker*>(data);
|
||||
self->processStream();
|
||||
};
|
||||
|
||||
m_stream = pw_stream_new_simple(pw_main_loop_get_loop(m_loop), "caelestia-shell", props, &events, this);
|
||||
|
||||
const int success = pw_stream_connect(m_stream, PW_DIRECTION_INPUT, PW_ID_ANY,
|
||||
static_cast<pw_stream_flags>(
|
||||
PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS),
|
||||
params, 1);
|
||||
if (success < 0) {
|
||||
qWarning() << "PipeWireWorker::init: failed to connect stream";
|
||||
pw_stream_destroy(m_stream);
|
||||
pw_main_loop_destroy(m_loop);
|
||||
pw_deinit();
|
||||
return;
|
||||
}
|
||||
|
||||
pw_main_loop_run(m_loop);
|
||||
|
||||
pw_stream_destroy(m_stream);
|
||||
pw_main_loop_destroy(m_loop);
|
||||
pw_deinit();
|
||||
}
|
||||
|
||||
void PipeWireWorker::handleTimeout(void* data, uint64_t expirations) {
|
||||
auto* self = static_cast<PipeWireWorker*>(data);
|
||||
|
||||
if (self->m_token.stop_requested()) {
|
||||
pw_main_loop_quit(self->m_loop);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self->m_idle) {
|
||||
if (expirations < 10) {
|
||||
self->m_collector->clearBuffer();
|
||||
} else {
|
||||
self->m_idle = true;
|
||||
timespec timeout = { 0, 500 * SPA_NSEC_PER_MSEC };
|
||||
pw_loop_update_timer(pw_main_loop_get_loop(self->m_loop), self->m_timer, &timeout, &timeout, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PipeWireWorker::streamStateChanged(pw_stream_state state) {
|
||||
m_idle = false;
|
||||
switch (state) {
|
||||
case PW_STREAM_STATE_PAUSED: {
|
||||
timespec timeout = { 0, 10 * SPA_NSEC_PER_MSEC };
|
||||
pw_loop_update_timer(pw_main_loop_get_loop(m_loop), m_timer, &timeout, &timeout, false);
|
||||
break;
|
||||
}
|
||||
case PW_STREAM_STATE_STREAMING:
|
||||
pw_loop_update_timer(pw_main_loop_get_loop(m_loop), m_timer, nullptr, nullptr, false);
|
||||
break;
|
||||
case PW_STREAM_STATE_ERROR:
|
||||
pw_main_loop_quit(m_loop);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void PipeWireWorker::processStream() {
|
||||
if (m_token.stop_requested()) {
|
||||
pw_main_loop_quit(m_loop);
|
||||
return;
|
||||
}
|
||||
|
||||
pw_buffer* buffer = pw_stream_dequeue_buffer(m_stream);
|
||||
if (buffer == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const spa_buffer* buf = buffer->buffer;
|
||||
const qint16* samples = reinterpret_cast<const qint16*>(buf->datas[0].data);
|
||||
if (samples == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const quint32 count = buf->datas[0].chunk->size / 2;
|
||||
m_collector->loadChunk(samples, count);
|
||||
|
||||
pw_stream_queue_buffer(m_stream, buffer);
|
||||
}
|
||||
|
||||
unsigned int PipeWireWorker::nextPowerOf2(unsigned int n) {
|
||||
if (n == 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
n--;
|
||||
n |= n >> 1;
|
||||
n |= n >> 2;
|
||||
n |= n >> 4;
|
||||
n |= n >> 8;
|
||||
n |= n >> 16;
|
||||
n++;
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
AudioCollector& AudioCollector::instance() {
|
||||
static AudioCollector instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void AudioCollector::clearBuffer() {
|
||||
auto* writeBuffer = m_writeBuffer.load(std::memory_order_relaxed);
|
||||
std::fill(writeBuffer->begin(), writeBuffer->end(), 0.0f);
|
||||
|
||||
auto* oldRead = m_readBuffer.exchange(writeBuffer, std::memory_order_acq_rel);
|
||||
m_writeBuffer.store(oldRead, std::memory_order_release);
|
||||
}
|
||||
|
||||
void AudioCollector::loadChunk(const qint16* samples, quint32 count) {
|
||||
if (count > ac::CHUNK_SIZE) {
|
||||
count = ac::CHUNK_SIZE;
|
||||
}
|
||||
|
||||
auto* writeBuffer = m_writeBuffer.load(std::memory_order_relaxed);
|
||||
std::transform(samples, samples + count, writeBuffer->begin(), [](qint16 sample) {
|
||||
return sample / 32768.0f;
|
||||
});
|
||||
|
||||
auto* oldRead = m_readBuffer.exchange(writeBuffer, std::memory_order_acq_rel);
|
||||
m_writeBuffer.store(oldRead, std::memory_order_release);
|
||||
}
|
||||
|
||||
quint32 AudioCollector::readChunk(float* out, quint32 count) {
|
||||
if (count == 0 || count > ac::CHUNK_SIZE) {
|
||||
count = ac::CHUNK_SIZE;
|
||||
}
|
||||
|
||||
auto* readBuffer = m_readBuffer.load(std::memory_order_acquire);
|
||||
std::memcpy(out, readBuffer->data(), count * sizeof(float));
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
quint32 AudioCollector::readChunk(double* out, quint32 count) {
|
||||
if (count == 0 || count > ac::CHUNK_SIZE) {
|
||||
count = ac::CHUNK_SIZE;
|
||||
}
|
||||
|
||||
auto* readBuffer = m_readBuffer.load(std::memory_order_acquire);
|
||||
std::transform(readBuffer->begin(), readBuffer->begin() + count, out, [](float sample) {
|
||||
return static_cast<double>(sample);
|
||||
});
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
AudioCollector::AudioCollector(QObject* parent)
|
||||
: Service(parent)
|
||||
, m_buffer1(ac::CHUNK_SIZE)
|
||||
, m_buffer2(ac::CHUNK_SIZE)
|
||||
, m_readBuffer(&m_buffer1)
|
||||
, m_writeBuffer(&m_buffer2) {}
|
||||
|
||||
AudioCollector::~AudioCollector() {
|
||||
stop();
|
||||
}
|
||||
|
||||
void AudioCollector::start() {
|
||||
if (m_thread.joinable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearBuffer();
|
||||
|
||||
m_thread = std::jthread([this](std::stop_token token) {
|
||||
PipeWireWorker worker(token, this);
|
||||
});
|
||||
}
|
||||
|
||||
void AudioCollector::stop() {
|
||||
if (m_thread.joinable()) {
|
||||
m_thread.request_stop();
|
||||
m_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace caelestia::services
|
||||
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include "service.hpp"
|
||||
#include <atomic>
|
||||
#include <pipewire/pipewire.h>
|
||||
#include <qmutex.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <spa/param/audio/format-utils.h>
|
||||
#include <stop_token>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace caelestia::services {
|
||||
|
||||
namespace ac {
|
||||
|
||||
constexpr quint32 SAMPLE_RATE = 44100;
|
||||
constexpr quint32 CHUNK_SIZE = 512;
|
||||
|
||||
} // namespace ac
|
||||
|
||||
class AudioCollector;
|
||||
|
||||
class PipeWireWorker {
|
||||
public:
|
||||
explicit PipeWireWorker(std::stop_token token, AudioCollector* collector);
|
||||
|
||||
void run();
|
||||
|
||||
private:
|
||||
pw_main_loop* m_loop;
|
||||
pw_stream* m_stream;
|
||||
spa_source* m_timer;
|
||||
bool m_idle;
|
||||
|
||||
std::stop_token m_token;
|
||||
AudioCollector* m_collector;
|
||||
|
||||
static void handleTimeout(void* data, uint64_t expirations);
|
||||
void streamStateChanged(pw_stream_state state);
|
||||
void processStream();
|
||||
|
||||
[[nodiscard]] unsigned int nextPowerOf2(unsigned int n);
|
||||
};
|
||||
|
||||
class AudioCollector : public Service {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
AudioCollector(const AudioCollector&) = delete;
|
||||
AudioCollector& operator=(const AudioCollector&) = delete;
|
||||
|
||||
static AudioCollector& instance();
|
||||
|
||||
void clearBuffer();
|
||||
void loadChunk(const qint16* samples, quint32 count);
|
||||
quint32 readChunk(float* out, quint32 count = 0);
|
||||
quint32 readChunk(double* out, quint32 count = 0);
|
||||
|
||||
private:
|
||||
explicit AudioCollector(QObject* parent = nullptr);
|
||||
~AudioCollector();
|
||||
|
||||
std::jthread m_thread;
|
||||
std::vector<float> m_buffer1;
|
||||
std::vector<float> m_buffer2;
|
||||
std::atomic<std::vector<float>*> m_readBuffer;
|
||||
std::atomic<std::vector<float>*> m_writeBuffer;
|
||||
quint32 m_sampleCount;
|
||||
|
||||
void reload();
|
||||
void start() override;
|
||||
void stop() override;
|
||||
};
|
||||
|
||||
} // namespace caelestia::services
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "audioprovider.hpp"
|
||||
|
||||
#include "audiocollector.hpp"
|
||||
#include "service.hpp"
|
||||
#include <qdebug.h>
|
||||
#include <qthread.h>
|
||||
|
||||
namespace caelestia::services {
|
||||
|
||||
AudioProcessor::AudioProcessor(QObject* parent)
|
||||
: QObject(parent) {}
|
||||
|
||||
AudioProcessor::~AudioProcessor() {
|
||||
stop();
|
||||
}
|
||||
|
||||
void AudioProcessor::init() {
|
||||
m_timer = new QTimer(this);
|
||||
m_timer->setInterval(static_cast<int>(ac::CHUNK_SIZE * 1000.0 / ac::SAMPLE_RATE));
|
||||
connect(m_timer, &QTimer::timeout, this, &AudioProcessor::process);
|
||||
}
|
||||
|
||||
void AudioProcessor::start() {
|
||||
QMetaObject::invokeMethod(&AudioCollector::instance(), &AudioCollector::ref, Qt::QueuedConnection, this);
|
||||
if (m_timer) {
|
||||
m_timer->start();
|
||||
}
|
||||
}
|
||||
|
||||
void AudioProcessor::stop() {
|
||||
if (m_timer) {
|
||||
m_timer->stop();
|
||||
}
|
||||
QMetaObject::invokeMethod(&AudioCollector::instance(), &AudioCollector::unref, Qt::QueuedConnection, this);
|
||||
}
|
||||
|
||||
AudioProvider::AudioProvider(QObject* parent)
|
||||
: Service(parent)
|
||||
, m_processor(nullptr)
|
||||
, m_thread(nullptr) {}
|
||||
|
||||
AudioProvider::~AudioProvider() {
|
||||
if (m_thread) {
|
||||
m_thread->quit();
|
||||
m_thread->wait();
|
||||
}
|
||||
}
|
||||
|
||||
void AudioProvider::init() {
|
||||
if (!m_processor) {
|
||||
qWarning() << "AudioProvider::init: attempted to init with no processor set";
|
||||
return;
|
||||
}
|
||||
|
||||
m_thread = new QThread(this);
|
||||
m_processor->moveToThread(m_thread);
|
||||
|
||||
connect(m_thread, &QThread::started, m_processor, &AudioProcessor::init);
|
||||
connect(m_thread, &QThread::finished, m_processor, &AudioProcessor::deleteLater);
|
||||
connect(m_thread, &QThread::finished, m_thread, &QThread::deleteLater);
|
||||
|
||||
m_thread->start();
|
||||
}
|
||||
|
||||
void AudioProvider::start() {
|
||||
if (m_processor) {
|
||||
AudioCollector::instance(); // Create instance on main thread
|
||||
QMetaObject::invokeMethod(m_processor, &AudioProcessor::start);
|
||||
}
|
||||
}
|
||||
|
||||
void AudioProvider::stop() {
|
||||
if (m_processor) {
|
||||
QMetaObject::invokeMethod(m_processor, &AudioProcessor::stop);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace caelestia::services
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "service.hpp"
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtimer.h>
|
||||
|
||||
namespace caelestia::services {
|
||||
|
||||
class AudioProcessor : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AudioProcessor(QObject* parent = nullptr);
|
||||
~AudioProcessor();
|
||||
|
||||
void init();
|
||||
|
||||
public slots:
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
protected:
|
||||
virtual void process() = 0;
|
||||
|
||||
private:
|
||||
QTimer* m_timer;
|
||||
};
|
||||
|
||||
class AudioProvider : public Service {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AudioProvider(QObject* parent = nullptr);
|
||||
~AudioProvider();
|
||||
|
||||
protected:
|
||||
AudioProcessor* m_processor;
|
||||
|
||||
void init();
|
||||
|
||||
private:
|
||||
QThread* m_thread;
|
||||
|
||||
void start() override;
|
||||
void stop() override;
|
||||
};
|
||||
|
||||
} // namespace caelestia::services
|
||||
@@ -0,0 +1,58 @@
|
||||
#include "beattracker.hpp"
|
||||
|
||||
#include "audiocollector.hpp"
|
||||
#include "audioprovider.hpp"
|
||||
#include <aubio/aubio.h>
|
||||
|
||||
namespace caelestia::services {
|
||||
|
||||
BeatProcessor::BeatProcessor(QObject* parent)
|
||||
: AudioProcessor(parent)
|
||||
, m_tempo(new_aubio_tempo("default", 1024, ac::CHUNK_SIZE, ac::SAMPLE_RATE))
|
||||
, m_in(new_fvec(ac::CHUNK_SIZE))
|
||||
, m_out(new_fvec(2)) {};
|
||||
|
||||
BeatProcessor::~BeatProcessor() {
|
||||
if (m_tempo) {
|
||||
del_aubio_tempo(m_tempo);
|
||||
}
|
||||
if (m_in) {
|
||||
del_fvec(m_in);
|
||||
}
|
||||
del_fvec(m_out);
|
||||
}
|
||||
|
||||
void BeatProcessor::process() {
|
||||
if (!m_tempo || !m_in) {
|
||||
return;
|
||||
}
|
||||
|
||||
AudioCollector::instance().readChunk(m_in->data);
|
||||
|
||||
aubio_tempo_do(m_tempo, m_in, m_out);
|
||||
if (!qFuzzyIsNull(m_out->data[0])) {
|
||||
emit beat(aubio_tempo_get_bpm(m_tempo));
|
||||
}
|
||||
}
|
||||
|
||||
BeatTracker::BeatTracker(QObject* parent)
|
||||
: AudioProvider(parent)
|
||||
, m_bpm(120) {
|
||||
m_processor = new BeatProcessor();
|
||||
init();
|
||||
|
||||
connect(static_cast<BeatProcessor*>(m_processor), &BeatProcessor::beat, this, &BeatTracker::updateBpm);
|
||||
}
|
||||
|
||||
smpl_t BeatTracker::bpm() const {
|
||||
return m_bpm;
|
||||
}
|
||||
|
||||
void BeatTracker::updateBpm(smpl_t bpm) {
|
||||
if (!qFuzzyCompare(bpm + 1.0f, m_bpm + 1.0f)) {
|
||||
m_bpm = bpm;
|
||||
emit bpmChanged();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace caelestia::services
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include "audioprovider.hpp"
|
||||
#include <aubio/aubio.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace caelestia::services {
|
||||
|
||||
class BeatProcessor : public AudioProcessor {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit BeatProcessor(QObject* parent = nullptr);
|
||||
~BeatProcessor();
|
||||
|
||||
signals:
|
||||
void beat(smpl_t bpm);
|
||||
|
||||
protected:
|
||||
void process() override;
|
||||
|
||||
private:
|
||||
aubio_tempo_t* m_tempo;
|
||||
fvec_t* m_in;
|
||||
fvec_t* m_out;
|
||||
};
|
||||
|
||||
class BeatTracker : public AudioProvider {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(smpl_t bpm READ bpm NOTIFY bpmChanged)
|
||||
|
||||
public:
|
||||
explicit BeatTracker(QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] smpl_t bpm() const;
|
||||
|
||||
signals:
|
||||
void bpmChanged();
|
||||
void beat(smpl_t bpm);
|
||||
|
||||
private:
|
||||
smpl_t m_bpm;
|
||||
|
||||
void updateBpm(smpl_t bpm);
|
||||
};
|
||||
|
||||
} // namespace caelestia::services
|
||||
@@ -0,0 +1,140 @@
|
||||
#include "cavaprovider.hpp"
|
||||
|
||||
#include "audiocollector.hpp"
|
||||
#include "audioprovider.hpp"
|
||||
#include <cava/cavacore.h>
|
||||
#include <cstddef>
|
||||
#include <qdebug.h>
|
||||
|
||||
namespace caelestia::services {
|
||||
|
||||
CavaProcessor::CavaProcessor(QObject* parent)
|
||||
: AudioProcessor(parent)
|
||||
, m_plan(nullptr)
|
||||
, m_in(new double[ac::CHUNK_SIZE])
|
||||
, m_out(nullptr)
|
||||
, m_bars(0) {};
|
||||
|
||||
CavaProcessor::~CavaProcessor() {
|
||||
cleanup();
|
||||
delete[] m_in;
|
||||
}
|
||||
|
||||
void CavaProcessor::process() {
|
||||
if (!m_plan || m_bars == 0 || !m_out) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int count = static_cast<int>(AudioCollector::instance().readChunk(m_in));
|
||||
|
||||
// Process in data via cava
|
||||
cava_execute(m_in, count, m_out, m_plan);
|
||||
|
||||
// Apply monstercat filter
|
||||
QVector<double> values(m_bars);
|
||||
|
||||
// Left to right pass
|
||||
const double inv = 1.0 / 1.5;
|
||||
double carry = 0.0;
|
||||
for (int i = 0; i < m_bars; ++i) {
|
||||
carry = std::max(m_out[i], carry * inv);
|
||||
values[i] = carry;
|
||||
}
|
||||
|
||||
// Right to left pass and combine
|
||||
carry = 0.0;
|
||||
for (int i = m_bars - 1; i >= 0; --i) {
|
||||
carry = std::max(m_out[i], carry * inv);
|
||||
values[i] = std::max(values[i], carry);
|
||||
}
|
||||
|
||||
// Update values
|
||||
if (values != m_values) {
|
||||
m_values = std::move(values);
|
||||
emit valuesChanged(m_values);
|
||||
}
|
||||
}
|
||||
|
||||
void CavaProcessor::setBars(int bars) {
|
||||
if (bars < 0) {
|
||||
qWarning() << "CavaProcessor::setBars: bars must be greater than 0. Setting to 0.";
|
||||
bars = 0;
|
||||
}
|
||||
|
||||
if (m_bars != bars) {
|
||||
m_bars = bars;
|
||||
reload();
|
||||
}
|
||||
}
|
||||
|
||||
void CavaProcessor::reload() {
|
||||
cleanup();
|
||||
initCava();
|
||||
}
|
||||
|
||||
void CavaProcessor::cleanup() {
|
||||
if (m_plan) {
|
||||
cava_destroy(m_plan);
|
||||
m_plan = nullptr;
|
||||
}
|
||||
|
||||
if (m_out) {
|
||||
delete[] m_out;
|
||||
m_out = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void CavaProcessor::initCava() {
|
||||
if (m_plan || m_bars == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_plan = cava_init(m_bars, ac::SAMPLE_RATE, 1, 1, 0.85, 50, 10000);
|
||||
m_out = new double[static_cast<size_t>(m_bars)];
|
||||
}
|
||||
|
||||
CavaProvider::CavaProvider(QObject* parent)
|
||||
: AudioProvider(parent)
|
||||
, m_bars(0)
|
||||
, m_values(m_bars, 0.0) {
|
||||
m_processor = new CavaProcessor();
|
||||
init();
|
||||
|
||||
connect(static_cast<CavaProcessor*>(m_processor), &CavaProcessor::valuesChanged, this, &CavaProvider::updateValues);
|
||||
}
|
||||
|
||||
int CavaProvider::bars() const {
|
||||
return m_bars;
|
||||
}
|
||||
|
||||
void CavaProvider::setBars(int bars) {
|
||||
if (bars < 0) {
|
||||
qWarning() << "CavaProvider::setBars: bars must be greater than 0. Setting to 0.";
|
||||
bars = 0;
|
||||
}
|
||||
|
||||
if (m_bars == bars) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_values.resize(bars, 0.0);
|
||||
m_bars = bars;
|
||||
emit barsChanged();
|
||||
emit valuesChanged();
|
||||
|
||||
QMetaObject::invokeMethod(
|
||||
static_cast<CavaProcessor*>(m_processor), &CavaProcessor::setBars, Qt::QueuedConnection, bars);
|
||||
}
|
||||
|
||||
QVector<double> CavaProvider::values() const {
|
||||
return m_values;
|
||||
}
|
||||
|
||||
void CavaProvider::updateValues(QVector<double> values) {
|
||||
if (values != m_values) {
|
||||
m_values = values;
|
||||
emit valuesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace caelestia::services
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include "audioprovider.hpp"
|
||||
#include <cava/cavacore.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace caelestia::services {
|
||||
|
||||
class CavaProcessor : public AudioProcessor {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CavaProcessor(QObject* parent = nullptr);
|
||||
~CavaProcessor();
|
||||
|
||||
void setBars(int bars);
|
||||
|
||||
signals:
|
||||
void valuesChanged(QVector<double> values);
|
||||
|
||||
protected:
|
||||
void process() override;
|
||||
|
||||
private:
|
||||
struct cava_plan* m_plan;
|
||||
double* m_in;
|
||||
double* m_out;
|
||||
|
||||
int m_bars;
|
||||
QVector<double> m_values;
|
||||
|
||||
void reload();
|
||||
void initCava();
|
||||
void cleanup();
|
||||
};
|
||||
|
||||
class CavaProvider : public AudioProvider {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(int bars READ bars WRITE setBars NOTIFY barsChanged)
|
||||
|
||||
Q_PROPERTY(QVector<double> values READ values NOTIFY valuesChanged)
|
||||
|
||||
public:
|
||||
explicit CavaProvider(QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] int bars() const;
|
||||
void setBars(int bars);
|
||||
|
||||
[[nodiscard]] QVector<double> values() const;
|
||||
|
||||
signals:
|
||||
void barsChanged();
|
||||
void valuesChanged();
|
||||
|
||||
private:
|
||||
int m_bars;
|
||||
QVector<double> m_values;
|
||||
|
||||
void updateValues(QVector<double> values);
|
||||
};
|
||||
|
||||
} // namespace caelestia::services
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "service.hpp"
|
||||
|
||||
#include <qdebug.h>
|
||||
#include <qpointer.h>
|
||||
|
||||
namespace caelestia::services {
|
||||
|
||||
Service::Service(QObject* parent)
|
||||
: QObject(parent) {}
|
||||
|
||||
void Service::ref(QObject* sender) {
|
||||
if (m_refs.isEmpty()) {
|
||||
start();
|
||||
}
|
||||
|
||||
QObject::connect(sender, &QObject::destroyed, this, &Service::unref);
|
||||
m_refs << sender;
|
||||
}
|
||||
|
||||
void Service::unref(QObject* sender) {
|
||||
if (m_refs.remove(sender) && m_refs.isEmpty()) {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace caelestia::services
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qset.h>
|
||||
|
||||
namespace caelestia::services {
|
||||
|
||||
class Service : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Service(QObject* parent = nullptr);
|
||||
|
||||
void ref(QObject* sender);
|
||||
void unref(QObject* sender);
|
||||
|
||||
private:
|
||||
QSet<QObject*> m_refs;
|
||||
|
||||
virtual void start() = 0;
|
||||
virtual void stop() = 0;
|
||||
};
|
||||
|
||||
} // namespace caelestia::services
|
||||
@@ -0,0 +1,36 @@
|
||||
#include "serviceref.hpp"
|
||||
|
||||
#include "service.hpp"
|
||||
|
||||
namespace caelestia::services {
|
||||
|
||||
ServiceRef::ServiceRef(Service* service, QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_service(service) {
|
||||
if (m_service) {
|
||||
m_service->ref(this);
|
||||
}
|
||||
}
|
||||
|
||||
Service* ServiceRef::service() const {
|
||||
return m_service;
|
||||
}
|
||||
|
||||
void ServiceRef::setService(Service* service) {
|
||||
if (m_service == service) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_service) {
|
||||
m_service->unref(this);
|
||||
}
|
||||
|
||||
m_service = service;
|
||||
emit serviceChanged();
|
||||
|
||||
if (m_service) {
|
||||
m_service->ref(this);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace caelestia::services
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "service.hpp"
|
||||
#include <qpointer.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace caelestia::services {
|
||||
|
||||
class ServiceRef : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(caelestia::services::Service* service READ service WRITE setService NOTIFY serviceChanged)
|
||||
|
||||
public:
|
||||
explicit ServiceRef(Service* service = nullptr, QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] Service* service() const;
|
||||
void setService(Service* service);
|
||||
|
||||
signals:
|
||||
void serviceChanged();
|
||||
|
||||
private:
|
||||
QPointer<Service> m_service;
|
||||
};
|
||||
|
||||
} // namespace caelestia::services
|
||||
312
.config/quickshell/caelestia/plugin/src/Caelestia/appdb.cpp
Normal file
312
.config/quickshell/caelestia/plugin/src/Caelestia/appdb.cpp
Normal file
@@ -0,0 +1,312 @@
|
||||
#include "appdb.hpp"
|
||||
|
||||
#include <qsqldatabase.h>
|
||||
#include <qsqlquery.h>
|
||||
#include <quuid.h>
|
||||
|
||||
namespace caelestia {
|
||||
|
||||
AppEntry::AppEntry(QObject* entry, unsigned int frequency, QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_entry(entry)
|
||||
, m_frequency(frequency) {
|
||||
const auto mo = m_entry->metaObject();
|
||||
const auto tmo = metaObject();
|
||||
|
||||
for (const auto& prop :
|
||||
{ "name", "comment", "execString", "startupClass", "genericName", "categories", "keywords" }) {
|
||||
const auto metaProp = mo->property(mo->indexOfProperty(prop));
|
||||
const auto thisMetaProp = tmo->property(tmo->indexOfProperty(prop));
|
||||
QObject::connect(m_entry, metaProp.notifySignal(), this, thisMetaProp.notifySignal());
|
||||
}
|
||||
|
||||
QObject::connect(m_entry, &QObject::destroyed, this, [this]() {
|
||||
m_entry = nullptr;
|
||||
deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
QObject* AppEntry::entry() const {
|
||||
return m_entry;
|
||||
}
|
||||
|
||||
quint32 AppEntry::frequency() const {
|
||||
return m_frequency;
|
||||
}
|
||||
|
||||
void AppEntry::setFrequency(unsigned int frequency) {
|
||||
if (m_frequency != frequency) {
|
||||
m_frequency = frequency;
|
||||
emit frequencyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void AppEntry::incrementFrequency() {
|
||||
m_frequency++;
|
||||
emit frequencyChanged();
|
||||
}
|
||||
|
||||
QString AppEntry::id() const {
|
||||
if (!m_entry) {
|
||||
return "";
|
||||
}
|
||||
return m_entry->property("id").toString();
|
||||
}
|
||||
|
||||
QString AppEntry::name() const {
|
||||
if (!m_entry) {
|
||||
return "";
|
||||
}
|
||||
return m_entry->property("name").toString();
|
||||
}
|
||||
|
||||
QString AppEntry::comment() const {
|
||||
if (!m_entry) {
|
||||
return "";
|
||||
}
|
||||
return m_entry->property("comment").toString();
|
||||
}
|
||||
|
||||
QString AppEntry::execString() const {
|
||||
if (!m_entry) {
|
||||
return "";
|
||||
}
|
||||
return m_entry->property("execString").toString();
|
||||
}
|
||||
|
||||
QString AppEntry::startupClass() const {
|
||||
if (!m_entry) {
|
||||
return "";
|
||||
}
|
||||
return m_entry->property("startupClass").toString();
|
||||
}
|
||||
|
||||
QString AppEntry::genericName() const {
|
||||
if (!m_entry) {
|
||||
return "";
|
||||
}
|
||||
return m_entry->property("genericName").toString();
|
||||
}
|
||||
|
||||
QString AppEntry::categories() const {
|
||||
if (!m_entry) {
|
||||
return "";
|
||||
}
|
||||
return m_entry->property("categories").toStringList().join(" ");
|
||||
}
|
||||
|
||||
QString AppEntry::keywords() const {
|
||||
if (!m_entry) {
|
||||
return "";
|
||||
}
|
||||
return m_entry->property("keywords").toStringList().join(" ");
|
||||
}
|
||||
|
||||
AppDb::AppDb(QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_timer(new QTimer(this))
|
||||
, m_uuid(QUuid::createUuid().toString()) {
|
||||
m_timer->setSingleShot(true);
|
||||
m_timer->setInterval(300);
|
||||
QObject::connect(m_timer, &QTimer::timeout, this, &AppDb::updateApps);
|
||||
|
||||
auto db = QSqlDatabase::addDatabase("QSQLITE", m_uuid);
|
||||
db.setDatabaseName(":memory:");
|
||||
db.open();
|
||||
|
||||
QSqlQuery query(db);
|
||||
query.exec("CREATE TABLE IF NOT EXISTS frequencies (id TEXT PRIMARY KEY, frequency INTEGER)");
|
||||
}
|
||||
|
||||
QString AppDb::uuid() const {
|
||||
return m_uuid;
|
||||
}
|
||||
|
||||
QString AppDb::path() const {
|
||||
return m_path;
|
||||
}
|
||||
|
||||
void AppDb::setPath(const QString& path) {
|
||||
auto newPath = path.isEmpty() ? ":memory:" : path;
|
||||
|
||||
if (m_path == newPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_path = newPath;
|
||||
emit pathChanged();
|
||||
|
||||
auto db = QSqlDatabase::database(m_uuid, false);
|
||||
db.close();
|
||||
db.setDatabaseName(newPath);
|
||||
db.open();
|
||||
|
||||
QSqlQuery query(db);
|
||||
query.exec("CREATE TABLE IF NOT EXISTS frequencies (id TEXT PRIMARY KEY, frequency INTEGER)");
|
||||
|
||||
updateAppFrequencies();
|
||||
}
|
||||
|
||||
QObjectList AppDb::entries() const {
|
||||
return m_entries;
|
||||
}
|
||||
|
||||
void AppDb::setEntries(const QObjectList& entries) {
|
||||
if (m_entries == entries) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_entries = entries;
|
||||
emit entriesChanged();
|
||||
|
||||
m_timer->start();
|
||||
}
|
||||
|
||||
QStringList AppDb::favouriteApps() const {
|
||||
return m_favouriteApps;
|
||||
}
|
||||
|
||||
void AppDb::setFavouriteApps(const QStringList& favApps) {
|
||||
if (m_favouriteApps == favApps) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_favouriteApps = favApps;
|
||||
emit favouriteAppsChanged();
|
||||
m_favouriteAppsRegex.clear();
|
||||
m_favouriteAppsRegex.reserve(m_favouriteApps.size());
|
||||
for (const QString& item : std::as_const(m_favouriteApps)) {
|
||||
const QRegularExpression re(regexifyString(item));
|
||||
if (re.isValid()) {
|
||||
m_favouriteAppsRegex << re;
|
||||
} else {
|
||||
qWarning() << "AppDb::setFavouriteApps: Regular expression is not valid: " << re.pattern();
|
||||
}
|
||||
}
|
||||
|
||||
emit appsChanged();
|
||||
}
|
||||
|
||||
QString AppDb::regexifyString(const QString& original) const {
|
||||
if (original.startsWith('^') && original.endsWith('$'))
|
||||
return original;
|
||||
|
||||
const QString escaped = QRegularExpression::escape(original);
|
||||
return QStringLiteral("^%1$").arg(escaped);
|
||||
}
|
||||
|
||||
QQmlListProperty<AppEntry> AppDb::apps() {
|
||||
return QQmlListProperty<AppEntry>(this, &getSortedApps());
|
||||
}
|
||||
|
||||
void AppDb::incrementFrequency(const QString& id) {
|
||||
auto db = QSqlDatabase::database(m_uuid);
|
||||
QSqlQuery query(db);
|
||||
|
||||
query.prepare("INSERT INTO frequencies (id, frequency) "
|
||||
"VALUES (:id, 1) "
|
||||
"ON CONFLICT (id) DO UPDATE SET frequency = frequency + 1");
|
||||
query.bindValue(":id", id);
|
||||
query.exec();
|
||||
|
||||
auto* app = m_apps.value(id);
|
||||
if (app) {
|
||||
const auto before = getSortedApps();
|
||||
|
||||
app->incrementFrequency();
|
||||
|
||||
if (before != getSortedApps()) {
|
||||
emit appsChanged();
|
||||
}
|
||||
} else {
|
||||
qWarning() << "AppDb::incrementFrequency: could not find app with id" << id;
|
||||
}
|
||||
}
|
||||
|
||||
QList<AppEntry*>& AppDb::getSortedApps() const {
|
||||
m_sortedApps = m_apps.values();
|
||||
std::sort(m_sortedApps.begin(), m_sortedApps.end(), [this](AppEntry* a, AppEntry* b) {
|
||||
bool aIsFav = isFavourite(a);
|
||||
bool bIsFav = isFavourite(b);
|
||||
if (aIsFav != bIsFav) {
|
||||
return aIsFav;
|
||||
}
|
||||
if (a->frequency() != b->frequency()) {
|
||||
return a->frequency() > b->frequency();
|
||||
}
|
||||
return a->name().localeAwareCompare(b->name()) < 0;
|
||||
});
|
||||
return m_sortedApps;
|
||||
}
|
||||
|
||||
bool AppDb::isFavourite(const AppEntry* app) const {
|
||||
for (const QRegularExpression& re : m_favouriteAppsRegex) {
|
||||
if (re.match(app->id()).hasMatch()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
quint32 AppDb::getFrequency(const QString& id) const {
|
||||
auto db = QSqlDatabase::database(m_uuid);
|
||||
QSqlQuery query(db);
|
||||
|
||||
query.prepare("SELECT frequency FROM frequencies WHERE id = :id");
|
||||
query.bindValue(":id", id);
|
||||
|
||||
if (query.exec() && query.next()) {
|
||||
return query.value(0).toUInt();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void AppDb::updateAppFrequencies() {
|
||||
const auto before = getSortedApps();
|
||||
|
||||
for (auto* app : std::as_const(m_apps)) {
|
||||
app->setFrequency(getFrequency(app->id()));
|
||||
}
|
||||
|
||||
if (before != getSortedApps()) {
|
||||
emit appsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void AppDb::updateApps() {
|
||||
bool dirty = false;
|
||||
|
||||
for (const auto& entry : std::as_const(m_entries)) {
|
||||
const auto id = entry->property("id").toString();
|
||||
if (!m_apps.contains(id)) {
|
||||
dirty = true;
|
||||
auto* const newEntry = new AppEntry(entry, getFrequency(id), this);
|
||||
QObject::connect(newEntry, &QObject::destroyed, this, [id, this]() {
|
||||
if (m_apps.remove(id)) {
|
||||
emit appsChanged();
|
||||
}
|
||||
});
|
||||
m_apps.insert(id, newEntry);
|
||||
}
|
||||
}
|
||||
|
||||
QSet<QString> newIds;
|
||||
for (const auto& entry : std::as_const(m_entries)) {
|
||||
newIds.insert(entry->property("id").toString());
|
||||
}
|
||||
|
||||
for (auto it = m_apps.keyBegin(); it != m_apps.keyEnd(); ++it) {
|
||||
const auto& id = *it;
|
||||
if (!newIds.contains(id)) {
|
||||
dirty = true;
|
||||
m_apps.take(id)->deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
if (dirty) {
|
||||
emit appsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace caelestia
|
||||
116
.config/quickshell/caelestia/plugin/src/Caelestia/appdb.hpp
Normal file
116
.config/quickshell/caelestia/plugin/src/Caelestia/appdb.hpp
Normal file
@@ -0,0 +1,116 @@
|
||||
#pragma once
|
||||
|
||||
#include <qhash.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qqmllist.h>
|
||||
#include <qregularexpression.h>
|
||||
#include <qtimer.h>
|
||||
|
||||
namespace caelestia {
|
||||
|
||||
class AppEntry : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("AppEntry instances can only be retrieved from an AppDb")
|
||||
|
||||
// The actual DesktopEntry, but we don't have access to the type so it's a QObject
|
||||
Q_PROPERTY(QObject* entry READ entry CONSTANT)
|
||||
|
||||
Q_PROPERTY(quint32 frequency READ frequency NOTIFY frequencyChanged)
|
||||
Q_PROPERTY(QString id READ id CONSTANT)
|
||||
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
|
||||
Q_PROPERTY(QString comment READ comment NOTIFY commentChanged)
|
||||
Q_PROPERTY(QString execString READ execString NOTIFY execStringChanged)
|
||||
Q_PROPERTY(QString startupClass READ startupClass NOTIFY startupClassChanged)
|
||||
Q_PROPERTY(QString genericName READ genericName NOTIFY genericNameChanged)
|
||||
Q_PROPERTY(QString categories READ categories NOTIFY categoriesChanged)
|
||||
Q_PROPERTY(QString keywords READ keywords NOTIFY keywordsChanged)
|
||||
|
||||
public:
|
||||
explicit AppEntry(QObject* entry, quint32 frequency, QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] QObject* entry() const;
|
||||
|
||||
[[nodiscard]] quint32 frequency() const;
|
||||
void setFrequency(quint32 frequency);
|
||||
void incrementFrequency();
|
||||
|
||||
[[nodiscard]] QString id() const;
|
||||
[[nodiscard]] QString name() const;
|
||||
[[nodiscard]] QString comment() const;
|
||||
[[nodiscard]] QString execString() const;
|
||||
[[nodiscard]] QString startupClass() const;
|
||||
[[nodiscard]] QString genericName() const;
|
||||
[[nodiscard]] QString categories() const;
|
||||
[[nodiscard]] QString keywords() const;
|
||||
|
||||
signals:
|
||||
void frequencyChanged();
|
||||
void nameChanged();
|
||||
void commentChanged();
|
||||
void execStringChanged();
|
||||
void startupClassChanged();
|
||||
void genericNameChanged();
|
||||
void categoriesChanged();
|
||||
void keywordsChanged();
|
||||
|
||||
private:
|
||||
QObject* m_entry;
|
||||
quint32 m_frequency;
|
||||
};
|
||||
|
||||
class AppDb : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(QString uuid READ uuid CONSTANT)
|
||||
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged REQUIRED)
|
||||
Q_PROPERTY(QObjectList entries READ entries WRITE setEntries NOTIFY entriesChanged REQUIRED)
|
||||
Q_PROPERTY(QStringList favouriteApps READ favouriteApps WRITE setFavouriteApps NOTIFY favouriteAppsChanged REQUIRED)
|
||||
Q_PROPERTY(QQmlListProperty<caelestia::AppEntry> apps READ apps NOTIFY appsChanged)
|
||||
|
||||
public:
|
||||
explicit AppDb(QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] QString uuid() const;
|
||||
|
||||
[[nodiscard]] QString path() const;
|
||||
void setPath(const QString& path);
|
||||
|
||||
[[nodiscard]] QObjectList entries() const;
|
||||
void setEntries(const QObjectList& entries);
|
||||
|
||||
[[nodiscard]] QStringList favouriteApps() const;
|
||||
void setFavouriteApps(const QStringList& favApps);
|
||||
|
||||
[[nodiscard]] QQmlListProperty<AppEntry> apps();
|
||||
|
||||
Q_INVOKABLE void incrementFrequency(const QString& id);
|
||||
|
||||
signals:
|
||||
void pathChanged();
|
||||
void entriesChanged();
|
||||
void favouriteAppsChanged();
|
||||
void appsChanged();
|
||||
|
||||
private:
|
||||
QTimer* m_timer;
|
||||
|
||||
const QString m_uuid;
|
||||
QString m_path;
|
||||
QObjectList m_entries;
|
||||
QStringList m_favouriteApps; // unedited string list from qml
|
||||
QList<QRegularExpression> m_favouriteAppsRegex; // pre-regexified m_favouriteApps list
|
||||
QHash<QString, AppEntry*> m_apps;
|
||||
mutable QList<AppEntry*> m_sortedApps;
|
||||
|
||||
QString regexifyString(const QString& original) const;
|
||||
QList<AppEntry*>& getSortedApps() const;
|
||||
bool isFavourite(const AppEntry* app) const;
|
||||
quint32 getFrequency(const QString& id) const;
|
||||
void updateAppFrequencies();
|
||||
void updateApps();
|
||||
};
|
||||
|
||||
} // namespace caelestia
|
||||
131
.config/quickshell/caelestia/plugin/src/Caelestia/cutils.cpp
Normal file
131
.config/quickshell/caelestia/plugin/src/Caelestia/cutils.cpp
Normal file
@@ -0,0 +1,131 @@
|
||||
#include "cutils.hpp"
|
||||
|
||||
#include <QtConcurrent/qtconcurrentrun.h>
|
||||
#include <QtQuick/qquickitemgrabresult.h>
|
||||
#include <QtQuick/qquickwindow.h>
|
||||
#include <qdir.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qfuturewatcher.h>
|
||||
#include <qqmlengine.h>
|
||||
|
||||
namespace caelestia {
|
||||
|
||||
void CUtils::saveItem(QQuickItem* target, const QUrl& path) {
|
||||
this->saveItem(target, path, QRect(), QJSValue(), QJSValue());
|
||||
}
|
||||
|
||||
void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect) {
|
||||
this->saveItem(target, path, rect, QJSValue(), QJSValue());
|
||||
}
|
||||
|
||||
void CUtils::saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved) {
|
||||
this->saveItem(target, path, QRect(), onSaved, QJSValue());
|
||||
}
|
||||
|
||||
void CUtils::saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved, QJSValue onFailed) {
|
||||
this->saveItem(target, path, QRect(), onSaved, onFailed);
|
||||
}
|
||||
|
||||
void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved) {
|
||||
this->saveItem(target, path, rect, onSaved, QJSValue());
|
||||
}
|
||||
|
||||
void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed) {
|
||||
if (!target) {
|
||||
qWarning() << "CUtils::saveItem: a target is required";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!path.isLocalFile()) {
|
||||
qWarning() << "CUtils::saveItem:" << path << "is not a local file";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target->window()) {
|
||||
qWarning() << "CUtils::saveItem: unable to save target" << target << "without a window";
|
||||
return;
|
||||
}
|
||||
|
||||
auto scaledRect = rect;
|
||||
const qreal scale = target->window()->devicePixelRatio();
|
||||
if (rect.isValid() && !qFuzzyCompare(scale + 1.0, 2.0)) {
|
||||
scaledRect =
|
||||
QRectF(rect.left() * scale, rect.top() * scale, rect.width() * scale, rect.height() * scale).toRect();
|
||||
}
|
||||
|
||||
const QSharedPointer<const QQuickItemGrabResult> grabResult = target->grabToImage();
|
||||
|
||||
QObject::connect(grabResult.data(), &QQuickItemGrabResult::ready, this,
|
||||
[grabResult, scaledRect, path, onSaved, onFailed, this]() {
|
||||
const auto future = QtConcurrent::run([=]() {
|
||||
QImage image = grabResult->image();
|
||||
|
||||
if (scaledRect.isValid()) {
|
||||
image = image.copy(scaledRect);
|
||||
}
|
||||
|
||||
const QString file = path.toLocalFile();
|
||||
const QString parent = QFileInfo(file).absolutePath();
|
||||
return QDir().mkpath(parent) && image.save(file);
|
||||
});
|
||||
|
||||
auto* watcher = new QFutureWatcher<bool>(this);
|
||||
auto* engine = qmlEngine(this);
|
||||
|
||||
QObject::connect(watcher, &QFutureWatcher<bool>::finished, this, [=]() {
|
||||
if (watcher->result()) {
|
||||
if (onSaved.isCallable()) {
|
||||
onSaved.call(
|
||||
{ QJSValue(path.toLocalFile()), engine->toScriptValue(QVariant::fromValue(path)) });
|
||||
}
|
||||
} else {
|
||||
qWarning() << "CUtils::saveItem: failed to save" << path;
|
||||
if (onFailed.isCallable()) {
|
||||
onFailed.call({ engine->toScriptValue(QVariant::fromValue(path)) });
|
||||
}
|
||||
}
|
||||
watcher->deleteLater();
|
||||
});
|
||||
watcher->setFuture(future);
|
||||
});
|
||||
}
|
||||
|
||||
bool CUtils::copyFile(const QUrl& source, const QUrl& target, bool overwrite) const {
|
||||
if (!source.isLocalFile()) {
|
||||
qWarning() << "CUtils::copyFile: source" << source << "is not a local file";
|
||||
return false;
|
||||
}
|
||||
if (!target.isLocalFile()) {
|
||||
qWarning() << "CUtils::copyFile: target" << target << "is not a local file";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (overwrite && QFile::exists(target.toLocalFile())) {
|
||||
if (!QFile::remove(target.toLocalFile())) {
|
||||
qWarning() << "CUtils::copyFile: overwrite was specified but failed to remove" << target.toLocalFile();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return QFile::copy(source.toLocalFile(), target.toLocalFile());
|
||||
}
|
||||
|
||||
bool CUtils::deleteFile(const QUrl& path) const {
|
||||
if (!path.isLocalFile()) {
|
||||
qWarning() << "CUtils::deleteFile: path" << path << "is not a local file";
|
||||
return false;
|
||||
}
|
||||
|
||||
return QFile::remove(path.toLocalFile());
|
||||
}
|
||||
|
||||
QString CUtils::toLocalFile(const QUrl& url) const {
|
||||
if (!url.isLocalFile()) {
|
||||
qWarning() << "CUtils::toLocalFile: given url is not a local file" << url;
|
||||
return QString();
|
||||
}
|
||||
|
||||
return url.toLocalFile();
|
||||
}
|
||||
|
||||
} // namespace caelestia
|
||||
29
.config/quickshell/caelestia/plugin/src/Caelestia/cutils.hpp
Normal file
29
.config/quickshell/caelestia/plugin/src/Caelestia/cutils.hpp
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <QtQuick/qquickitem.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace caelestia {
|
||||
|
||||
class CUtils : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
public:
|
||||
// clang-format off
|
||||
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path);
|
||||
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect);
|
||||
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved);
|
||||
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved, QJSValue onFailed);
|
||||
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved);
|
||||
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed);
|
||||
// clang-format on
|
||||
|
||||
Q_INVOKABLE bool copyFile(const QUrl& source, const QUrl& target, bool overwrite = true) const;
|
||||
Q_INVOKABLE bool deleteFile(const QUrl& path) const;
|
||||
Q_INVOKABLE QString toLocalFile(const QUrl& url) const;
|
||||
};
|
||||
|
||||
} // namespace caelestia
|
||||
@@ -0,0 +1,223 @@
|
||||
#include "imageanalyser.hpp"
|
||||
|
||||
#include <QtConcurrent/qtconcurrentrun.h>
|
||||
#include <QtQuick/qquickitemgrabresult.h>
|
||||
#include <qfuturewatcher.h>
|
||||
#include <qimage.h>
|
||||
#include <qquickwindow.h>
|
||||
|
||||
namespace caelestia {
|
||||
|
||||
ImageAnalyser::ImageAnalyser(QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_futureWatcher(new QFutureWatcher<AnalyseResult>(this))
|
||||
, m_source("")
|
||||
, m_sourceItem(nullptr)
|
||||
, m_rescaleSize(128)
|
||||
, m_dominantColour(0, 0, 0)
|
||||
, m_luminance(0) {
|
||||
QObject::connect(m_futureWatcher, &QFutureWatcher<AnalyseResult>::finished, this, [this]() {
|
||||
if (!m_futureWatcher->future().isResultReadyAt(0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto result = m_futureWatcher->result();
|
||||
if (m_dominantColour != result.first) {
|
||||
m_dominantColour = result.first;
|
||||
emit dominantColourChanged();
|
||||
}
|
||||
if (!qFuzzyCompare(m_luminance + 1.0, result.second + 1.0)) {
|
||||
m_luminance = result.second;
|
||||
emit luminanceChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QString ImageAnalyser::source() const {
|
||||
return m_source;
|
||||
}
|
||||
|
||||
void ImageAnalyser::setSource(const QString& source) {
|
||||
if (m_source == source) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_source = source;
|
||||
emit sourceChanged();
|
||||
|
||||
if (m_sourceItem) {
|
||||
m_sourceItem = nullptr;
|
||||
emit sourceItemChanged();
|
||||
}
|
||||
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
QQuickItem* ImageAnalyser::sourceItem() const {
|
||||
return m_sourceItem;
|
||||
}
|
||||
|
||||
void ImageAnalyser::setSourceItem(QQuickItem* sourceItem) {
|
||||
if (m_sourceItem == sourceItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_sourceItem = sourceItem;
|
||||
emit sourceItemChanged();
|
||||
|
||||
if (!m_source.isEmpty()) {
|
||||
m_source = "";
|
||||
emit sourceChanged();
|
||||
}
|
||||
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
int ImageAnalyser::rescaleSize() const {
|
||||
return m_rescaleSize;
|
||||
}
|
||||
|
||||
void ImageAnalyser::setRescaleSize(int rescaleSize) {
|
||||
if (m_rescaleSize == rescaleSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_rescaleSize = rescaleSize;
|
||||
emit rescaleSizeChanged();
|
||||
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
QColor ImageAnalyser::dominantColour() const {
|
||||
return m_dominantColour;
|
||||
}
|
||||
|
||||
qreal ImageAnalyser::luminance() const {
|
||||
return m_luminance;
|
||||
}
|
||||
|
||||
void ImageAnalyser::requestUpdate() {
|
||||
if (m_source.isEmpty() && !m_sourceItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_sourceItem || (m_sourceItem->window() && m_sourceItem->window()->isVisible() && m_sourceItem->width() > 0 &&
|
||||
m_sourceItem->height() > 0)) {
|
||||
update();
|
||||
} else if (m_sourceItem) {
|
||||
if (!m_sourceItem->window()) {
|
||||
QObject::connect(m_sourceItem, &QQuickItem::windowChanged, this, &ImageAnalyser::requestUpdate,
|
||||
Qt::SingleShotConnection);
|
||||
} else if (!m_sourceItem->window()->isVisible()) {
|
||||
QObject::connect(m_sourceItem->window(), &QQuickWindow::visibleChanged, this, &ImageAnalyser::requestUpdate,
|
||||
Qt::SingleShotConnection);
|
||||
}
|
||||
if (m_sourceItem->width() <= 0) {
|
||||
QObject::connect(
|
||||
m_sourceItem, &QQuickItem::widthChanged, this, &ImageAnalyser::requestUpdate, Qt::SingleShotConnection);
|
||||
}
|
||||
if (m_sourceItem->height() <= 0) {
|
||||
QObject::connect(m_sourceItem, &QQuickItem::heightChanged, this, &ImageAnalyser::requestUpdate,
|
||||
Qt::SingleShotConnection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ImageAnalyser::update() {
|
||||
if (m_source.isEmpty() && !m_sourceItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_futureWatcher->isRunning()) {
|
||||
m_futureWatcher->cancel();
|
||||
}
|
||||
|
||||
if (m_sourceItem) {
|
||||
const QSharedPointer<const QQuickItemGrabResult> grabResult = m_sourceItem->grabToImage();
|
||||
QObject::connect(grabResult.data(), &QQuickItemGrabResult::ready, this, [grabResult, this]() {
|
||||
m_futureWatcher->setFuture(QtConcurrent::run(&ImageAnalyser::analyse, grabResult->image(), m_rescaleSize));
|
||||
});
|
||||
} else {
|
||||
m_futureWatcher->setFuture(QtConcurrent::run([=, this](QPromise<AnalyseResult>& promise) {
|
||||
const QImage image(m_source);
|
||||
analyse(promise, image, m_rescaleSize);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
void ImageAnalyser::analyse(QPromise<AnalyseResult>& promise, const QImage& image, int rescaleSize) {
|
||||
if (image.isNull()) {
|
||||
qWarning() << "ImageAnalyser::analyse: image is null";
|
||||
return;
|
||||
}
|
||||
|
||||
QImage img = image;
|
||||
|
||||
if (rescaleSize > 0 && (img.width() > rescaleSize || img.height() > rescaleSize)) {
|
||||
img = img.scaled(rescaleSize, rescaleSize, Qt::KeepAspectRatio, Qt::FastTransformation);
|
||||
}
|
||||
|
||||
if (promise.isCanceled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (img.format() != QImage::Format_ARGB32) {
|
||||
img = img.convertToFormat(QImage::Format_ARGB32);
|
||||
}
|
||||
|
||||
if (promise.isCanceled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uchar* data = img.bits();
|
||||
const int width = img.width();
|
||||
const int height = img.height();
|
||||
const qsizetype bytesPerLine = img.bytesPerLine();
|
||||
|
||||
std::unordered_map<quint32, int> colours;
|
||||
qreal totalLuminance = 0.0;
|
||||
int count = 0;
|
||||
|
||||
for (int y = 0; y < height; ++y) {
|
||||
const uchar* line = data + y * bytesPerLine;
|
||||
for (int x = 0; x < width; ++x) {
|
||||
if (promise.isCanceled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uchar* pixel = line + x * 4;
|
||||
|
||||
if (pixel[3] == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const quint32 mr = static_cast<quint32>(pixel[0] & 0xF8);
|
||||
const quint32 mg = static_cast<quint32>(pixel[1] & 0xF8);
|
||||
const quint32 mb = static_cast<quint32>(pixel[2] & 0xF8);
|
||||
++colours[(mr << 16) | (mg << 8) | mb];
|
||||
|
||||
const qreal r = pixel[0] / 255.0;
|
||||
const qreal g = pixel[1] / 255.0;
|
||||
const qreal b = pixel[2] / 255.0;
|
||||
totalLuminance += std::sqrt(0.299 * r * r + 0.587 * g * g + 0.114 * b * b);
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
quint32 dominantColour = 0;
|
||||
int maxCount = 0;
|
||||
for (const auto& [colour, colourCount] : colours) {
|
||||
if (promise.isCanceled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (colourCount > maxCount) {
|
||||
dominantColour = colour;
|
||||
maxCount = colourCount;
|
||||
}
|
||||
}
|
||||
|
||||
promise.addResult(qMakePair(QColor((0xFFu << 24) | dominantColour), count == 0 ? 0.0 : totalLuminance / count));
|
||||
}
|
||||
|
||||
} // namespace caelestia
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include <QtQuick/qquickitem.h>
|
||||
#include <qfuture.h>
|
||||
#include <qfuturewatcher.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace caelestia {
|
||||
|
||||
class ImageAnalyser : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged)
|
||||
Q_PROPERTY(QQuickItem* sourceItem READ sourceItem WRITE setSourceItem NOTIFY sourceItemChanged)
|
||||
Q_PROPERTY(int rescaleSize READ rescaleSize WRITE setRescaleSize NOTIFY rescaleSizeChanged)
|
||||
Q_PROPERTY(QColor dominantColour READ dominantColour NOTIFY dominantColourChanged)
|
||||
Q_PROPERTY(qreal luminance READ luminance NOTIFY luminanceChanged)
|
||||
|
||||
public:
|
||||
explicit ImageAnalyser(QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] QString source() const;
|
||||
void setSource(const QString& source);
|
||||
|
||||
[[nodiscard]] QQuickItem* sourceItem() const;
|
||||
void setSourceItem(QQuickItem* sourceItem);
|
||||
|
||||
[[nodiscard]] int rescaleSize() const;
|
||||
void setRescaleSize(int rescaleSize);
|
||||
|
||||
[[nodiscard]] QColor dominantColour() const;
|
||||
[[nodiscard]] qreal luminance() const;
|
||||
|
||||
Q_INVOKABLE void requestUpdate();
|
||||
|
||||
signals:
|
||||
void sourceChanged();
|
||||
void sourceItemChanged();
|
||||
void rescaleSizeChanged();
|
||||
void dominantColourChanged();
|
||||
void luminanceChanged();
|
||||
|
||||
private:
|
||||
using AnalyseResult = QPair<QColor, qreal>;
|
||||
|
||||
QFutureWatcher<AnalyseResult>* const m_futureWatcher;
|
||||
|
||||
QString m_source;
|
||||
QQuickItem* m_sourceItem;
|
||||
int m_rescaleSize;
|
||||
|
||||
QColor m_dominantColour;
|
||||
qreal m_luminance;
|
||||
|
||||
void update();
|
||||
static void analyse(QPromise<AnalyseResult>& promise, const QImage& image, int rescaleSize);
|
||||
};
|
||||
|
||||
} // namespace caelestia
|
||||
@@ -0,0 +1,52 @@
|
||||
#include "qalculator.hpp"
|
||||
|
||||
#include <libqalculate/qalculate.h>
|
||||
|
||||
namespace caelestia {
|
||||
|
||||
Qalculator::Qalculator(QObject* parent)
|
||||
: QObject(parent) {
|
||||
if (!CALCULATOR) {
|
||||
new Calculator();
|
||||
CALCULATOR->loadExchangeRates();
|
||||
CALCULATOR->loadGlobalDefinitions();
|
||||
CALCULATOR->loadLocalDefinitions();
|
||||
}
|
||||
}
|
||||
|
||||
QString Qalculator::eval(const QString& expr, bool printExpr) const {
|
||||
if (expr.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
EvaluationOptions eo;
|
||||
PrintOptions po;
|
||||
|
||||
std::string parsed;
|
||||
std::string result = CALCULATOR->calculateAndPrint(
|
||||
CALCULATOR->unlocalizeExpression(expr.toStdString(), eo.parse_options), 100, eo, po, &parsed);
|
||||
|
||||
std::string error;
|
||||
while (CALCULATOR->message()) {
|
||||
if (!CALCULATOR->message()->message().empty()) {
|
||||
if (CALCULATOR->message()->type() == MESSAGE_ERROR) {
|
||||
error += "error: ";
|
||||
} else if (CALCULATOR->message()->type() == MESSAGE_WARNING) {
|
||||
error += "warning: ";
|
||||
}
|
||||
error += CALCULATOR->message()->message();
|
||||
}
|
||||
CALCULATOR->nextMessage();
|
||||
}
|
||||
if (!error.empty()) {
|
||||
return QString::fromStdString(error);
|
||||
}
|
||||
|
||||
if (printExpr) {
|
||||
return QString("%1 = %2").arg(parsed).arg(result);
|
||||
}
|
||||
|
||||
return QString::fromStdString(result);
|
||||
}
|
||||
|
||||
} // namespace caelestia
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace caelestia {
|
||||
|
||||
class Qalculator : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
public:
|
||||
explicit Qalculator(QObject* parent = nullptr);
|
||||
|
||||
Q_INVOKABLE QString eval(const QString& expr, bool printExpr = true) const;
|
||||
};
|
||||
|
||||
} // namespace caelestia
|
||||
@@ -0,0 +1,35 @@
|
||||
#include "requests.hpp"
|
||||
|
||||
#include <qnetworkaccessmanager.h>
|
||||
#include <qnetworkreply.h>
|
||||
#include <qnetworkrequest.h>
|
||||
|
||||
namespace caelestia {
|
||||
|
||||
Requests::Requests(QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_manager(new QNetworkAccessManager(this)) {}
|
||||
|
||||
void Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError) const {
|
||||
if (!onSuccess.isCallable()) {
|
||||
qWarning() << "Requests::get: onSuccess is not callable";
|
||||
return;
|
||||
}
|
||||
|
||||
QNetworkRequest request(url);
|
||||
auto reply = m_manager->get(request);
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished, [reply, onSuccess, onError]() {
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
onSuccess.call({ QString(reply->readAll()) });
|
||||
} else if (onError.isCallable()) {
|
||||
onError.call({ reply->errorString() });
|
||||
} else {
|
||||
qWarning() << "Requests::get: request failed with error" << reply->errorString();
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace caelestia
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <qnetworkaccessmanager.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlengine.h>
|
||||
|
||||
namespace caelestia {
|
||||
|
||||
class Requests : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
public:
|
||||
explicit Requests(QObject* parent = nullptr);
|
||||
|
||||
Q_INVOKABLE void get(const QUrl& url, QJSValue callback, QJSValue onError = QJSValue()) const;
|
||||
|
||||
private:
|
||||
QNetworkAccessManager* m_manager;
|
||||
};
|
||||
|
||||
} // namespace caelestia
|
||||
116
.config/quickshell/caelestia/plugin/src/Caelestia/toaster.cpp
Normal file
116
.config/quickshell/caelestia/plugin/src/Caelestia/toaster.cpp
Normal file
@@ -0,0 +1,116 @@
|
||||
#include "toaster.hpp"
|
||||
|
||||
#include <qdebug.h>
|
||||
#include <qlogging.h>
|
||||
#include <qtimer.h>
|
||||
|
||||
namespace caelestia {
|
||||
|
||||
Toast::Toast(const QString& title, const QString& message, const QString& icon, Type type, int timeout, QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_closed(false)
|
||||
, m_title(title)
|
||||
, m_message(message)
|
||||
, m_icon(icon)
|
||||
, m_type(type)
|
||||
, m_timeout(timeout) {
|
||||
QTimer::singleShot(timeout, this, &Toast::close);
|
||||
|
||||
if (m_icon.isEmpty()) {
|
||||
switch (m_type) {
|
||||
case Type::Success:
|
||||
m_icon = "check_circle_unread";
|
||||
break;
|
||||
case Type::Warning:
|
||||
m_icon = "warning";
|
||||
break;
|
||||
case Type::Error:
|
||||
m_icon = "error";
|
||||
break;
|
||||
default:
|
||||
m_icon = "info";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (timeout <= 0) {
|
||||
switch (m_type) {
|
||||
case Type::Warning:
|
||||
m_timeout = 7000;
|
||||
break;
|
||||
case Type::Error:
|
||||
m_timeout = 10000;
|
||||
break;
|
||||
default:
|
||||
m_timeout = 5000;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Toast::closed() const {
|
||||
return m_closed;
|
||||
}
|
||||
|
||||
QString Toast::title() const {
|
||||
return m_title;
|
||||
}
|
||||
|
||||
QString Toast::message() const {
|
||||
return m_message;
|
||||
}
|
||||
|
||||
QString Toast::icon() const {
|
||||
return m_icon;
|
||||
}
|
||||
|
||||
int Toast::timeout() const {
|
||||
return m_timeout;
|
||||
}
|
||||
|
||||
Toast::Type Toast::type() const {
|
||||
return m_type;
|
||||
}
|
||||
|
||||
void Toast::close() {
|
||||
if (!m_closed) {
|
||||
m_closed = true;
|
||||
emit closedChanged();
|
||||
}
|
||||
|
||||
if (m_locks.isEmpty()) {
|
||||
emit finishedClose();
|
||||
}
|
||||
}
|
||||
|
||||
void Toast::lock(QObject* sender) {
|
||||
m_locks << sender;
|
||||
QObject::connect(sender, &QObject::destroyed, this, &Toast::unlock);
|
||||
}
|
||||
|
||||
void Toast::unlock(QObject* sender) {
|
||||
if (m_locks.remove(sender) && m_closed) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
Toaster::Toaster(QObject* parent)
|
||||
: QObject(parent) {}
|
||||
|
||||
QQmlListProperty<Toast> Toaster::toasts() {
|
||||
return QQmlListProperty<Toast>(this, &m_toasts);
|
||||
}
|
||||
|
||||
void Toaster::toast(const QString& title, const QString& message, const QString& icon, Toast::Type type, int timeout) {
|
||||
auto* toast = new Toast(title, message, icon, type, timeout, this);
|
||||
QObject::connect(toast, &Toast::finishedClose, this, [toast, this]() {
|
||||
if (m_toasts.removeOne(toast)) {
|
||||
emit toastsChanged();
|
||||
toast->deleteLater();
|
||||
}
|
||||
});
|
||||
m_toasts.push_front(toast);
|
||||
emit toastsChanged();
|
||||
}
|
||||
|
||||
} // namespace caelestia
|
||||
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qqmllist.h>
|
||||
#include <qset.h>
|
||||
|
||||
namespace caelestia {
|
||||
|
||||
class Toast : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("Toast instances can only be retrieved from a Toaster")
|
||||
|
||||
Q_PROPERTY(bool closed READ closed NOTIFY closedChanged)
|
||||
Q_PROPERTY(QString title READ title CONSTANT)
|
||||
Q_PROPERTY(QString message READ message CONSTANT)
|
||||
Q_PROPERTY(QString icon READ icon CONSTANT)
|
||||
Q_PROPERTY(int timeout READ timeout CONSTANT)
|
||||
Q_PROPERTY(Type type READ type CONSTANT)
|
||||
|
||||
public:
|
||||
enum class Type {
|
||||
Info = 0,
|
||||
Success,
|
||||
Warning,
|
||||
Error
|
||||
};
|
||||
Q_ENUM(Type)
|
||||
|
||||
explicit Toast(const QString& title, const QString& message, const QString& icon, Type type, int timeout,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] bool closed() const;
|
||||
[[nodiscard]] QString title() const;
|
||||
[[nodiscard]] QString message() const;
|
||||
[[nodiscard]] QString icon() const;
|
||||
[[nodiscard]] int timeout() const;
|
||||
[[nodiscard]] Type type() const;
|
||||
|
||||
Q_INVOKABLE void close();
|
||||
Q_INVOKABLE void lock(QObject* sender);
|
||||
Q_INVOKABLE void unlock(QObject* sender);
|
||||
|
||||
signals:
|
||||
void closedChanged();
|
||||
void finishedClose();
|
||||
|
||||
private:
|
||||
QSet<QObject*> m_locks;
|
||||
|
||||
bool m_closed;
|
||||
QString m_title;
|
||||
QString m_message;
|
||||
QString m_icon;
|
||||
Type m_type;
|
||||
int m_timeout;
|
||||
};
|
||||
|
||||
class Toaster : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
Q_PROPERTY(QQmlListProperty<caelestia::Toast> toasts READ toasts NOTIFY toastsChanged)
|
||||
|
||||
public:
|
||||
explicit Toaster(QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] QQmlListProperty<Toast> toasts();
|
||||
|
||||
Q_INVOKABLE void toast(const QString& title, const QString& message, const QString& icon = QString(),
|
||||
caelestia::Toast::Type type = Toast::Type::Info, int timeout = 5000);
|
||||
|
||||
signals:
|
||||
void toastsChanged();
|
||||
|
||||
private:
|
||||
QList<Toast*> m_toasts;
|
||||
};
|
||||
|
||||
} // namespace caelestia
|
||||
Reference in New Issue
Block a user