Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ jobs:
- name: Resolve dependencies (Ubuntu, Qt 5)
run: |
sudo apt-get update
sudo apt-get install -y qtbase5-dev qtwebengine5-dev qt5-qmake libx11-dev
sudo apt-get install -y qtbase5-dev qtwebengine5-dev libqt5svg5-dev qt5-qmake libx11-dev
echo "QMAKE=qmake" >> "$GITHUB_ENV"
if: runner.os == 'Linux' && matrix.qt == 5

- name: Resolve dependencies (Ubuntu, Qt 6)
run: |
sudo apt-get update
sudo apt-get install -y qt6-base-dev qt6-webengine-dev qmake6 libgl-dev libx11-dev
sudo apt-get install -y qt6-base-dev qt6-webengine-dev qt6-svg-dev qmake6 libgl-dev libx11-dev
echo "QMAKE=qmake6" >> "$GITHUB_ENV"
if: runner.os == 'Linux' && matrix.qt == 6

Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,17 +76,19 @@ What gets stored (if allowed): cookies.

#### Dependencies

Liquid is built with Qt 6 (Qt 5.15 is still supported) and Qt WebEngine.
Liquid is built with Qt 6 (Qt 5.15 is still supported), Qt WebEngine, and Qt SVG.

On Debian/Ubuntu:

```console
sudo apt install qt6-base-dev qt6-webengine-dev qmake6 libx11-dev build-essential
sudo apt install qt6-base-dev qt6-webengine-dev qt6-svg-dev qmake6 libx11-dev build-essential
```

For Qt 5.15, the packages are `qtbase5-dev qtwebengine5-dev libqt5svg5-dev qt5-qmake` instead.

Optionally, also install `qt6-base-private-dev` (requires Qt 6.5 or newer): it lets Liquid bring windows of already running Liquid apps to front on Wayland, instead of the desktop environment merely showing a notification.

