Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ set(SRC
sysinfo.cpp
taptowake.cpp
tilttowake.cpp
volumecontrol.cpp)
volumecontrol.cpp
WatchfaceHelper.cpp)
set(HEADERS
sysinfo.h
taptowake.h
tilttowake.h
volumecontrol.h)
volumecontrol.h
WatchfaceHelper.h)

add_library(asteroid-settings ${SRC} ${HEADERS} resources.qrc
${CMAKE_CURRENT_BINARY_DIR}/mceiface.h
Expand All @@ -34,6 +36,7 @@ target_link_libraries(asteroid-settings PRIVATE
Qt5::Quick
Qt5::DBus
Qt5::Multimedia
Qt5::Network
AsteroidApp)

install(TARGETS asteroid-settings
Expand Down
214 changes: 214 additions & 0 deletions src/WatchfaceHelper.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/*
* Copyright (C) 2026 - Timo Könnecke <github.com/moWerk>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#include "WatchfaceHelper.h"

#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QProcess>
#include <QStandardPaths>
#include <QUrl>

static const QStringList PREVIEW_SIZES = {
QStringLiteral("112"), QStringLiteral("128"),
QStringLiteral("144"), QStringLiteral("160"), QStringLiteral("182")
};

WatchfaceHelper *WatchfaceHelper::s_instance = nullptr;

WatchfaceHelper::WatchfaceHelper(QObject *parent)
: QObject(parent)
, m_nam(new QNetworkAccessManager(this))
{
s_instance = this;
// Ensure user watchface and cache directories exist on first run
QDir().mkpath(userWatchfacePath());
QDir().mkpath(cachePath());
}

WatchfaceHelper *WatchfaceHelper::instance()
{
if (!s_instance)
s_instance = new WatchfaceHelper();
return s_instance;
}

QObject *WatchfaceHelper::qmlInstance(QQmlEngine *, QJSEngine *)
{
return instance();
}

// ── Path helpers ──────────────────────────────────────────────────────────────

QString WatchfaceHelper::userDataPath() const
{
return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)
+ QStringLiteral("/asteroid-launcher/");
}

QString WatchfaceHelper::userWatchfacePath() const
{
return userDataPath() + QStringLiteral("watchfaces/");
}

QString WatchfaceHelper::userAssetPath() const
{
return QStringLiteral("file://") + userDataPath();
}

QString WatchfaceHelper::userFontsPath() const
{
return QStandardPaths::writableLocation(QStandardPaths::HomeLocation)
+ QStringLiteral("/.fonts/");
}

QString WatchfaceHelper::cachePath() const
{
return QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
+ QStringLiteral("/watchface-store/");
}

bool WatchfaceHelper::isPathAllowed(const QString &path) const
{
if (path.startsWith(cachePath())) return true;
if (path.startsWith(userDataPath())) return true;
if (path.startsWith(userFontsPath())) return true;
return false;
}

// ── Public API ────────────────────────────────────────────────────────────────

void WatchfaceHelper::downloadFile(const QString &url, const QString &destPath)
{
if (!isPathAllowed(destPath)) {
qWarning() << "WatchfaceHelper: blocked write attempt to" << destPath;
emit downloadComplete(destPath, false);
return;
}

QUrl qurl(url);
QNetworkRequest req(qurl);
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
QNetworkReply *reply = m_nam->get(req);

connect(reply, &QNetworkReply::downloadProgress,
this, [this, destPath](qint64 recv, qint64 total) {
emit downloadProgress(destPath, recv, total);
});

connect(reply, &QNetworkReply::finished,
this, [this, reply, destPath]() {
reply->deleteLater();

if (reply->error() != QNetworkReply::NoError) {
qWarning() << "WatchfaceHelper: download error for"
<< destPath << ":" << reply->errorString();
emit downloadComplete(destPath, false);
return;
}

const QFileInfo fi(destPath);
if (!QDir().mkpath(fi.absolutePath())) {
qWarning() << "WatchfaceHelper: cannot create directory"
<< fi.absolutePath();
emit downloadComplete(destPath, false);
return;
}

QFile file(destPath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
qWarning() << "WatchfaceHelper: cannot open for writing:" << destPath;
emit downloadComplete(destPath, false);
return;
}

file.write(reply->readAll());
file.close();
emit downloadComplete(destPath, true);
});
}

bool WatchfaceHelper::mkpath(const QString &dirPath)
{
return QDir().mkpath(dirPath);
}

bool WatchfaceHelper::removeWatchface(const QString &name)
{
bool removedQml = false;

const QString qmlPath = userWatchfacePath() + name + QStringLiteral(".qml");
if (QFile::exists(qmlPath))
removedQml = QFile::remove(qmlPath);

for (const QString &size : PREVIEW_SIZES) {
const QString p = userDataPath()
+ QStringLiteral("watchfaces-preview/")
+ size + QStringLiteral("/") + name + QStringLiteral(".png");
if (QFile::exists(p)) QFile::remove(p);
}

QDir imgDir(userDataPath() + QStringLiteral("watchface-img/"));
if (imgDir.exists()) {
const QStringList filters = {
name + QStringLiteral("-*"),
name + QStringLiteral(".*")
};
for (const QString &f : imgDir.entryList(filters, QDir::Files))
imgDir.remove(f);
}

return removedQml;
}

void WatchfaceHelper::restartSession()
{
QProcess::execute(QStringLiteral("fc-cache"), {QStringLiteral("-f")});
QProcess::startDetached(QStringLiteral("systemctl"),
{QStringLiteral("--user"), QStringLiteral("restart"), QStringLiteral("asteroid-launcher")});
}

QString WatchfaceHelper::readFile(const QString &path) const
{
if (!path.startsWith(cachePath())) {
qWarning() << "WatchfaceHelper: blocked read attempt from" << path;
return QString();
}
QFile f(path);
if (!f.open(QIODevice::ReadOnly | QIODevice::Text))
return QString();
return QString::fromUtf8(f.readAll());
}

bool WatchfaceHelper::writeFile(const QString &path, const QString &content)
{
if (!isPathAllowed(path)) {
qWarning() << "WatchfaceHelper: blocked write attempt to" << path;
return false;
}
const QFileInfo fi(path);
QDir().mkpath(fi.absolutePath());
QFile f(path);
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
return false;
f.write(content.toUtf8());
return true;
}
113 changes: 113 additions & 0 deletions src/WatchfaceHelper.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright (C) 2026 - Timo Könnecke <github.com/moWerk>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#ifndef WATCHFACEHELPER_H
#define WATCHFACEHELPER_H

#include <QNetworkAccessManager>
#include <QObject>
#include <QQmlEngine>
#include <QString>

class WatchfaceHelper : public QObject
{
Q_OBJECT

public:
explicit WatchfaceHelper(QObject *parent = nullptr);
static WatchfaceHelper *instance();
static QObject *qmlInstance(QQmlEngine *engine, QJSEngine *scriptEngine);

/*!
* \brief Download a remote URL and write to destPath.
* destPath must be within an allowed user-writable path — blocked otherwise.
*/
Q_INVOKABLE void downloadFile(const QString &url, const QString &destPath);

/*!
* \brief Remove all user-folder files belonging to a community watchface.
*/
Q_INVOKABLE bool removeWatchface(const QString &name);

/*!
* \brief Create a directory path recursively (mkdir -p equivalent).
*/
Q_INVOKABLE bool mkpath(const QString &dirPath);

/*!
* \brief Rebuild the fontconfig user cache after font install.
*/
Q_INVOKABLE void restartSession();

/*!
* \brief Base path for cached watchface store thumbnails.
* Returns QStandardPaths::CacheLocation + "/watchface-store/"
*/
Q_INVOKABLE QString cachePath() const;

/*!
* \brief User-writable watchface QML directory.
* Returns ~/.local/share/asteroid-launcher/watchfaces/
*/
Q_INVOKABLE QString userWatchfacePath() const;

/*!
* \brief User-writable asteroid-launcher data root as file:// URL.
* Returns file://~/.local/share/asteroid-launcher/
*/
Q_INVOKABLE QString userAssetPath() const;

/*!
* \brief User-writable fonts directory.
* Returns ~/.fonts/
*/
Q_INVOKABLE QString userFontsPath() const;

/*!
* \brief Read a file from the cache location and return its contents.
* Only files within cachePath() are readable — all other paths are blocked.
*/
Q_INVOKABLE QString readFile(const QString &path) const;

/*!
* \brief Write content to a file at destPath.
* destPath must be within an allowed user-writable path — blocked otherwise.
*/
Q_INVOKABLE bool writeFile(const QString &path, const QString &content);

signals:
/*!
* \brief Emitted when a downloadFile() call completes.
* \param destPath the destination path originally requested
* \param success true if the file was written successfully
*/
void downloadComplete(const QString &destPath, bool success);

/*!
* \brief Emitted periodically during a download for progress tracking.
*/
void downloadProgress(const QString &destPath, qint64 received, qint64 total);

private:
bool isPathAllowed(const QString &path) const;
QString userDataPath() const;

QNetworkAccessManager *m_nam;
static WatchfaceHelper *s_instance;
};

#endif // WATCHFACEHELPER_H
3 changes: 3 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include "tilttowake.h"
#include "taptowake.h"
#include "sysinfo.h"
#include "WatchfaceHelper.h"

int main(int argc, char *argv[])
{
Expand All @@ -38,6 +39,8 @@ int main(int argc, char *argv[])
qmlRegisterType<TiltToWake>("org.asteroid.settings", 1, 0, "TiltToWake");
qmlRegisterType<TapToWake>("org.asteroid.settings", 1, 0, "TapToWake");
qmlRegisterType<SysInfo>("org.asteroid.settings", 1, 0, "SysInfo");
qmlRegisterSingletonType<WatchfaceHelper>("org.asteroid.settings", 1, 0, "WatchfaceHelper",
WatchfaceHelper::qmlInstance);
view->setSource(QUrl("qrc:/qml/main.qml"));
view->rootContext()->setContextProperty("qtVersion", QString(qVersion()));
view->rootContext()->setContextProperty("kernelVersion", QString(buf.release));
Expand Down
Loading