diff --git a/README.md b/README.md index 41510bb..79279e0 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,20 @@ sudo make uninstall ``` +## Where things are stored + +Each Liquid app is a single `.liquid` file: an XML document containing everything about that app +(settings, custom CSS and JavaScript, window state, cookies, icon). +Liquid apps reside in the `apps` directory inside of Liquid's data directory, which is located at: + + - GNU/Linux, BSD, and others: `~/.liquid/` + - macOS: `~/Library/Application Support/Liquid/` + - Windows: `%LOCALAPPDATA%\Liquid\` + +Keep in mind that `.liquid` files may contain secrets (cookies, proxy credentials). + + ## Customize -Placing a file named `liquid.qss` into `~/.config/liquid/` will serve as additional stylesheet for the program. +Placing a file named `liquid.qss` into Liquid's data directory will serve as additional stylesheet for the program. You can use [base.qss](res/styles/base.qss) as reference. diff --git a/inc/liquid.hpp b/inc/liquid.hpp index f11ae77..e26dc8f 100644 --- a/inc/liquid.hpp +++ b/inc/liquid.hpp @@ -13,7 +13,8 @@ class Liquid static bool detectDarkModeUncached(void); static QByteArray generateRandomByteArray(const int byteLength); static QDir getAppsDir(void); - static QDir getConfigDir(void); + static QDir getDataDir(void); + static QString getLiquidAppFilePath(const QString liquidAppName); static QString getDefaultUserAgentString(void); static QStringList getLiquidAppsList(void); static void raiseWindow(QWidget* window); diff --git a/inc/liquidappfile.hpp b/inc/liquidappfile.hpp new file mode 100644 index 0000000..7ede2ab --- /dev/null +++ b/inc/liquidappfile.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include + +// The .liquid file format: a single, human-readable XML file per Liquid app, containing everything about it +// (settings, custom CSS and JS, state, cookies, icon), with multi-line code stored verbatim in CDATA sections. +// +// It's plugged into QSettings as a custom format, so the rest of the program keeps working with familiar +// QSettings keys (e.g. "URL", "Proxy/Host"), while this class maps them to and from XML. +class LiquidAppFile +{ +public: + // Registers the format with QSettings upon first use + static QSettings::Format format(void); + + // Opens the .liquid file of the Liquid app by the given name (the caller takes ownership) + static QSettings* open(const QString& liquidAppName); + +private: + static bool read(QIODevice& device, QSettings::SettingsMap& map); + static bool write(QIODevice& device, const QSettings::SettingsMap& map); +}; diff --git a/inc/liquidappwindow.hpp b/inc/liquidappwindow.hpp index 3d4488f..b581ed3 100644 --- a/inc/liquidappwindow.hpp +++ b/inc/liquidappwindow.hpp @@ -59,6 +59,8 @@ public slots: private: void takeSnapshot(const bool fullPage); + void rememberGeometry(void); + static bool isPositionManagedByWindowSystem(void); QImage captureFullPage(void); void renderSnapshot(QPainter* painter); void flash(void); @@ -69,7 +71,7 @@ public slots: void waitForNextFrame(void); bool waitForDocumentToFinishLoading(void); - QString* liquidAppName; + QString liquidAppName; QString liquidAppWindowTitle; QIcon iconToSave; @@ -77,7 +79,7 @@ public slots: LiquidAppWebPage* liquidAppWebPage = Q_NULLPTR; QWebEngineProfile* liquidAppWebProfile = Q_NULLPTR; QWebEngineSettings* liquidAppWebSettings = Q_NULLPTR; - QByteArray liquidAppWindowGeometry; + QRect rememberedGeometry; // Window geometry while in normal state (neither maximized nor full-screen) QTimer* rememberGeometryTimer = Q_NULLPTR; QList zoomFactors; diff --git a/inc/lqd.h b/inc/lqd.h index 6cccd9e..af8d34c 100644 --- a/inc/lqd.h +++ b/inc/lqd.h @@ -3,6 +3,7 @@ /* Various globals */ #define LQD_PROG_TITLE "Liquid" #define LQD_APPS_DIR_NAME "apps" +#define LQD_APP_FILE_EXTENSION "liquid" #define LQD_DEFAULT_BG_COLOR Qt::white #define LQD_DEFAULT_PROXY_HOST "0.0.0.0" #define LQD_DEFAULT_PROXY_PORT 8080 @@ -76,7 +77,13 @@ #define LQD_CFG_KEY_NAME_USE_CUSTOM_BG "UseCustomBackground" // boolean, defaults to FALSE #define LQD_CFG_KEY_NAME_USER_AGENT "UserAgent" // text #define LQD_CFG_KEY_NAME_URL "URL" // text, required -#define LQD_CFG_KEY_NAME_WIN_GEOM "WindowGeometry" // text +#define LQD_CFG_KEY_NAME_WIN_GEOM "WindowGeometry" // text (used only by the main window) +#define LQD_CFG_KEY_NAME_WIN_X "WindowX" // number +#define LQD_CFG_KEY_NAME_WIN_Y "WindowY" // number +#define LQD_CFG_KEY_NAME_WIN_WIDTH "WindowWidth" // number +#define LQD_CFG_KEY_NAME_WIN_HEIGHT "WindowHeight" // number +#define LQD_CFG_KEY_NAME_WIN_MAXIMIZED "WindowMaximized" // boolean, defaults to FALSE +#define LQD_CFG_KEY_NAME_WIN_FULLSCREEN "WindowFullScreen" // boolean, defaults to FALSE #define LQD_CFG_KEY_NAME_ZOOM_LVL "ZoomLevel" // number, defaults to 1 /* Keyboard shortcuts (all windows and dialog boxes) */ diff --git a/liquid.pro b/liquid.pro index 39cbaea..5d10d64 100644 --- a/liquid.pro +++ b/liquid.pro @@ -24,6 +24,7 @@ INCLUDEPATH += $${INC_DIR} HEADERS += inc/lqd.h \ inc/liquid.hpp \ + inc/liquidappfile.hpp \ inc/liquidappcookiejar.hpp \ inc/liquidappconfigwindow.hpp \ inc/liquidappwebpage.hpp \ @@ -33,6 +34,7 @@ HEADERS += inc/lqd.h \ inc/svgsnapshot.hpp \ SOURCES += src/liquid.cpp \ + src/liquidappfile.cpp \ src/liquidappcookiejar.cpp \ src/liquidappconfigwindow.cpp \ src/liquidappwebpage.cpp \ diff --git a/sample-apps/README.md b/sample-apps/README.md index 7cacea0..d5cc720 100644 --- a/sample-apps/README.md +++ b/sample-apps/README.md @@ -1,8 +1,13 @@ # Sample Liquid apps -Copy files from directory `apps` into `~/.config/liquid/apps/` -and then load them by running `liquid `, e.g.: +Copy `.liquid` files from this directory into Liquid's `apps` directory, and then run them using `liquid `: - mkdir -p ~/.config/liquid/apps/ - cp -n apps/* ~/.config/liquid/apps/ + mkdir -p ~/.liquid/apps/ + cp -n *.liquid ~/.liquid/apps/ liquid hackernews + +Liquid's `apps` directory is located at: + + - GNU/Linux, BSD, and others: `~/.liquid/apps/` + - macOS: `~/Library/Application Support/Liquid/apps/` + - Windows: `%LOCALAPPDATA%\Liquid\apps\` diff --git a/sample-apps/fccid.ini b/sample-apps/fccid.ini deleted file mode 100644 index ceb8e57..0000000 --- a/sample-apps/fccid.ini +++ /dev/null @@ -1,9 +0,0 @@ -[General] -AllowCookies=false -AllowThirdPartyCookies=false -EnableJS=false -LockWindowGeometry=true -Title=FCCID -URL=https://fccid.io/ -WindowGeometry=01d9d0cb00030000000005c50000005a000009c100000710000005c5000000a4000009c10000071000000000000000000f00000005c5000000a4000009c100000710 -ZoomLevel=1.0000000238418578 diff --git a/sample-apps/fccid.liquid b/sample-apps/fccid.liquid new file mode 100644 index 0000000..4fb4fcc --- /dev/null +++ b/sample-apps/fccid.liquid @@ -0,0 +1,9 @@ + + + https://fccid.io/ + FCCID + + + + + diff --git a/sample-apps/github.ini b/sample-apps/github.ini deleted file mode 100644 index 6422f73..0000000 --- a/sample-apps/github.ini +++ /dev/null @@ -1,9 +0,0 @@ -[General] -AllowCookies=true -AllowThirdPartyCookies=false -BackgroundColor=#24292e -EnableJS=true -Title=GitHub -URL=https://github.com -WindowGeometry=01d9d0cb000200000000017900000206000005aa0000048b0000017d00000223000005a60000048700000000000000000780 -ZoomLevel=0.9 diff --git a/sample-apps/github.liquid b/sample-apps/github.liquid new file mode 100644 index 0000000..6847686 --- /dev/null +++ b/sample-apps/github.liquid @@ -0,0 +1,9 @@ + + + https://github.com + GitHub + + + + + diff --git a/sample-apps/hackernews.ini b/sample-apps/hackernews.ini deleted file mode 100644 index e604f37..0000000 --- a/sample-apps/hackernews.ini +++ /dev/null @@ -1,13 +0,0 @@ -[General] -AdditionalDomains=hn.algolia.com www.ycombinator.com -AllowCookies=true -AllowThirdPartyCookies=false -BackgroundColor=#f6f6ef -CookiesAllowed=true -EnableJS=true -HideScrollBars=true -Icon=000000220051005000690078006d0061007000490063006f006e0045006e00670069006e0065000000010000000189504e470d0a1a0a0000000d49484452000001000000010008060000005c72a866000000097048597300000ec400000ec401952b0e1b00000c9249444154789ceddd797094f51dc7f1cf86209701f1e2926a295539542ce2550a08111052abad7574dadae9d47a4debc1a82897a1da2a6a6b3dc68ead54c769c75694706a02088a1c55393c10040b05410e112160082124db3f9ed6b1a340b2bfefee3ebbdff7eb4f073fbb4392f73e64f7799e44f23a3da73afd500592920290ef1292ea2535d1a444f25a25f9c1071c4a4805d97e0e00b2a780577fc0a9244700806b0400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c23008063040070ac30db4f20a7346b25b56863b75753255557daede5aa264da5a2e36cb6aa2ba3bf57340801688ca3bf268d7b572a6862b3b76d8d54da5daaafb3d9cb55436e97be774ff84e7d9d34be87b47575f89613fc13a031b6ac92163f6db7d7ee64a9cf95767bb9a87991543cc2666be1447ef81b890034d6f4f152ed3ebbbde163a484e32fc3a09ba5564787efd4ee93661a1c4538e3f83b2f453b374af31eb3db6b778ad4e70abbbd5cd2bc482abec5666bdea3d1d7068d420052517e9fed2fef86393d0a281e21b56c1bbeb36fb7543e217cc72187df7506aa764815f7dbed75e8269d75b9dd5e2e68d1263afcb7503e21fa9aa0d10840aae63e2cedde6ab7e7ed28a07884d4f2a8f09ddddba2af0552e2e83bce584d55f40b412b1d7b48bd2fb3db8bb3966da44137d96ccd28e57dff000420c4c289d2f6b5767bc3c7fa380ab8f0569b0f547db24e5a30317cc73107df6d6954572b4d196db7d7b1a774e6f7edf6e2a8655b69e08d365b53c6445f03a48c00845a3a49dab8dc6e2fdf8f0206df2a356f1dbeb3e91d69c93fc2779ccbe3efb40c49d64b93efb0db3be174a9d725767b71d2aaad74c1af6cb6268f8cfeee1184005858394b5a3dcf6e2f5f8f0206df1e7df827d407af49ef9587ef800098b13c0ae8dc4b3ae362bbbd3868758c74c12f6db6ca0cffae9d230056d6bf212d7bc16eaf649cdd561c0cb94d6a7664f8cedb53a5b58bc277208900d89a3adaeed4dece67e6cf51c091c748030c5efd93f5d16ffe61860058daba5a5af494dddef03c390a183232ba984aa8c5cf489b5784efe07304c0da8c52bbd3854fec2d9d5e62b3952d45c749fd6f08df39b05f9a61f8c94b482200f6767e64fbd9f45c3f0ab07af57ff57169c7faf01dfc1f02900e1513ec4e173ea98fd47398cd56a6b56e27f5bf3e7ca7e633e9a5df86efe04b08403a54ed8cae19602557df111832523aa265f8ceac07a53ddbc377f02504205de63e2c556eb1d9fafa39528fa1365b99d2babdd4ffbaf09dcf3e91e6fc3e7c075f8900a4cbfe6adb5f5ae5da51c0d03ba4a62dc27766de2deddb13be83af4400d269c193d2c71fd86c75394fea3ed8662bdddab497fa5d1bbef3e987d2fc27c277705004209deaeba4a9861f5c29b9cb6e2b9d2e1a25356d1ebe3375ac74a0267c07074500d26de9f3d287cb6cb6be71bed4add8662b5d8eea28f5fd45f8ce9695d2eb7f0ddfc1211180744bd647a7ae5a89fb5180d5ab7fd99d9cee9b0104201356cd91de9f6bb3d5b5af74ea409b2d6b6d3b497daf0edf59f74fe9ed69e13b382c02902996a7b0c6f528e0a2d15261b3f01d4ef7cd18029029ebdf94963d6fb3f5cd7ed2c9036cb6acb4ed2c7dfbe7e13b2b5e94d6bc1abe830621009934658c547fc066ebbb313b0a18365a2a3c226c239994ca0c2fb28ac3220099b46db5b4f02f365b270f888e04e2e0e813a5f37f16bef3e6b3d2a6b7c277d0600420d366fc5aaaadb6d98acbef02868d0a7ff5afab95a6e5d8a71df30001c8b45d1f49731fb1d93a7560f4ae40361d7392cdabff6b7fb2bdc90a1a84006443f90469ef2e9bad6c1f050c1b2d35691ab6b17faf34f31e9be783462100d9b0d7f074e16ec5d12704b3e1d82ed2793f0ddf99f390ed8d56d16004205be63e22edda6cb395ada3008b57ffaa4fa5590fd83c1f341a01c8965ac3d385bb0f96ba9c63b3d550c77691cebb2a7ca7fc5ebbab27a1d10840362d9c286d5b63b355526ab3d350c3c74a0585611bbb3e92e63d6af37c901202904df575767717ee31543ae96c9badc339beab74ee8fc377a6dd25d572ba6f3611806c5b3e59dab0c4662b539f0eb478f5dfb65a5afcb4c9d341ea0840b625eba3535f2df41c16dd4b209d8eef2a9dfda3f09d2986775142ca08401cac9a23ad9a6db395eedf0594dc25153409dbd8b0c4f63e8a481901888bb23ba39361429d5e92bea38076a7487dae0cdfb13ae241300210171b964a4b27d96ca5eb6e4225e3c25ffd57cd8e8e78100b04204ea68db5395df88c8ba3bb0b5b6a7f8ad4e78af01d5efd638500c4c9b635d28289365b25636d763edf2b951281df2e4b2745473a880d021037338d4e173ee312e9845ee13b92d4a1bb74d6e5611bf575d165be112b04206e766d965e36b8bb702261771450322efcd57fe1c4e8bd7fc40a0188a38afba2330643f5ba54ea745ad846c7ee52efc057ffda7dd18550103b04208ef6564a2fdd1bbe934884df53b0a434da0931f7e1e873ff881d021057f31eb3f9a139f30752c79ea9fdbf9d4e93be7559d8e357574a1513c23690360420ae6aaba5e9a5e13b894474cdbe54948c0b7ff5af98205519fc7306694100e26cd15336bf38eb7d79f419fec6e8d8333a7a08b17babf4f21fc23690560420ceac4e172e68220d6de4dd76868f0d7ff59f3e5eda6f740564a4050188bb652f4477150a75ce4fa4a33a35eccf76e826f50efcb7fff6b5d15b7f883502900b2cee95577884547c4bc3feec45a3c2dff79f3a26bad63f628d00e482f7e74a2b6785ef7ce71aa9659b43ff99e3bb869ff1b7f12d69c973611bc80802902b2c4e176e5e24f5bbfed07f66e81de167fc95dd195de804b1470072c587cba4a506afaa036f940a0f7229efb69da57303aff4bbe655e9bdf2b00d640c01c825530dee2edca68374f6412ee839e4b6f0ebfc978d0cfbff91510420977cfc2f69c193e13b178ef8f27f2b3a4eea7b75d8ee5b65d2bad7c33690510420d7cc181fdd4b2f44c79ed1cd44be68d02d52d316a96f26eba52963c29e17328e00e49a4aa34fd77df12dc1e645d2801bc2f6163f236d5919b6818c2300b9a8e2fee89e7a21ba0f892ef32549fd6f905a1ce6edc14339506373de02328e00e4a2eacae89e7a21120969e04d526133a9f8e6b0ad571e973edd10b681ac2000b96adea3d2ce4d611be75e250dba496add3ef58d7d7ba4977e13f63c9035042057d51a1c76376b255d1a782431fb41e9b31d611bc81a0290cb163f2d6d7d3f6c23e433ff7bb64bb37f17f6f8c82a0290cb2cef2e9c8a17ef916aaab2f7f808460072ddf2c9d2bfb3f0e19b1deba5f97fccfce3c21401c807930d4e176eac69e3a4039cee9beb08403e58f38af45e45e61e6ff30ae9f5bf65eef1903604205f58dd5db8418f358ad37df30401c8171b974b4bfe9efec759bb487a677afa1f07194100f2c9d4b1e9bf0c1777f7cd2b04209f6c5f2bbdf6e7f4edbf3b53fa607efaf691710420dfccbc3b3defcd27eba52929de6004b14500f24dba6ec6f1c6b3d2a677ec77915504201fcd7a40aa32fc7c7e5d6df4be3ff20e01c847d5467717fe9ff94f489facb3db436c10807cf5ca63d2ce8de13b3555d167fe91970840beaaad9156cf0bdf79f92169f7b6f01dc41201c0a1ade01afff98c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c712c96b94ccf69300901d1c01008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e0588112d97e0a00b222c11100e05a81129aa4a4c49100e04442faefcffca4ff002a6d8284ddf2bc970000000049454e44ae426082ffffffff00000100000001000000000000000001 -URL=https://news.ycombinator.com/ -WindowGeometry=01d9d0cb00030000000002f50000001b000004c5000003ef000002f500000040000004c5000003ef00000000000000000780000002f500000040000004c5000003ef -LockWindowGeometry=false -ZoomLevel=0.9 diff --git a/sample-apps/hackernews.liquid b/sample-apps/hackernews.liquid new file mode 100644 index 0000000..de57e70 --- /dev/null +++ b/sample-apps/hackernews.liquid @@ -0,0 +1,13 @@ + + + https://news.ycombinator.com/ + + hn.algolia.com + www.ycombinator.com + + + + + + iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAMkklEQVR4nO3deXCU9R3H8c+GIJcB8eKSailVOVQs4lUKCBEQUqutdXTa2unUek3rwagol6HaKmprPcaOrVTHacdWlHBqAgiKHFU5PBAECwVBDhEhYAghJNs/ntaxo0Cyv+/uPrvf9+tPBz+7Q5L3PmT3eZ5E8jo9pzr9UAWSkgKQ7xKS6iU10aRE8lol+cEHHEpIBdl+DgCyp4BXf8CpJEcAgGsEAHCMAACOEQDAMQIAOEYAAMcIAOAYAQAcIwCAYwQAcIwAAI4RAMAxAgA4RgAAxwgA4BgBABwjAIBjBABwjAAAjhEAwDECADhGAADHCADgGAEAHCMAgGMEAHCMAACOEQDAMQIAOEYAAMcIAOAYAQAcIwCAYwQAcIwAAI4RAMAxAgA4RgAAxwgA4BgBABwjAIBjBABwjAAAjhEAwDECADhGAADHCADgGAEAHCMAgGMEAHCMAACOEQDAMQIAOEYAAMcIAOAYAQAcIwCAYwQAcKww208gpzRrJbVoY7dXUyVVV9rt5aomTaWi42y2qiujv1c0CAFojKO/Jo17VypoYrO3bY1U2l2qr7PZy1VDbpe+d0/4Tn2dNL6HtHV1+JYT/BOgMbaskhY/bbfX7mSpz5V2e7moeZFUPMJma+FEfvgbiQA01vTxUu0+u73hY6SE4y/DoJulVkeH79Tuk2YaHEU44/g7L0U7N0rzHrPba3eK1OcKu71c0rxIKr7FZmveo9HXBo1CAFJRfp/tL++GOT0KKB4htWwbvrNvt1Q+IXzHIYffdQaqdkgV99vtdegmnXW53V4uaNEmOvy3UD4h+pqg0QhAquY+LO3earfn7SigeITU8qjwnd3boq8FUuLoO85YTVX0C0ErHXtIvS+z24uzlm2kQTfZbM0o5X3/AAQgxMKJ0va1dnvDx/o4CrjwVpsPVH2yTlowMXzHMQffbWlUVytNGW2317GndOb37fbiqGVbaeCNNltTxkRfA6SMAIRaOknauNxuL9+PAgbfKjVvHb6z6R1pyT/Cd5zL4++0DEnWS5PvsNs74XSp1yV2e3HSqq10wa9stiaPjP7uEYQAWFg5S1o9z24vX48CBt8effgn1AevSe+Vh++AAJixPAro3Es642K7vThodYx0wS9ttsoM/66dIwBW1r8hLXvBbq9knN1WHAy5TWp2ZPjO21OltYvCdyCJANiaOtru1N7OZ+bPUcCRx0gDDF79k/XRb/5hhgBY2rpaWvSU3d7wPDkKGDIyuphKqMXPSJtXhO/gcwTA2oxSu9OFT+wtnV5is5UtRcdJ/W8I3zmwX5ph+MlLSCIA9nZ+ZPvZ9Fw/CrB69X/1cWnH+vAd/B8CkA4VE+xOFz6pj9RzmM1WprVuJ/W/Pnyn5jPppd+G7+BLCEA6VO2MrhlgJVffERgyUjqiZfjOrAelPdvDd/AlBCBd5j4sVW6x2fr6OVKPoTZbmdK6vdT/uvCdzz6R5vw+fAdfiQCky/5q219a5dpRwNA7pKYtwndm3i3t2xO+g69EANJpwZPSxx/YbHU5T+o+2GYr3dq0l/pdG77z6YfS/CfCd3BQBCCd6uukqYYfXCm5y24rnS4aJTVtHr4zdax0oCZ8BwdFANJt6fPSh8tstr5xvtSt2GYrXY7qKPX9RfjOlpXS638N38EhEYB0S9ZHp65aiftRgNWrf9mdnO6bAQQgE1bNkd6fa7PVta906kCbLWttO0l9rw7fWfdP6e1p4Ts4LAKQKZansMb1KOCi0VJhs/AdTvfNGAKQKevflJY9b7P1zX7SyQNstqy07Sx9++fhOytelNa8Gr6DBiEAmTRljFR/wGbruzE7Chg2Wio8ImwjmZTKDC+yisMiAJm0bbW08C82WycPiI4E4uDoE6Xzfxa+8+az0qa3wnfQYAQg02b8WqqtttmKy+8Cho0Kf/Wvq5Wm5dinHfMAAci0XR9Jcx+x2Tp1YPSuQDYdc5LNq/9rf7K9yQoahABkQ/kEae8um61sHwUMGy01aRq2sX+vNPMem+eDRiEA2bDX8HThbsXRJwSz4dgu0nk/Dd+Z85DtjVbRYAQgW+Y+Iu3abLOVraMAi1f/qk+lWQ/YPB80GgHIllrD04W7D5a6nGOz1VDHdpHOuyp8p/xeu6snodEIQDYtnChtW2OzVVJqs9NQw8dKBYVhG7s+kuY9avN8kBICkE31dXZ3F+4xVDrpbJutwzm+q3Tuj8N3pt0l1XK6bzYRgGxbPlnasMRmK1OfDrR49d+2Wlr8tMnTQeoIQLYl66NTXy30HBbdSyCdju8qnf2j8J0phndRQsoIQBysmiOtmm2zle7fBZTcJRU0CdvYsMT2PopIGQGIi7I7o5NhQp1ekr6jgHanSH2uDN+xOuJBMAIQFxuWSksn2Wyl625CJePCX/1XzY6OeBALBCBOpo21OV34jIujuwtban+K1OeK8B1e/WOFAMTJtjXSgok2WyVjbXY+3yuVEoHfLksnRUc6iA0CEDczjU4XPuMS6YRe4TuS1KG7dNblYRv1ddFlvhErBCBudm2WXja4u3AiYXcUUDIu/NV/4cTovX/ECgGIo4r7ojMGQ/W6VOp0WthGx+5S78BX/9p90YVQEDsEII72Vkov3Ru+k0iE31OwpDTaCTH34ehz/4gdAhBX8x6z+aE58wdSx56p/b+dTpO+dVnY41dXShUTwjaQNgQgrmqrpeml4TuJRHTNvlSUjAt/9a+YIFUZ/HMGaUEA4mzRUza/OOt9efQZ/sbo2DM6egixe6v08h/CNpBWBCDOrE4XLmgiDW3k3XaGjw1/9Z8+XtpvdAVkpAUBiLtlL0R3FQp1zk+kozo17M926Cb1Dvy3//a10Vt/iDUCkAss7pVXeIRUfEvD/uxFo8Lf9586JrrWP2KNAOSC9+dKK2eF73znGqllm0P/meO7hp/xt/EtaclzYRvICAKQKyxOF25eJPW7/tB/Zugd4Wf8ld0ZXegEsUcAcsWHy6SlBq+qA2+UCg9yKe+2naVzA6/0u+ZV6b3ysA1kDAHIJVMN7i7cpoN09kEu6DnktvDr/JeNDPv/kVEEIJd8/C9pwZPhOxeO+PJ/KzpO6nt12O5bZdK618M2kFEEINfMGB/dSy9Ex57RzUS+aNAtUtMWqW8m66UpY8KeFzKOAOSaSqNP133xLcHmRdKAG8L2Fj8jbVkZtoGMIwC5qOL+6J56IboPiS7zJUn9b5BaHObtwUM5UGNz3gIyjgDkourK6J56IRIJaeBNUmEzqfjmsK1XHpc+3RC2gawgALlq3qPSzk1hG+deJQ26SWrdPvWNfXukl34T9jyQNQQgV9UaHHY3ayVdGngkMftB6bMdYRvIGgKQyxY/LW19P2wj5DP/e7ZLs38X9vjIKgKQyyzvLpyKF++Raqqy9/gIRgBy3fLJ0r+z8OGbHeul+X/M/OPCFAHIB5MNThdurGnjpAOc7pvrCEA+WPOK9F5F5h5v8wrp9b9l7vGQNgQgX1jdXbhBjzWK033zBAHIFxuXS0v+nv7HWbtIemd6+h8HGUEA8snUsem/DBd3980rBCCfbF8rvfbn9O2/O1P6YH769pFxBCDfzLw7Pe/NJ+ulKSneYASxRQDyTbpuxvHGs9Kmd+x3kVUEIB/NekCqMvx8fl1t9L4/8g4ByEfVRncX/p/5T0ifrLPbQ2wQgHz1ymPSzo3hOzVV0Wf+kZcIQL6qrZFWzwvfefkhafe28B3EEgHAoa3gGv/5jAAAjhEAwDECADhGAADHCADgGAEAHCMAgGMEAHCMAACOEQDAMQIAOEYAAMcIAOAYAQAcIwCAYwQAcIwAAI4RAMAxAgA4RgAAxwgA4BgBABwjAIBjBABwjAAAjhEAwDECADhGAADHCADgGAEAHCMAgGMEAHCMAACOEQDAMQIAOEYAAMcIAOAYAQAcIwCAYwQAcIwAAI4RAMAxAgA4RgAAxwgA4BgBABwjAIBjBABwjAAAjhEAwDECADhGAADHEslrlMz2kwCQHRwBAI4RAMAxAgA4RgAAxwgA4BgBABwjAIBjBABwjAAAjhEAwDECADhGAADHCADgGAEAHCMAgGMEAHCMAACOEQDAMQIAOEYAAMcIAOAYAQAcIwCAYwQAcIwAAI4RAMAxAgA4RgAAxwgA4BgBABwjAIBjBABwjAAAjhEAwDECADhGAADHCADgGAEAHCMAgGMEAHCMAACOEQDAMQIAOEYAAMcIAOAYAQAcIwCAYwQAcIwAAI4RAMAxAgA4RgAAxwgA4FiBEtl+CgCyIsERAOBagRKapKTEkQDgREL678/8pP8AKm2ChN3yvJcAAAAASUVORK5CYII= + diff --git a/sample-apps/http-auth-demo.ini b/sample-apps/http-auth-demo.ini deleted file mode 100644 index 86e3cdb..0000000 --- a/sample-apps/http-auth-demo.ini +++ /dev/null @@ -1,5 +0,0 @@ -[General] -AllowCookies=false -AllowThirdPartyCookies=false -EnableJS=false -URL=https://authenticationtest.com/HTTPAuth/ diff --git a/sample-apps/http-auth-demo.liquid b/sample-apps/http-auth-demo.liquid new file mode 100644 index 0000000..cf1d8fb --- /dev/null +++ b/sample-apps/http-auth-demo.liquid @@ -0,0 +1,6 @@ + + + https://authenticationtest.com/HTTPAuth/ + + + diff --git a/src/liquid.cpp b/src/liquid.cpp index 8db84db..87cf1e1 100644 --- a/src/liquid.cpp +++ b/src/liquid.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -39,7 +40,7 @@ void Liquid::applyQtStyleSheets(QWidget* widget) // Load user-defined stylesheet { - QFile customStyleSheetFile(Liquid::getConfigDir().absolutePath() + QDir::separator() + PROG_NAME ".qss"); + QFile customStyleSheetFile(Liquid::getDataDir().filePath(PROG_NAME ".qss")); if (customStyleSheetFile.open(QFile::ReadOnly)) { styleSheet += QLatin1String(customStyleSheetFile.readAll()); customStyleSheetFile.close(); @@ -146,20 +147,31 @@ QByteArray Liquid::generateRandomByteArray(const int byteLength) QDir Liquid::getAppsDir(void) { - return QDir(getConfigDir().absolutePath() + QDir::separator() + LQD_APPS_DIR_NAME + QDir::separator()); + const QString path = getDataDir().filePath(LQD_APPS_DIR_NAME); + + QDir().mkpath(path); + + return QDir(path); } -QDir Liquid::getConfigDir(void) +QDir Liquid::getDataDir(void) { - const QSettings* settings = new QSettings(QSettings::IniFormat, - QSettings::UserScope, - PROG_NAME, - PROG_NAME, - Q_NULLPTR); +#if defined(Q_OS_MACOS) || defined(Q_OS_WIN) + // ~/Library/Application Support/Liquid/ on macOS, %LOCALAPPDATA%\Liquid\ on Windows + const QString path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QDir::separator() + LQD_PROG_TITLE; +#else + // ~/.liquid/ on GNU/Linux, BSD, etc (it's home to Liquid apps, not just to Liquid's own configuration) + const QString path = QDir::homePath() + QDir::separator() + "." PROG_NAME; +#endif - QFileInfo settingsFileInfo(settings->fileName()); + QDir().mkpath(path); - return QDir(settingsFileInfo.absolutePath() + QDir::separator()); + return QDir(path); +} + +QString Liquid::getLiquidAppFilePath(const QString liquidAppName) +{ + return getAppsDir().filePath(liquidAppName + "." LQD_APP_FILE_EXTENSION); } QString Liquid::getDefaultUserAgentString(void) @@ -169,7 +181,7 @@ QString Liquid::getDefaultUserAgentString(void) QStringList Liquid::getLiquidAppsList(void) { - const QFileInfoList liquidAppsFileList = getAppsDir().entryInfoList(QStringList() << "*.ini", + const QFileInfoList liquidAppsFileList = getAppsDir().entryInfoList(QStringList() << "*." LQD_APP_FILE_EXTENSION, QDir::Files | QDir::NoDotAndDotDot, QDir::Name | QDir::IgnoreCase); QStringList liquidAppsNames; diff --git a/src/liquidappconfigwindow.cpp b/src/liquidappconfigwindow.cpp index c9e0306..678e9f0 100644 --- a/src/liquidappconfigwindow.cpp +++ b/src/liquidappconfigwindow.cpp @@ -3,6 +3,7 @@ #include #include "liquid.hpp" +#include "liquidappfile.hpp" #include "liquidappconfigwindow.hpp" #include "lqd.h" #include "mainwindow.hpp" @@ -16,11 +17,7 @@ LiquidAppConfigDialog::LiquidAppConfigDialog(QWidget* parent, QString liquidAppN Liquid::applyQtStyleSheets(this); // Attempt to load liquid app's config file - QSettings* existingLiquidAppConfig = new QSettings(QSettings::IniFormat, - QSettings::UserScope, - QString(PROG_NAME "%1" LQD_APPS_DIR_NAME).arg(QDir::separator()), - liquidAppName, - Q_NULLPTR); + QSettings* existingLiquidAppConfig = LiquidAppFile::open(liquidAppName); // Check to see if Liquid app by this name already has config file if (liquidAppName.size() > 0) { @@ -814,11 +811,7 @@ void LiquidAppConfigDialog::save() return; } - QSettings* tempLiquidAppConfig = new QSettings(QSettings::IniFormat, - QSettings::UserScope, - QString(PROG_NAME) + QDir::separator() + LQD_APPS_DIR_NAME, - appName, - Q_NULLPTR); + QSettings* tempLiquidAppConfig = LiquidAppFile::open(appName); // URL { diff --git a/src/liquidappfile.cpp b/src/liquidappfile.cpp new file mode 100644 index 0000000..04500c2 --- /dev/null +++ b/src/liquidappfile.cpp @@ -0,0 +1,337 @@ +#include +#include +#include +#include + +#include "liquid.hpp" +#include "liquidappfile.hpp" +#include "lqd.h" + +namespace { + +const int FORMAT_VERSION = 1; +const QString ROOT_ELEMENT = QStringLiteral("liquid"); +const QString ICON_DATA_URL_PREFIX = QStringLiteral("data:image/png;base64,"); +const QString COOKIES_KEY_PREFIX = QStringLiteral(LQD_CFG_GROUP_NAME_COOKIES "/"); + +// Settings stored as text of their own elements +struct TextMapping { + const char* key; + const char* element; +}; + +const TextMapping TEXT_MAPPINGS[] = { + { LQD_CFG_KEY_NAME_URL, "url" }, + { LQD_CFG_KEY_NAME_TITLE, "title" }, + { LQD_CFG_KEY_NAME_NOTES, "notes" }, + { LQD_CFG_KEY_NAME_USER_AGENT, "user-agent" }, +}; + +// Settings stored as attributes, grouped into elements (written in the order of their first appearance) +struct AttributeMapping { + const char* key; + const char* element; + const char* attribute; +}; + +const AttributeMapping ATTRIBUTE_MAPPINGS[] = { + { LQD_CFG_KEY_NAME_ENABLE_JS, "javascript", "enabled" }, + { LQD_CFG_KEY_NAME_ALLOW_COOKIES, "cookies", "enabled" }, + { LQD_CFG_KEY_NAME_ALLOW_3RD_PARTY_COOKIES, "cookies", "third-party" }, + { LQD_CFG_KEY_NAME_WIN_X, "window", "x" }, + { LQD_CFG_KEY_NAME_WIN_Y, "window", "y" }, + { LQD_CFG_KEY_NAME_WIN_WIDTH, "window", "width" }, + { LQD_CFG_KEY_NAME_WIN_HEIGHT, "window", "height" }, + { LQD_CFG_KEY_NAME_WIN_MAXIMIZED, "window", "maximized" }, + { LQD_CFG_KEY_NAME_WIN_FULLSCREEN, "window", "fullscreen" }, + { LQD_CFG_KEY_NAME_LOCK_WIN_GEOM, "window", "locked" }, + { LQD_CFG_KEY_NAME_REMOVE_WINDOW_FRAME, "window", "frameless" }, + { LQD_CFG_KEY_NAME_ZOOM_LVL, "viewport", "zoom" }, + { LQD_CFG_KEY_NAME_HIDE_SCROLLBARS, "viewport", "hide-scrollbars" }, + { LQD_CFG_KEY_NAME_USE_CUSTOM_BG, "viewport", "custom-background" }, + { LQD_CFG_KEY_NAME_CUSTOM_BG_COLOR, "viewport", "background-color" }, + { LQD_CFG_KEY_NAME_MUTE_AUDIO, "audio", "muted" }, + { LQD_CFG_KEY_NAME_USE_PROXY, "proxy", "enabled" }, + { LQD_CFG_KEY_NAME_PROXY_USE_SOCKS, "proxy", "socks" }, + { LQD_CFG_KEY_NAME_PROXY_HOST, "proxy", "host" }, + { LQD_CFG_KEY_NAME_PROXY_PORT, "proxy", "port" }, + { LQD_CFG_KEY_NAME_PROXY_USE_AUTH, "proxy", "authenticate" }, + { LQD_CFG_KEY_NAME_PROXY_USER_NAME, "proxy", "username" }, + { LQD_CFG_KEY_NAME_PROXY_USER_PASSWORD, "proxy", "password" }, +}; + +// Multi-line code, stored verbatim (in CDATA sections) +struct CodeMapping { + const char* key; + const char* element; +}; + +const CodeMapping CODE_MAPPINGS[] = { + { LQD_CFG_KEY_NAME_ADDITIONAL_CSS, "style" }, + { LQD_CFG_KEY_NAME_ADDITIONAL_JS, "script" }, +}; + +// Strips characters that aren't allowed in XML documents +QString sanitize(const QString& text) +{ + QString result; + result.reserve(text.size()); + + for (int i = 0; i < text.size(); i++) { + const QChar character = text.at(i); + const uint code = character.unicode(); + + if (character.isHighSurrogate()) { + if (i + 1 < text.size() && text.at(i + 1).isLowSurrogate()) { + result += character; + result += text.at(++i); + } + } else if (character.isLowSurrogate() || code == 0xFFFE || code == 0xFFFF) { + continue; + } else if (code >= 0x20 || code == 0x9 || code == 0xA || code == 0xD) { + result += character; + } + } + + return result; +} + +// Removes the setting from the map, returning its value as text +bool take(QSettings::SettingsMap& map, const QString& key, QString* value) +{ + const QSettings::SettingsMap::iterator it = map.find(key); + + if (it == map.end()) { + return false; + } + + *value = sanitize(it.value().toString()); + map.erase(it); + + return true; +} + +} // namespace + +QSettings::Format LiquidAppFile::format(void) +{ + static const QSettings::Format registeredFormat = + QSettings::registerFormat(LQD_APP_FILE_EXTENSION, &LiquidAppFile::read, &LiquidAppFile::write, Qt::CaseSensitive); + + return registeredFormat; +} + +QSettings* LiquidAppFile::open(const QString& liquidAppName) +{ + return new QSettings(Liquid::getLiquidAppFilePath(liquidAppName), format()); +} + +bool LiquidAppFile::read(QIODevice& device, QSettings::SettingsMap& map) +{ + QXmlStreamReader xml(&device); + + if (!xml.readNextStartElement() || xml.name() != ROOT_ELEMENT) { + return false; + } + + // Anything this version of Liquid doesn't recognize (e.g. made by a newer one) gets skipped + while (xml.readNextStartElement()) { + const QString element = xml.name().toString(); + bool isHandled = false; + + for (const TextMapping& mapping : TEXT_MAPPINGS) { + if (element == mapping.element) { + map.insert(mapping.key, xml.readElementText()); + isHandled = true; + break; + } + } + + for (const CodeMapping& mapping : CODE_MAPPINGS) { + if (!isHandled && element == mapping.element) { + map.insert(mapping.key, xml.readElementText()); + isHandled = true; + } + } + + if (isHandled) { + continue; + } + + if (element == "domains") { + QStringList domains; + + while (xml.readNextStartElement()) { + if (xml.name() == QLatin1String("domain")) { + const QString domain = xml.readElementText().trimmed(); + + if (!domain.isEmpty()) { + domains.append(domain); + } + } else { + xml.skipCurrentElement(); + } + } + + if (!domains.isEmpty()) { + map.insert(LQD_CFG_KEY_NAME_ADDITIONAL_DOMAINS, domains.join(' ')); + } + } else if (element == "cookie-jar") { + while (xml.readNextStartElement()) { + const QString id = xml.attributes().value("id").toString(); + + if (xml.name() == QLatin1String("cookie") && !id.isEmpty()) { + map.insert(COOKIES_KEY_PREFIX + id, xml.readElementText()); + } else { + xml.skipCurrentElement(); + } + } + } else if (element == "settings") { + while (xml.readNextStartElement()) { + const QString key = xml.attributes().value("key").toString(); + + if (xml.name() == QLatin1String("setting") && !key.isEmpty()) { + map.insert(key, xml.readElementText()); + } else { + xml.skipCurrentElement(); + } + } + } else if (element == "icon") { + static const QRegularExpression whitespace(QStringLiteral("\\s")); + + map.insert(LQD_CFG_KEY_NAME_ICON, ICON_DATA_URL_PREFIX + xml.readElementText().remove(whitespace)); + } else { + const QXmlStreamAttributes attributes = xml.attributes(); + + for (const AttributeMapping& mapping : ATTRIBUTE_MAPPINGS) { + if (element == mapping.element && attributes.hasAttribute(mapping.attribute)) { + map.insert(mapping.key, attributes.value(mapping.attribute).toString()); + } + } + + xml.skipCurrentElement(); + } + } + + return !xml.hasError(); +} + +bool LiquidAppFile::write(QIODevice& device, const QSettings::SettingsMap& settingsMap) +{ + // Settings get removed from this copy as they're written, whatever remains goes into in the end + QSettings::SettingsMap map = settingsMap; + QString value; + + QXmlStreamWriter xml(&device); + xml.setAutoFormatting(true); + xml.setAutoFormattingIndent(4); + + xml.writeStartDocument(); + xml.writeStartElement(ROOT_ELEMENT); + xml.writeAttribute("version", QString::number(FORMAT_VERSION)); + + for (const TextMapping& mapping : TEXT_MAPPINGS) { + if (take(map, mapping.key, &value)) { + xml.writeTextElement(mapping.element, value); + } + } + + if (take(map, LQD_CFG_KEY_NAME_ADDITIONAL_DOMAINS, &value)) { + xml.writeStartElement("domains"); + + for (const QString& domain : value.split(' ', Qt::SkipEmptyParts)) { + xml.writeTextElement("domain", domain); + } + + xml.writeEndElement(); + } + + // Elements made of attributes + QStringList writtenElements; + for (const AttributeMapping& elementMapping : ATTRIBUTE_MAPPINGS) { + if (writtenElements.contains(elementMapping.element)) { + continue; + } + writtenElements.append(elementMapping.element); + + QXmlStreamAttributes attributes; + for (const AttributeMapping& mapping : ATTRIBUTE_MAPPINGS) { + if (QLatin1String(mapping.element) == QLatin1String(elementMapping.element) && take(map, mapping.key, &value)) { + attributes.append(mapping.attribute, value); + } + } + + if (!attributes.isEmpty()) { + xml.writeEmptyElement(elementMapping.element); + xml.writeAttributes(attributes); + } + } + + for (const CodeMapping& mapping : CODE_MAPPINGS) { + if (take(map, mapping.key, &value)) { + xml.writeStartElement(mapping.element); + xml.writeCDATA(value); // Sections containing "]]>" get split automatically + xml.writeEndElement(); + } + } + + // Cookies and the icon go last, since they're long and not meant to be read by humans + QStringList cookieKeys; + for (const QString& key : map.keys()) { + if (key.startsWith(COOKIES_KEY_PREFIX)) { + cookieKeys.append(key); + } + } + + QString icon; + const bool hasIcon = take(map, LQD_CFG_KEY_NAME_ICON, &icon) && icon.startsWith(ICON_DATA_URL_PREFIX); + if (!hasIcon && !icon.isEmpty()) { + map.insert(LQD_CFG_KEY_NAME_ICON, icon); // Not a PNG data URL, keep it as a generic setting + } + + // Anything else + bool hasOtherSettings = false; + for (QSettings::SettingsMap::const_iterator it = map.constBegin(); it != map.constEnd(); ++it) { + if (cookieKeys.contains(it.key())) { + continue; + } + + if (!hasOtherSettings) { + xml.writeStartElement("settings"); + hasOtherSettings = true; + } + + xml.writeStartElement("setting"); + xml.writeAttribute("key", sanitize(it.key())); + xml.writeCharacters(sanitize(it.value().toString())); + xml.writeEndElement(); + } + if (hasOtherSettings) { + xml.writeEndElement(); + } + + if (!cookieKeys.isEmpty()) { + xml.writeStartElement("cookie-jar"); + + for (const QString& key : cookieKeys) { + xml.writeStartElement("cookie"); + xml.writeAttribute("id", sanitize(key.mid(COOKIES_KEY_PREFIX.size()))); + xml.writeCharacters(sanitize(map.value(key).toString())); + xml.writeEndElement(); + } + + xml.writeEndElement(); + } + + if (hasIcon) { + xml.writeStartElement("icon"); + xml.writeAttribute("type", "image/png"); + xml.writeCharacters(icon.mid(ICON_DATA_URL_PREFIX.size())); + xml.writeEndElement(); + } + + xml.writeEndElement(); // liquid + xml.writeEndDocument(); + + return !xml.hasError(); +} diff --git a/src/liquidappwindow.cpp b/src/liquidappwindow.cpp index 2881449..18656da 100644 --- a/src/liquidappwindow.cpp +++ b/src/liquidappwindow.cpp @@ -22,6 +22,7 @@ #include #include "liquid.hpp" +#include "liquidappfile.hpp" #include "liquidappcookiejar.hpp" #include "liquidappwebpage.hpp" #include "liquidappwindow.hpp" @@ -105,12 +106,12 @@ LiquidAppWindow::LiquidAppWindow(const QString* name) : QWebEngineView() setMinimumSize(LQD_APP_WIN_MIN_SIZE_W, LQD_APP_WIN_MIN_SIZE_H); // Tune web engine + // NOTE: GPU rasterization and the GPU blocklist are deliberately left up to the web engine: + // forcing GPU rasterization on drivers known to mishandle it (e.g. nouveau) garbles text qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--enable-features=AutoplayIgnoreWebAudio"); qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--enable-accelerated-video-decode"); qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--enable-gpu-compositing"); - qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--enable-gpu-rasterization"); qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--enable-smooth-scrolling"); - qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--ignore-gpu-blocklist"); qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--num-raster-threads=4"); qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--use-fake-ui-for-media-stream"); @@ -133,22 +134,15 @@ LiquidAppWindow::LiquidAppWindow(const QString* name) : QWebEngineView() rememberGeometryTimer = new QTimer(this); rememberGeometryTimer->setSingleShot(true); rememberGeometryTimer->setInterval(200); - connect(rememberGeometryTimer, &QTimer::timeout, this, [this]() { - if (!isFullScreen()) { - liquidAppWindowGeometry = saveGeometry(); - } - }); + connect(rememberGeometryTimer, &QTimer::timeout, this, &LiquidAppWindow::rememberGeometry); // Disable default QWebEngineView's context menu setContextMenuPolicy(Qt::PreventContextMenu); - liquidAppName = (QString*)name; + // Keep a copy, the string pointed at may not outlive this window + liquidAppName = *name; - liquidAppConfig = new QSettings(QSettings::IniFormat, - QSettings::UserScope, - QString(PROG_NAME "%1" LQD_APPS_DIR_NAME).arg(QDir::separator()), - *name, - Q_NULLPTR); + liquidAppConfig = LiquidAppFile::open(*name); // These default settings affect everything (including sub-frames) #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) @@ -173,9 +167,9 @@ LiquidAppWindow::LiquidAppWindow(const QString* name) : QWebEngineView() liquidAppWebSettings = liquidAppWebPage->settings(); // Set default window title - liquidAppWindowTitle = *liquidAppName; + liquidAppWindowTitle = liquidAppName; - updateWindowTitle(*liquidAppName); + updateWindowTitle(liquidAppName); // Pre-fill all possible zoom factors to snap desired zoom level to { @@ -431,7 +425,7 @@ void LiquidAppWindow::bindKeyboardShortcuts(void) } QDir().mkpath(path); - page()->save(Liquid::getUniqueFilePath(path, *liquidAppName, "mhtml")); + page()->save(Liquid::getUniqueFilePath(path, liquidAppName, "mhtml")); }); // Make it possible to intercept zoom events @@ -551,7 +545,7 @@ void LiquidAppWindow::hardReload(void) // Ensure that while this Liquid App is being reset, the window title remains to be set to this Liquid App's name // to mimic the same experience that happens when the user initially runs this Liquid app if (!liquidAppWindowTitleIsReadOnly) { - liquidAppWindowTitle = *liquidAppName; + liquidAppWindowTitle = liquidAppName; const QString js = QString("(()=>{"\ "let e=document.createElement('title');"\ @@ -677,34 +671,51 @@ void LiquidAppWindow::loadLiquidAppConfig(void) // Restore window geometry { - bool geometryRestored = false; - - if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_WIN_GEOM)) { - geometryRestored = restoreGeometry(QByteArray::fromHex( - liquidAppConfig->value(LQD_CFG_KEY_NAME_WIN_GEOM).toByteArray() - )); - - // Discard saved geometry if it doesn't fit on screen (e.g. the screen setup has changed), - // windows that are way too large may not show up at all on some platforms (e.g. Wayland) - if (geometryRestored) { - const QScreen* screen = QGuiApplication::screenAt(geometry().center()); - if (screen == Q_NULLPTR) { - screen = QGuiApplication::primaryScreen(); - } - const QRect availableGeometry = screen->availableGeometry(); - - if (width() > availableGeometry.width() || height() > availableGeometry.height()) { - qDebug().noquote() << "Saved window geometry doesn't fit on screen, using default geometry instead"; - geometryRestored = false; - } + const QRect primaryScreenArea = QGuiApplication::primaryScreen()->availableGeometry(); + // Defaults to a quarter of the primary screen, centered + QRect windowGeometry(0, 0, primaryScreenArea.width() / 2, primaryScreenArea.height() / 2); + windowGeometry.moveCenter(primaryScreenArea.center()); + + if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_WIN_WIDTH) && liquidAppConfig->contains(LQD_CFG_KEY_NAME_WIN_HEIGHT)) { + const int savedWidth = liquidAppConfig->value(LQD_CFG_KEY_NAME_WIN_WIDTH).toInt(); + const int savedHeight = liquidAppConfig->value(LQD_CFG_KEY_NAME_WIN_HEIGHT).toInt(); + + if (savedWidth > 0 && savedHeight > 0) { + windowGeometry.setSize(QSize(savedWidth, savedHeight).expandedTo(minimumSize())); + windowGeometry.moveCenter(primaryScreenArea.center()); } } - if (!geometryRestored) { - const QRect currentScreenSize = QGuiApplication::primaryScreen()->availableGeometry(); - const int currentScreenWidth = currentScreenSize.width(); - const int currentScreenHeight = currentScreenSize.height(); - setGeometry(currentScreenWidth / 4, currentScreenHeight / 4, currentScreenWidth / 2, currentScreenHeight / 2); + if (!isPositionManagedByWindowSystem() + && liquidAppConfig->contains(LQD_CFG_KEY_NAME_WIN_X) && liquidAppConfig->contains(LQD_CFG_KEY_NAME_WIN_Y)) { + windowGeometry.moveTopLeft(QPoint(liquidAppConfig->value(LQD_CFG_KEY_NAME_WIN_X).toInt(), + liquidAppConfig->value(LQD_CFG_KEY_NAME_WIN_Y).toInt())); + } + + // Make sure the window ends up on a screen, and fits on it (the screen setup may have changed since, + // windows that are way too large may not show up at all on some platforms, e.g. Wayland) + QScreen* screen = QGuiApplication::screenAt(windowGeometry.center()); + if (screen == Q_NULLPTR) { + screen = QGuiApplication::primaryScreen(); + windowGeometry.moveCenter(screen->availableGeometry().center()); + } + + const QRect screenArea = screen->availableGeometry(); + if (windowGeometry.width() > screenArea.width() || windowGeometry.height() > screenArea.height()) { + qDebug().noquote() << "Saved window size doesn't fit on screen, shrinking it"; + windowGeometry.setSize(windowGeometry.size().boundedTo(screenArea.size())); + windowGeometry.moveCenter(screenArea.center()); + } + + setGeometry(windowGeometry); + rememberedGeometry = windowGeometry; + + if (liquidAppConfig->value(LQD_CFG_KEY_NAME_WIN_MAXIMIZED, false).toBool()) { + setWindowState(windowState() | Qt::WindowMaximized); + } + + if (liquidAppConfig->value(LQD_CFG_KEY_NAME_WIN_FULLSCREEN, false).toBool()) { + setWindowState(windowState() | Qt::WindowFullScreen); } } @@ -805,12 +816,26 @@ void LiquidAppWindow::loadStarted(void) void LiquidAppWindow::moveEvent(QMoveEvent *event) { - // Remember window position - liquidAppWindowGeometry = saveGeometry(); + // Remember window position once it settles (window state may not be up to date yet while getting maximized) + rememberGeometryTimer->start(); QWebEngineView::moveEvent(event); } +void LiquidAppWindow::rememberGeometry(void) +{ + // Only the "normal" geometry gets remembered, maximized and full-screen states are saved separately + if (!(windowState() & (Qt::WindowMaximized | Qt::WindowFullScreen | Qt::WindowMinimized))) { + rememberedGeometry = geometry(); + } +} + +bool LiquidAppWindow::isPositionManagedByWindowSystem(void) +{ + // On Wayland, programs can neither know nor choose where their windows are + return QGuiApplication::platformName().startsWith("wayland"); +} + void LiquidAppWindow::onIconChanged(QIcon icon) { // Set window icon @@ -821,9 +846,8 @@ void LiquidAppWindow::onIconChanged(QIcon icon) void LiquidAppWindow::resizeEvent(QResizeEvent* event) { - // Remember window size (unless in full-screen mode), - // once it stops changing (e.g. the user is done dragging, or resize animation is over) - if (event->spontaneous() && !isFullScreen()) { + // Remember window size once it stops changing (e.g. the user is done dragging, or resize animation is over) + if (event->spontaneous()) { rememberGeometryTimer->start(); } @@ -856,8 +880,29 @@ void LiquidAppWindow::saveLiquidAppConfig(void) liquidAppConfig->setValue(LQD_CFG_KEY_NAME_ICON, QString("data:image/png;base64,%1").arg(QString(buffer.data().toBase64()))); } - if (!isFullScreen()) { - liquidAppConfig->setValue(LQD_CFG_KEY_NAME_WIN_GEOM, QString(liquidAppWindowGeometry.toHex())); + // Window geometry + rememberGeometry(); + if (rememberedGeometry.isValid()) { + liquidAppConfig->setValue(LQD_CFG_KEY_NAME_WIN_WIDTH, rememberedGeometry.width()); + liquidAppConfig->setValue(LQD_CFG_KEY_NAME_WIN_HEIGHT, rememberedGeometry.height()); + + // Position remains untouched where it's unknown (it may still be of use on a different machine) + if (!isPositionManagedByWindowSystem()) { + liquidAppConfig->setValue(LQD_CFG_KEY_NAME_WIN_X, rememberedGeometry.x()); + liquidAppConfig->setValue(LQD_CFG_KEY_NAME_WIN_Y, rememberedGeometry.y()); + } + } + + if (isMaximized()) { + liquidAppConfig->setValue(LQD_CFG_KEY_NAME_WIN_MAXIMIZED, true); + } else { + liquidAppConfig->remove(LQD_CFG_KEY_NAME_WIN_MAXIMIZED); + } + + if (isFullScreen()) { + liquidAppConfig->setValue(LQD_CFG_KEY_NAME_WIN_FULLSCREEN, true); + } else { + liquidAppConfig->remove(LQD_CFG_KEY_NAME_WIN_FULLSCREEN); } if (windowGeometryIsLocked) { @@ -939,7 +984,7 @@ void LiquidAppWindow::takeSnapshot(const bool fullPage) // Compose snapshot file base name (a unique number gets appended to it upon saving) const QString baseName = QString("%1 %2") - .arg(*liquidAppName) + .arg(liquidAppName) .arg(tr((fullPage) ? "full-page snapshot" : "snapshot")); if (fullPage) { @@ -1098,7 +1143,7 @@ void LiquidAppWindow::takeVectorSnapshot(void) page()->runJavaScript("delete window.lqdHtml2Svg;", QWebEngineScript::ApplicationWorld); if (!svg.isEmpty()) { - const QString baseName = QString("%1 %2").arg(*liquidAppName).arg(tr("vector snapshot")); + const QString baseName = QString("%1 %2").arg(liquidAppName).arg(tr("vector snapshot")); QSaveFile file(Liquid::getUniqueFilePath(getSnapshotsDirPath(), baseName, "svg")); if (file.open(QIODevice::WriteOnly) && file.write(svg) == svg.size() && file.commit()) { @@ -1454,7 +1499,7 @@ void LiquidAppWindow::updateWindowTitle(const QString title) if (title.size() > 0) { liquidAppWindowTitle = title; } else { - liquidAppWindowTitle = *liquidAppName; + liquidAppWindowTitle = liquidAppName; } } diff --git a/src/main.cpp b/src/main.cpp index 792070e..e336058 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,6 +7,7 @@ #include "lqd.h" #include "liquid.hpp" +#include "liquidappfile.hpp" #include "liquidappconfigwindow.hpp" #include "liquidappwindow.hpp" #include "mainwindow.hpp" @@ -133,11 +134,7 @@ int main(int argc, char **argv) attempt_to_create_or_run_liquid_app: // Attempt to load Liquid app's config file - QSettings* tempAppSettings = new QSettings(QSettings::IniFormat, - QSettings::UserScope, - QString(PROG_NAME) + QDir::separator() + LQD_APPS_DIR_NAME, - liquidAppName, - Q_NULLPTR); + QSettings* tempAppSettings = LiquidAppFile::open(liquidAppName); // Attempt to load app settings from a config file if (!parser.isSet(editAppDialogFlag) && tempAppSettings->contains(LQD_CFG_KEY_NAME_URL)) { diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 4811675..1be94e5 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -6,6 +6,7 @@ #include #include "liquid.hpp" +#include "liquidappfile.hpp" #include "liquidappconfigwindow.hpp" #include "lqd.h" #include "mainwindow.hpp" @@ -22,7 +23,7 @@ MainWindow::MainWindow() : QScrollArea() setWindowIcon(QIcon(":/images/" PROG_NAME ".svg")); #endif - settings = new QSettings(PROG_NAME, PROG_NAME); + settings = new QSettings(Liquid::getDataDir().filePath(PROG_NAME ".ini"), QSettings::IniFormat); if (settings->contains(LQD_CFG_KEY_NAME_WIN_GEOM)) { QByteArray geometry = QByteArray::fromHex( settings->value(LQD_CFG_KEY_NAME_WIN_GEOM).toByteArray() @@ -124,11 +125,7 @@ void MainWindow::populateTable(void) appListTable->insertRow(i); - QSettings* liquidAppConfig = new QSettings(QSettings::IniFormat, - QSettings::UserScope, - QString(PROG_NAME "%1" LQD_APPS_DIR_NAME).arg(QDir::separator()), - liquidAppName, - Q_NULLPTR); + QSettings* liquidAppConfig = LiquidAppFile::open(liquidAppName); ////////////////// // First column // diff --git a/tests/_data_/README.md b/tests/_data_/README.md index b2b3f29..ddc76bb 100644 --- a/tests/_data_/README.md +++ b/tests/_data_/README.md @@ -6,6 +6,6 @@ made for performing various acceptance tests. ## How to use: -1. copy `apps/test.ini` into `~/.config/liquid/apps/` +1. copy `apps/additional-js.liquid` into `~/.liquid/apps/` 2. run `make server` -3. run `../bin/liquid test` +3. run `../bin/liquid additional-js` diff --git a/tests/_data_/apps/additional-js.ini b/tests/_data_/apps/additional-js.ini deleted file mode 100644 index db45912..0000000 --- a/tests/_data_/apps/additional-js.ini +++ /dev/null @@ -1,14 +0,0 @@ -; Modifying this file may result in test page displaying incorrect information - -[General] -BackgroundColor=#fff -CookiesAllowed=true -AdditionalJS=document.getElementById('custom-js').innerText = 'Check' -WindowGeometryLocked=true -WindowGeometry=01d9d0cb00020000000004e0000000000000072e00000388000004e40000001d0000072a0000038400000000000000000780 -JavaScriptEnabled=true -ThirdPartyCookiesAllowed=false -Title=Liquid Test -URL=http://0.0.0.0:5151 -CustomUserAgent=Liquid Browser -Zoom=0.6999999523162842 diff --git a/tests/_data_/apps/additional-js.liquid b/tests/_data_/apps/additional-js.liquid new file mode 100644 index 0000000..7294025 --- /dev/null +++ b/tests/_data_/apps/additional-js.liquid @@ -0,0 +1,11 @@ + + + http://0.0.0.0:5151 + Liquid Test + Liquid Browser + + + + + +