On macOS and Windows, install Qt 6 with the Qt WebEngine, Qt WebChannel, and Qt Positioning modules using the [Qt Online Installer](https://www.qt.io/download-qt-installer).
On macOS and Windows, install Qt 6 with the Qt WebEngine, Qt WebChannel, and Qt Positioning modules (Qt SVG comes with Qt itself) using the [Qt Online Installer](https://www.qt.io/download-qt-installer).

#### Build

Expand Down
3 changes: 3 additions & 0 deletions inc/liquid.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ class Liquid
static QDir getDataDir(void);
static QString getLiquidAppFilePath(const QString liquidAppName);
static QString getDefaultUserAgentString(void);
static QString getDesktopFilePath(const QString liquidAppName);
static QString getDesktopFileIconValue(const QString liquidAppName);
static QStringList getLiquidAppsList(void);
static void raiseWindow(QWidget* window, const QString activationToken = QString());
static void requestActivationToken(QWidget* window, const std::function<void(const QString&)> callback);
Expand All @@ -27,4 +29,5 @@ class Liquid
static void removeDesktopFile(const QString liquidAppName);
static void runLiquidApp(const QString liquidAppName, const QString activationToken = QString());
static void sleep(const int ms);
static void updateDesktopFileIcon(const QString liquidAppName);
};
5 changes: 5 additions & 0 deletions inc/liquidappconfigwindow.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
#include <QtGui>
#include <QtWidgets>

#include "liquidappicon.hpp"

class LiquidAppConfigDialog : public QDialog
{
public:
Expand Down Expand Up @@ -49,6 +51,9 @@ public slots:
QLineEdit* nameInput;
QLineEdit* addressInput;
QCheckBox* createIconCheckBox;
QToolButton* iconButton;
LiquidAppIcon chosenIcon;
bool iconWasEdited = false;
QCheckBox* planningToRunCheckBox = Q_NULLPTR;

// General tab
Expand Down
92 changes: 92 additions & 0 deletions inc/liquidappicon.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#pragma once

#include <QByteArray>
#include <QIcon>
#include <QImage>
#include <QList>
#include <QObject>
#include <QSettings>
#include <QUrl>

#include "lqd.h"

class QNetworkAccessManager;
class QNetworkReply;
class QWebEnginePage;

// A Liquid app's icon: either an SVG image (kept as-is, so it stays sharp at any size), or a raster one (kept as PNG);
// stored inside .liquid files, with a copy next to them for .desktop files to point at
class LiquidAppIcon
{
public:
LiquidAppIcon(void) {}

// Accepts SVG and any raster format Qt can read (picking the largest image for files containing several, e.g. .ico);
// SVG images that could make whatever displays them reach out for something (e.g. external images) get rasterized
static LiquidAppIcon fromData(const QByteArray& data);
static LiquidAppIcon fromImage(const QImage& image);
static LiquidAppIcon fromIcon(const QIcon& icon); // Takes the largest image the icon contains

// The Liquid app's own icon (null if it doesn't have one yet)
static LiquidAppIcon load(QSettings* liquidAppConfig);

// The Liquid app's own icon, or Liquid's icon if it doesn't have one yet
static QIcon loadIcon(QSettings* liquidAppConfig);

// Makes this the Liquid app's icon
void save(QSettings* liquidAppConfig, const QString& liquidAppName) const;

// Takes the icon away from the Liquid app (it'll get fetched from the website again)
static void remove(QSettings* liquidAppConfig, const QString& liquidAppName);

// Puts the icon file in place (for .desktop files) if it's missing, e.g. for icons from older versions of Liquid
static void ensureIconFile(QSettings* liquidAppConfig, const QString& liquidAppName);

// Where the copy of the icon for .desktop files lives (an empty string if there's none)
static QString getIconFilePath(const QString& liquidAppName);
static void removeIconFile(const QString& liquidAppName);

bool isNull(void) const;
bool isSvg(void) const;

// How good of an icon this is (vector images beat raster ones of any size)
int score(void) const;

QImage toImage(const int size = LQD_ICON_MAX_SIZE) const;
QIcon toIcon(void) const;

private:
QByteArray svg;
QImage image;
};

// Finds the best icon a website has to offer (looking at the page that's currently shown), downloads all candidates
// and picks the best one
class LiquidAppIconFetcher : public QObject
{
Q_OBJECT

public:
LiquidAppIconFetcher(QWebEnginePage* page, QObject* parent = Q_NULLPTR);

void start(void);

signals:
// The icon is null if no usable one has been found
void finished(const LiquidAppIcon& icon);

private:
void onPageInspected(const QString& json);
void download(const QUrl& url, const bool isMaskable);
void onDownloaded(QNetworkReply* reply, const bool isMaskable);
void onManifestDownloaded(QNetworkReply* reply);
void finishIfDone(void);

QWebEnginePage* page;
QNetworkAccessManager* networkAccessManager;
QList<QUrl> requestedUrls;
int pendingRequestCount = 0;
LiquidAppIcon bestIcon;
qreal bestScore = 0;
bool isDone = false;
};
13 changes: 12 additions & 1 deletion inc/liquidappwindow.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

#include "liquidappwebpage.hpp"

class LiquidAppIcon;
class LiquidAppIconFetcher;
class LiquidAppWebPage;
class SnapshotFlashOverlay;
class QPainter;
Expand Down Expand Up @@ -75,11 +77,20 @@ public slots:
QVariant runJavaScriptSync(const QString& js, const int timeoutMs = 5000);
void waitForNextFrame(void);
bool waitForDocumentToFinishLoading(void);
void applyIcon(void);
void maybeFetchIcon(void);
void useFetchedIcon(const LiquidAppIcon& icon);
bool isShowingOwnWebsite(void);

QString liquidAppName;

QString liquidAppWindowTitle;
QIcon iconToSave;

// The icon provided by the page that's currently shown (used only until the Liquid app has an icon of its own)
QIcon pageIcon;
// Fetching the website's icon happens once per run, and only if the Liquid app doesn't have one yet
LiquidAppIconFetcher* iconFetcher = Q_NULLPTR;
bool iconFetchAttempted = false;

LiquidAppWebPage* liquidAppWebPage = Q_NULLPTR;
QWebEngineProfile* liquidAppWebProfile = Q_NULLPTR;
Expand Down
7 changes: 7 additions & 0 deletions inc/lqd.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#define LQD_PROG_TITLE "Liquid"
#define LQD_APPS_DIR_NAME "apps"
#define LQD_APP_FILE_EXTENSION "liquid"
#define LQD_ICONS_DIR_NAME "icons" // Copies of Liquid apps' icons, for .desktop files to point at
#define LQD_DEFAULT_BG_COLOR Qt::white
#define LQD_DEFAULT_PROXY_HOST "0.0.0.0"
#define LQD_DEFAULT_PROXY_PORT 8080
Expand Down Expand Up @@ -36,12 +37,18 @@
#define LQD_INSTANCE_MSG_PREVIEW_CSS "preview-css" // Liquid apps only, followed by a space and base64-encoded CSS
#define LQD_INSTANCE_MSG_PREVIEW_FRAME "preview-frame" // Liquid apps only, followed by a space and "on" or "off"
#define LQD_INSTANCE_MSG_PREVIEW_SCROLL "preview-scrollbars" // Liquid apps only, followed by a space and "on" or "off"
#define LQD_INSTANCE_MSG_ICON_CHANGED "icon-changed" // Main window only, followed by a space and Liquid app's name
#define LQD_INSTANCE_MSG_STARTED "started" // Main window only, followed by a space and Liquid app's name
#define LQD_ACTIVATION_TOKEN_TIMEOUT 500 // Milliseconds to wait for the compositor to issue an activation token
#define LQD_PREVIEW_CONNECT_TIMEOUT 100 // Milliseconds to wait for connecting to a Liquid app for previewing settings
#define LQD_RECREATED_WINDOW_RAISE_DELAY 100 // Milliseconds to wait before bringing a re-created native window to front
#define LQD_SCROLLBARS_RELAYOUT_DELAY 100 // Milliseconds to keep the document unscrollable for, to make it redo its scrollbars
#define LQD_CSS_PREVIEW_DELAY 150 // Milliseconds to wait after the last keystroke before previewing CSS
#define LQD_ICON_DOWNLOAD_TIMEOUT 10000 // Milliseconds to wait for an icon (or web app manifest) to download
#define LQD_ICON_MAX_CANDIDATES 16 // How many icons a website gets to offer at most
#define LQD_ICON_MAX_FILE_SIZE (4 * 1024 * 1024) // Bytes
#define LQD_ICON_MAX_SVG_SIZE (512 * 1024) // Bytes, larger SVG icons get rasterized
#define LQD_ICON_MAX_SIZE 256 // Pixels, larger icons get scaled down before they get stored
#define LQD_INSTANCE_STOP_TIMEOUT 5000 // Milliseconds to wait for a Liquid app to quit

/* Names of scripts Liquid injects into web pages */
Expand Down
1 change: 1 addition & 0 deletions inc/mainwindow.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class MainWindow : public QScrollArea
~MainWindow();

// Starts keeping track of the Liquid app by the given name (called when Liquid apps report being started)
void updateLiquidAppIcon(const QString& liquidAppName);
void watchLiquidApp(const QString& liquidAppName);

protected:
Expand Down
4 changes: 3 additions & 1 deletion liquid.pro
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ VERSION_PATCH = 11

VERSION = $${VERSION_MAJOR}.$${VERSION_MINOR}.$${VERSION_PATCH}

QT += core gui network widgets webenginecore webenginewidgets
QT += core gui network svg widgets webenginecore webenginewidgets
CONFIG += c++17
TEMPLATE = app

Expand All @@ -25,6 +25,7 @@ INCLUDEPATH += $${INC_DIR}
HEADERS += inc/lqd.h \
inc/liquid.hpp \
inc/liquidappfile.hpp \
inc/liquidappicon.hpp \
inc/liquidappcookiejar.hpp \
inc/liquidappconfigwindow.hpp \
inc/liquidappwebpage.hpp \
Expand All @@ -35,6 +36,7 @@ HEADERS += inc/lqd.h \

SOURCES += src/liquid.cpp \
src/liquidappfile.cpp \
src/liquidappicon.cpp \
src/liquidappcookiejar.cpp \
src/liquidappconfigwindow.cpp \
src/liquidappwebpage.cpp \
Expand Down
46 changes: 45 additions & 1 deletion src/liquid.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "lqd.h"
#include "liquid.hpp"
#include "liquidappicon.hpp"
#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS)
#include "liquidx11.h"
#endif
Expand Down Expand Up @@ -73,7 +74,7 @@ void Liquid::createDesktopFile(const QString liquidAppName, const QString liquid
context += "[Desktop Entry]\n";
context += "Type=Application\n";
context += "Name=" + liquidAppName + "\n";
context += "Icon=internet-web-browser\n";
context += "Icon=" + getDesktopFileIconValue(liquidAppName) + "\n";
context += "Exec=liquid " + liquidAppName + "\n";
context += "Comment=" + liquidAppStartingUrl + "\n";
context += "Categories=Network;WebBrowser;\n";
Expand Down Expand Up @@ -103,6 +104,49 @@ void Liquid::createDesktopFile(const QString liquidAppName, const QString liquid
#endif
}

QString Liquid::getDesktopFilePath(const QString liquidAppName)
{
// QString desktopPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
return QDir::homePath() + QDir::separator() + "Desktop" + QDir::separator() + liquidAppName + ".desktop";
}

QString Liquid::getDesktopFileIconValue(const QString liquidAppName)
{
const QString iconFilePath = LiquidAppIcon::getIconFilePath(liquidAppName);

return (iconFilePath.isEmpty()) ? "internet-web-browser" : iconFilePath;
}

void Liquid::updateDesktopFileIcon(const QString liquidAppName)
{
#if defined(Q_OS_LINUX)
// Only the icon gets changed, the rest of the file may have been edited by the user
QFile file(getDesktopFilePath(liquidAppName));
if (!file.open(QIODevice::ReadOnly)) {
return;
}
QStringList lines = QString::fromUtf8(file.readAll()).split('\n');
file.close();

bool changed = false;
for (QString& line : lines) {
if (line.startsWith("Icon=")) {
const QString newLine = "Icon=" + getDesktopFileIconValue(liquidAppName);
changed = changed || line != newLine;
line = newLine;
}
}

// Writing in place keeps the file's permissions (it has to remain executable)
if (changed && file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
file.write(lines.join('\n').toUtf8());
file.close();
}
#else
Q_UNUSED(liquidAppName);
#endif
}

bool Liquid::detectDarkMode(void)
{
// Detecting it may involve spawning a process, do it only once per program run
Expand Down
Loading
Loading