├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── dist └── liquid.desktop ├── docs └── arch │ └── ADRs.md ├── inc ├── SetWindowBackgroundColor.h ├── liquid.hpp ├── liquidappconfigwindow.hpp ├── liquidappcookiejar.hpp ├── liquidappwebpage.hpp ├── liquidappwindow.hpp ├── lqd.h └── mainwindow.hpp ├── liquid.pro ├── res ├── images │ ├── checkers.svg │ └── liquid.svg ├── resources.qrc ├── scripts │ └── html2svg.js └── styles │ ├── base.qss │ ├── dark.qss │ └── light.qss ├── sample-apps ├── README.md ├── fccid.ini ├── github.ini ├── hackernews.ini └── http-auth-demo.ini ├── src ├── SetWindowBackgroundColor.mm ├── liquid.cpp ├── liquidappconfigwindow.cpp ├── liquidappcookiejar.cpp ├── liquidappwebpage.cpp ├── liquidappwindow.cpp ├── main.cpp └── mainwindow.cpp └── tests └── _data_ ├── README.md ├── apps └── additional-js.ini └── index.html /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | paths-ignore: 7 | - 'dist/' 8 | - 'docs/' 9 | - 'sample-apps/' 10 | - 'LICENSE' 11 | - 'README.md' 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ${{ matrix.os }} 17 | 18 | strategy: 19 | matrix: 20 | include: 21 | - os: ubuntu-18.04 22 | - os: ubuntu-20.04 23 | - os: macos-10.15 24 | - os: macos-11 25 | - os: windows-2019 26 | - os: windows-2022 27 | 28 | continue-on-error: true 29 | 30 | steps: 31 | - uses: actions/checkout@v2 32 | - uses: ilammy/msvc-dev-cmd@v1 33 | 34 | - name: Resolve dependencies (Ubuntu) 35 | run: | 36 | sudo apt-get update && \ 37 | sudo apt-get install qt5-default qtwebengine5-dev 38 | if: contains(matrix.os, 'ubuntu') 39 | 40 | - name: Resolve dependencies (macOS and Windows) 41 | uses: jurplel/install-qt-action@v2 42 | with: 43 | modules: 'qtwebengine' 44 | if: contains(matrix.os, 'macos') || contains(matrix.os, 'windows') 45 | 46 | - name: Build (Ubuntu and macOS) 47 | run: | 48 | qmake 49 | make -j 50 | if: contains(matrix.os, 'ubuntu') || contains(matrix.os, 'macos') 51 | 52 | - name: Build (Windows) 53 | run: | 54 | qmake 55 | nmake 56 | if: contains(matrix.os, 'windows') 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .directory 3 | .DS_Store 4 | 5 | # Chromium 6 | VideoDecodeStats 7 | 8 | # C++ objects and libs 9 | *.slo 10 | *.lo 11 | *.o 12 | *.a 13 | *.la 14 | *.lai 15 | *.so 16 | *.so.* 17 | *.dll 18 | *.dylib 19 | 20 | # Qt-es 21 | object_script.*.Release 22 | object_script.*.Debug 23 | *_plugin_import.cpp 24 | /.qmake.cache 25 | /.qmake.stash 26 | *.pro.user 27 | *.pro.user.* 28 | *.qbs.user 29 | *.qbs.user.* 30 | *.moc 31 | moc_*.cpp 32 | moc_*.h 33 | qrc_*.cpp 34 | ui_*.h 35 | *.qmlc 36 | *.jsc 37 | Makefile* 38 | *build-* 39 | *.qm 40 | *.prl 41 | 42 | # Qt unit tests 43 | target_wrapper.* 44 | 45 | # QtCreator 46 | *.autosave 47 | 48 | # QtCreator Qml 49 | *.qmlproject.user 50 | *.qmlproject.user.* 51 | 52 | # QtCreator CMake 53 | CMakeLists.txt.user* 54 | 55 | # QtCreator 4.8< compilation database 56 | compile_commands.json 57 | 58 | # QtCreator local machine specific files for imported projects 59 | *creator.user* 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :ocean: Liquid 2 | 3 | Liquid is a tool that turns web pages into desktop applications. 4 | 5 | 6 | ## How it works 7 | 8 | Liquid is capable of making websites appear and behave more like native OS applications: isolated, customizable, and running as separate system processes. 9 | 10 | You’ll be able to: 11 | - encapsulate websites the way virtual machines and Docker do it for operating systems 12 | - safely and securely utilize different browsing identities 13 | - remain safe from being tracked by third-party websites 14 | - make sure you stay within specific domain range 15 | 16 | What gets stored (if allowed): cookies. 17 | 18 | 19 | ## Comparison table 20 | 21 | | Feature | :ocean: Liquid | :earth_africa: Browsers | Notes | 22 | |:-------------------------------------------------|:--------------:|:-------------------------------------:|:----------------------------------| 23 | | Custom User-Agent string | ✅ | ✅ | Some browsers require a plug-in | 24 | | Window transparency | ✅ | ❌ | See-through websites | 25 | | Full-page snapshots | ✅ | ❌ | Possible with plug-ins | 26 | | Transparent snapshots | ✅ | ❌ | See-through snapshots of websites | 27 | | Vector snapshots | ❌ | ❌ | Experimental feature, SVG | 28 | | Ability to save pages as monolithic HTML files | ❌ | ❌ | Possible with plug-ins | 29 | | Complete absence of pop-up windows | ✅ | ❌ | Can be optionally disabled in most browsers | 30 | | Ability to completely disable JS | ✅ | ✅ | | 31 | | Ability to disable all cookies | ✅ | ✅ | | 32 | | Ability to disable third-party cookies | ✅ | ✅ | | 33 | | Ability to inject custom JS code into web pages | ✅ | ❌ | Possible with plug-ins | 34 | | Support for HTTP basic authentication mechanism | ✅ | ✅ | | 35 | | Ability to inject custom CSS code into web pages | ✅ | ❌ | Possible with plug-ins | 36 | | Limit websites to stay within specific domain(s) | ✅ | ❌ | | 37 | | Simultaneous usage of multiple user accounts | ✅ | ❌ | Can be achieved using profiles and extensions in some browsers | 38 | | Per-website proxy settings | ✅ | ❌ | Possible with plug-ins | 39 | | Ability to hide scroll bars | ✅ | ❌ | | 40 | | Window geometry lock | ✅ | ❌ | Possible with plug-ins | 41 | | Ability to remove window frame | ✅ | ❌ | | 42 | | Minimalistic tabless design | ✅ | ❌ | | 43 | | Fine zoom | ✅ | ❌ | | 44 | | Search within the page | ❌ | ✅ | | 45 | | Permanently mute website | ✅ | ❌ | Browsers automatically unmute, Liquid remembers the state | 46 | | Ability to go full-screen | ✅ | ✅ | | 47 | | Full control over full-screen capabilities | ✅ | ❌ | Liquid acts more like a mobile device simulator when it comes to full-screen | 48 | | Mandatory off-the-record capabilities | ✅ | ❌ | | 49 | 50 | 51 | ## Keyboard shortcuts 52 | 53 | | Action | Primary | Alternative | 54 | |:------------------------------------------|:------------------:|:---------------------------------:| 55 | | Zoom in | `Ctrl`+`=` | `Ctrl`+_mouse wheel up_ | 56 | | Zoom out | `Ctrl`+`-` | `Ctrl`+_mouse wheel down_ | 57 | | Fine zoom in | `Ctrl`+`Shift`+`=` | `Ctrl`+`Shift`+_mouse wheel up_ | 58 | | Fine zoom out | `Ctrl`+`Shift`+`-` | `Ctrl`+`Shift`+_mouse wheel down_ | 59 | | Reset zoom level | `Ctrl`+`0` | `Ctrl`+`Shift`+`0` | 60 | | Toggle full-screen mode | `Ctrl`+`Shift`+`F` | `F11` | 61 | | Stop loading / exit from full-screen mode | `Esc` | | 62 | | Take snapshot | `Ctrl`+`T` | | 63 | | Take full-page snapshot | `Ctrl`+`Shift`+`T` | | 64 | | Toggle window size lock | `Ctrl`+`L` | | 65 | | Toggle mute | `Ctrl`+`M` | | 66 | | Refresh current page | `Ctrl`+`R` | | 67 | | Reload app | `Ctrl`+`Shift`+`R` | | 68 | | Close app | `Ctrl`+`Q` | `Ctrl`+`W` | 69 | | Go back | `Ctrl`+`←` | `Backspace` | 70 | | Go forward | `Ctrl`+`→` | | 71 | | Open link using default web browser | `Ctrl`+_click_ | | 72 | 73 | 74 | ## Working with the codebase 75 | 76 | #### Build 77 | 78 | ```console 79 | qmake 80 | make -j 81 | ``` 82 | 83 | #### Install 84 | 85 | ```console 86 | sudo make install 87 | ``` 88 | 89 | #### Uninstall 90 | 91 | ```console 92 | sudo make uninstall 93 | ``` 94 | 95 | 96 | ## Customize 97 | 98 | Placing a file named `liquid.qss` into `~/.config/liquid/` will serve as additional stylesheet for the program. 99 | You can use [base.qss](res/styles/base.qss) as reference. 100 | -------------------------------------------------------------------------------- /dist/liquid.desktop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env xdg-open 2 | [Desktop Entry] 3 | Type=Application 4 | Encoding=UTF-8 5 | Name=Liquid 6 | Comment=Convert web pages into desktop applications 7 | Exec=liquid 8 | Terminal=false 9 | Icon=liquid 10 | Categories=Qt;Network;WebBrowser; 11 | -------------------------------------------------------------------------------- /docs/arch/ADRs.md: -------------------------------------------------------------------------------- 1 | # Architectural Decision Records 2 | 3 | This document contains overview of Liquid's software design represented as sections of nested lists. 4 | 5 | 6 | ## General 7 | 8 | - provide user with functionality maximally close to what is offered by popular web browsers 9 | - [x] uploading files 10 | - [ ] downloading files 11 | - [ ] camera support 12 | - [ ] microphone support 13 | 14 | ## Security and Privacy 15 | 16 | - default to maximum security and privacy settings, enable features on on-demand basis 17 | - [x] allow user to specify starting URL and additional domains to navigate within 18 | - [ ] add support for using wildcard (\*) pattern matching for domain names 19 | - [ ] allow user to completely cut off network requests to certain domains 20 | - [ ] allow user to use domain blacklist as domain whitelist 21 | - [x] do not let the user navigate to resources outside of specified domains 22 | - [x] make it possible to use modifier key to open system browser in order to navigate to external resources 23 | - [x] throw an error in case the web browser engine is not in OTR mode 24 | - [ ] always send Do Not Track HTTP header along with every network request 25 | - full control over cookies 26 | - [ ] option to store cookies inside the application config file 27 | - [x] option to reject all cookies 28 | - [x] option to specifically reject all third-party cookies 29 | - [ ] ability to disable localStorage 30 | 31 | - make the program provide both CLI and GUI i/o methods 32 | - [x] use lower-case flag and option names for CLI, upper-case for GUI 33 | - [x] make it possible to list all existing apps in CLI mode 34 | - [x] make it possible to initiate creation of new apps via CLI 35 | - [x] make it possible to run apps from CLI 36 | - [ ] make it possible to delete existing app via CLI 37 | 38 | ## User Interface 39 | 40 | - make the UI as user-friendly and easy to use as possible 41 | - switch fields automatically using basic logic and common sense, rather than disabling and forbidding the user from modifying them 42 | - maximal control over the UI 43 | - abilitiy to change page zoom level 44 | - [x] make the app zoom level extremely fine 45 | - [x] make it possible to increase in wider steps when holding additional modifier key 46 | - [x] make the app remember its zoom level 47 | - [x] ability to mute the app 48 | - [x] the app remembers its mute status 49 | - [x] prevent applications from going full-screen, let web pages go full-window instead 50 | - [x] completely disallow opening of popup windows 51 | - [x] ability to permanently "freeze" window size 52 | - [x] ability to hide scroll bars 53 | - [x] ability to remove window border 54 | - [ ] ability to hide window shadow 55 | - [x] allow application windows to be transparent, display see-through websites 56 | - [x] provide user with maximal ability of saving the contents of the app's window 57 | - [x] make it possible to take snapshots of the current view and of the full page 58 | - [x] allow snapshots to be semi-opaque 59 | - [ ] make it possible to take vector snapshots 60 | - [ ] make it possible to save pages as one single HTML file 61 | - [ ] make it possible to print the current page out 62 | -------------------------------------------------------------------------------- /inc/SetWindowBackgroundColor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void SetWindowBackgroundColor(long winId, float red, float green, float blue, float alpha); 4 | -------------------------------------------------------------------------------- /inc/liquid.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | class Liquid 8 | { 9 | public: 10 | static void applyQtStyleSheets(QWidget* widget); 11 | static void createDesktopFile(const QString liquidAppName, const QString liquidAppStartingUrl); 12 | static bool detectDarkMode(void); 13 | static QByteArray generateRandomByteArray(const int byteLength); 14 | static QDir getAppsDir(void); 15 | static QDir getConfigDir(void); 16 | static QString getDefaultUserAgentString(void); 17 | static QStringList getLiquidAppsList(void); 18 | static QString getReadableDateTimeString(void); 19 | static void removeDesktopFile(const QString liquidAppName); 20 | static void runLiquidApp(const QString liquidAppName); 21 | static void sleep(const int ms); 22 | }; 23 | -------------------------------------------------------------------------------- /inc/liquidappconfigwindow.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | class LiquidAppConfigDialog : public QDialog 16 | { 17 | public: 18 | LiquidAppConfigDialog(QWidget* parent = Q_NULLPTR, QString liquidAppName = ""); 19 | ~LiquidAppConfigDialog(void); 20 | 21 | QString getName(void); 22 | bool isPlanningToRun(void); 23 | void setPlanningToRun(const bool state); 24 | 25 | public slots: 26 | void save(); 27 | 28 | private: 29 | void bindShortcuts(void); 30 | static QFrame* separator(void); 31 | 32 | bool isEditingExistingBool = false; 33 | bool isPlanningToRunBool = false; 34 | 35 | QAction* quitAction; 36 | 37 | QColor* backgroundColor; 38 | 39 | QLineEdit* nameInput; 40 | QLineEdit* addressInput; 41 | QCheckBox* createIconCheckBox; 42 | QCheckBox* planningToRunCheckBox = Q_NULLPTR; 43 | 44 | // General tab 45 | QLineEdit* titleInput; 46 | QListView* additionalDomainsListView; 47 | QStandardItemModel* additionalDomainsModel; 48 | QLineEdit* userAgentInput; 49 | QPlainTextEdit* notesTextArea; 50 | 51 | // Appearance tab 52 | QCheckBox* hideScrollBarsCheckBox; 53 | QCheckBox* removeWindowFrameCheckBox; 54 | QCheckBox* useCustomBackgroundCheckBox; 55 | QPushButton* customBackgroundColorButton; 56 | QPlainTextEdit* additionalCssTextArea; 57 | 58 | // JavaScript tab 59 | QCheckBox* enableJavaScriptCheckBox; 60 | QLabel* additionalJsLabel; 61 | QPlainTextEdit* additionalJsTextArea; 62 | 63 | // Cookies tab 64 | QCheckBox* allowCookiesCheckBox; 65 | QCheckBox* allowThirdPartyCookiesCheckBox; 66 | QTableView* cookiesTableView; 67 | QStandardItemModel* cookiesModel; 68 | 69 | // Network tab 70 | QRadioButton* proxyModeSystemRadioButton; 71 | QRadioButton* proxyModeDirectRadioButton; 72 | QRadioButton* proxyModeCustomRadioButton; 73 | QComboBox* useSocksSelectBox; 74 | QLineEdit* proxyHostInput; 75 | QSpinBox* proxyPortInput; 76 | QCheckBox* proxyUseAuthCheckBox; 77 | QLineEdit* proxyUsernameInput; 78 | QLineEdit* proxyPasswordInput; 79 | }; 80 | -------------------------------------------------------------------------------- /inc/liquidappcookiejar.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "liquidappwindow.hpp" 9 | 10 | class LiquidAppCookieJar : public QNetworkCookieJar 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | LiquidAppCookieJar(QObject* parent = Q_NULLPTR); 16 | ~LiquidAppCookieJar(void); 17 | 18 | bool removeCookie(const QNetworkCookie& cookie); 19 | bool upsertCookie(const QNetworkCookie& cookie); 20 | 21 | void restoreCookies(QWebEngineCookieStore* cookieStore); 22 | 23 | private: 24 | QSettings* liquidAppConfig; 25 | LiquidAppWindow* liquidAppWindow; 26 | 27 | void save(void); 28 | }; 29 | -------------------------------------------------------------------------------- /inc/liquidappwebpage.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "liquidappwindow.hpp" 9 | 10 | class LiquidAppWindow; 11 | 12 | class LiquidAppWebPage : public QWebEnginePage 13 | { 14 | public: 15 | LiquidAppWebPage(QWebEngineProfile* profile, LiquidAppWindow* parent = Q_NULLPTR); 16 | 17 | void addAllowedDomain(const QString domain); 18 | void addAllowedDomains(const QStringList domainList); 19 | void closeJsDialog(); 20 | 21 | static void setWebSettingsToDefault(QWebEngineSettings* webSettings); 22 | 23 | signals: 24 | void authenticationRequired(const QUrl& requestUrl, QAuthenticator* authenticator); 25 | 26 | protected: 27 | bool acceptNavigationRequest(const QUrl& reqUrl, const QWebEnginePage::NavigationType navReqType, const bool isMainFrame) override; 28 | 29 | private: 30 | bool certificateError(const QWebEngineCertificateError& error); 31 | void javaScriptAlert(const QUrl& securityOrigin, const QString& msg) override; 32 | bool javaScriptConfirm(const QUrl& securityOrigin, const QString& msg) override; 33 | bool javaScriptPrompt(const QUrl& securityOrigin, const QString& msg, const QString& defaultValue, QString* result) override; 34 | 35 | LiquidAppWindow* liquidAppWindow = Q_NULLPTR; 36 | QStringList* allowedDomainsList = new QStringList(); 37 | 38 | QDialog* dialogWidget = Q_NULLPTR; 39 | }; 40 | -------------------------------------------------------------------------------- /inc/liquidappwindow.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "liquidappwebpage.hpp" 14 | 15 | class LiquidAppWebPage; 16 | 17 | class LiquidAppWindow : public QWebEngineView 18 | { 19 | Q_OBJECT 20 | 21 | public: 22 | explicit LiquidAppWindow(const QString* name); 23 | ~LiquidAppWindow(void); 24 | 25 | void setForgiveNextPageLoadError(const bool ok); 26 | 27 | QSettings* liquidAppConfig; 28 | 29 | public slots: 30 | void certificateError(void); 31 | void exitFullScreenMode(void); 32 | void hardReload(void); 33 | void loadFinished(bool ok); 34 | void loadStarted(void); 35 | void onIconChanged(QIcon icon); 36 | void stopLoadingOrExitFullScreenMode(void); 37 | void takeSnapshotSlot(void); 38 | void takeSnapshotFullPageSlot(void); 39 | void toggleFullScreenMode(void); 40 | void toggleWindowGeometryLock(void); 41 | void updateWindowTitle(const QString title); 42 | void zoomIn(const bool fine); 43 | void zoomOut(const bool fine); 44 | void zoomReset(void); 45 | 46 | protected: 47 | void attemptToSetZoomFactorTo(const qreal desiredZoomFactor); 48 | void closeEvent(QCloseEvent* event) override; 49 | bool eventFilter(QObject* watched, QEvent* event) override; 50 | bool handleWheelEvent(QWheelEvent* event); 51 | void moveEvent(QMoveEvent* event) override; 52 | void resizeEvent(QResizeEvent* event) override; 53 | void contextMenuEvent(QContextMenuEvent* event) override; 54 | 55 | private: 56 | void takeSnapshot(const bool fullPage); 57 | const QString colorToRgba(const QColor color); 58 | 59 | QString* liquidAppName; 60 | 61 | QString liquidAppWindowTitle; 62 | QIcon iconToSave; 63 | 64 | LiquidAppWebPage* liquidAppWebPage = Q_NULLPTR; 65 | QWebEngineProfile* liquidAppWebProfile = Q_NULLPTR; 66 | QWebEngineSettings* liquidAppWebSettings = Q_NULLPTR; 67 | QByteArray liquidAppWindowGeometry; 68 | QList zoomFactors; 69 | 70 | bool liquidAppWindowTitleIsReadOnly = false; 71 | bool forgiveNextPageLoadError = false; 72 | bool pageHasCertificateError = false; 73 | bool pageHasError = false; 74 | bool pageIsLoading = false; 75 | bool windowGeometryIsLocked = false; 76 | 77 | QNetworkProxy* proxy = Q_NULLPTR; 78 | 79 | // Keyboard shortcuts' actions 80 | QAction* backAction; 81 | QAction* backAction2; 82 | QAction* forwardAction; 83 | QAction* hardReloadAction; 84 | QAction* muteAudioAction; 85 | QAction* quitAction; 86 | QAction* quitAction2; 87 | QAction* reloadAction; 88 | QAction* reloadAction2; 89 | QAction* savePageAction; 90 | QAction* stopLoadingOrExitFullScreenModeAction; 91 | QAction* takeSnapshotAction; 92 | QAction* takeSnapshotFullPageAction; 93 | QAction* toggleFullScreenModeAction; 94 | QAction* toggleFullScreenModeAction2; 95 | QAction* toggleGeometryLockAction; 96 | QAction* zoomInAction; 97 | QAction* zoomOutAction; 98 | QAction* zoomInFineAction; 99 | QAction* zoomOutFineAction; 100 | QAction* zoomResetAction; 101 | QAction* zoomResetAltAction; 102 | 103 | // Context menu and its actions 104 | QMenu* contextMenu; 105 | QAction* contextMenuCopyUrlAction; 106 | QAction* contextMenuReloadAction; 107 | QAction* contextMenuBackAction; 108 | QAction* contextMenuForwardAction; 109 | QAction* contextMenuCloseAction; 110 | 111 | void bindKeyboardShortcuts(void); 112 | void loadLiquidAppConfig(void); 113 | void saveLiquidAppConfig(void); 114 | void setupContextMenu(void); 115 | }; 116 | -------------------------------------------------------------------------------- /inc/lqd.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* Various globals */ 4 | #define LQD_PROG_TITLE "Liquid" 5 | #define LQD_APPS_DIR_NAME "apps" 6 | #define LQD_DEFAULT_BG_COLOR Qt::white 7 | #define LQD_DEFAULT_PROXY_HOST "0.0.0.0" 8 | #define LQD_DEFAULT_PROXY_PORT 8080 9 | #define LQD_WIN_MIN_SIZE_W 200 10 | #define LQD_WIN_MIN_SIZE_H 400 11 | #define LQD_APP_WIN_MIN_SIZE_W 160 12 | #define LQD_APP_WIN_MIN_SIZE_H 120 13 | #define LQD_UI_MARGIN 24 14 | #define LQD_ZOOM_LVL_MIN 0.25 15 | #define LQD_ZOOM_LVL_MAX 5.0 // Limited to 5.0 by Chromium 16 | #define LQD_ZOOM_LVL_STEP 0.04 17 | #define LQD_ZOOM_LVL_STEP_FINE (LQD_ZOOM_LVL_STEP / 10) 18 | 19 | /* Textual icons */ 20 | #define LQD_ICON_ADD "➕" 21 | #define LQD_ICON_EDIT "⚙" 22 | #define LQD_ICON_ERROR "❌" 23 | #define LQD_ICON_LOADING "⏳" 24 | #define LQD_ICON_LOCKED "🖼" 25 | #define LQD_ICON_MUTED "🔇" 26 | #define LQD_ICON_WARNING "⚠️" 27 | #define LQD_ICON_DELETE "✖" 28 | #define LQD_ICON_RUN "➤" 29 | 30 | /* Liquid App config file group names */ 31 | #define LQD_CFG_GROUP_NAME_COOKIES "Cookies" 32 | #define LQD_CFG_GROUP_NAME_PROXY "Proxy" 33 | 34 | /* 35 | * Liquid App config key names. 36 | * Names must be given in a way that would let them answer to questions: 37 | * - "do what?" (for booleans) 38 | * - "what is it?" (everything else) 39 | * e.g.: ShowScrollBars, Icon, ZoomLevel, etc. 40 | */ 41 | #define LQD_CFG_KEY_NAME_ADDITIONAL_CSS "AdditionalCSS" // text 42 | #define LQD_CFG_KEY_NAME_ADDITIONAL_DOMAINS "AdditionalDomains" // text, whitespace-separated items 43 | #define LQD_CFG_KEY_NAME_ADDITIONAL_JS "AdditionalJS" // text 44 | #define LQD_CFG_KEY_NAME_ALLOW_COOKIES "AllowCookies" // boolean, defults to FALSE 45 | #define LQD_CFG_KEY_NAME_ALLOW_3RD_PARTY_COOKIES "AllowThirdPartyCookies" // boolean, defaults to FALSE 46 | #define LQD_CFG_KEY_NAME_CUSTOM_BG_COLOR "CustomBackgroundColor" // text 47 | #define LQD_CFG_KEY_NAME_ENABLE_JS "EnableJS" // boolean, defaults to FALSE 48 | #define LQD_CFG_KEY_NAME_HIDE_SCROLLBARS "HideScrollBars" // boolean, defaults to FALSE 49 | #define LQD_CFG_KEY_NAME_ICON "Icon" // text 50 | #define LQD_CFG_KEY_NAME_LOCK_WIN_GEOM "LockWindowGeometry" // boolean, defaults to FALSE 51 | #define LQD_CFG_KEY_NAME_MUTE_AUDIO "MuteAudio" // boolean, defaults to FALSE 52 | #define LQD_CFG_KEY_NAME_NOTES "Notes" // text 53 | #define LQD_CFG_KEY_NAME_PROXY_HOST LQD_CFG_GROUP_NAME_PROXY "/" "Host" // text 54 | #define LQD_CFG_KEY_NAME_PROXY_PORT LQD_CFG_GROUP_NAME_PROXY "/" "Port" // number 55 | #define LQD_CFG_KEY_NAME_PROXY_USE_AUTH LQD_CFG_GROUP_NAME_PROXY "/" "UseAuthentication" // boolean, defaults to FALSE 56 | #define LQD_CFG_KEY_NAME_PROXY_USE_SOCKS LQD_CFG_GROUP_NAME_PROXY "/" "UseSocks" // boolean, defaults to FALSE 57 | #define LQD_CFG_KEY_NAME_PROXY_USER_NAME LQD_CFG_GROUP_NAME_PROXY "/" "UserName" // text 58 | #define LQD_CFG_KEY_NAME_PROXY_USER_PASSWORD LQD_CFG_GROUP_NAME_PROXY "/" "UserPassword" // text 59 | #define LQD_CFG_KEY_NAME_REMOVE_WINDOW_FRAME "RemoveWindowFrame" // boolean, defaults to FALSE 60 | #define LQD_CFG_KEY_NAME_TITLE "Title" // text 61 | #define LQD_CFG_KEY_NAME_USE_PROXY "UseProxy" // boolean, defaults to FALSE 62 | #define LQD_CFG_KEY_NAME_USE_CUSTOM_BG "UseCustomBackground" // boolean, defaults to FALSE 63 | #define LQD_CFG_KEY_NAME_USER_AGENT "UserAgent" // text 64 | #define LQD_CFG_KEY_NAME_URL "URL" // text, required 65 | #define LQD_CFG_KEY_NAME_WIN_GEOM "WindowGeometry" // text 66 | #define LQD_CFG_KEY_NAME_ZOOM_LVL "ZoomLevel" // number, defaults to 1 67 | 68 | /* Keyboard shortcuts (all windows and dialog boxes) */ 69 | #define LQD_KBD_SEQ_MUTE_AUDIO "Ctrl+M" 70 | #define LQD_KBD_SEQ_GO_BACK "Ctrl+Left" 71 | #define LQD_KBD_SEQ_GO_BACK_2 "Backspace" 72 | #define LQD_KBD_SEQ_GO_FORWARD "Ctrl+Right" 73 | #define LQD_KBD_SEQ_RELOAD "Ctrl+R" 74 | #define LQD_KBD_SEQ_RELOAD_2 "F5" 75 | #define LQD_KBD_SEQ_HARD_RELOAD "Ctrl+Shift+R" 76 | #define LQD_KBD_SEQ_TOGGLE_FS_MODE "Ctrl+Shift+F" 77 | #define LQD_KBD_SEQ_TOGGLE_FS_MODE_2 "F11" 78 | #define LQD_KBD_SEQ_STOP_OR_EXIT_FS_MODE "Escape" 79 | #define LQD_KBD_SEQ_TOGGLE_WIN_GEOM_LOCK "Ctrl+L" 80 | #define LQD_KBD_SEQ_TAKE_SNAPSHOT "Ctrl+T" 81 | #define LQD_KBD_SEQ_TAKE_SNAPSHOT_FULL "Ctrl+Shift+T" 82 | #define LQD_KBD_SEQ_QUIT "Ctrl+Q" 83 | #define LQD_KBD_SEQ_QUIT_2 "Ctrl+W" 84 | #define LQD_KBD_SEQ_SAVE_PAGE "Ctrl+S" 85 | #define LQD_KBD_SEQ_SAVE_PAGE_AS "Ctrl+Shift+S" // TODO 86 | #define LQD_KBD_SEQ_ZOOM_LVL_INC "Ctrl+=" 87 | #define LQD_KBD_SEQ_ZOOM_LVL_INC_FINE "Ctrl+Shift+=" 88 | #define LQD_KBD_SEQ_ZOOM_LVL_DEC "Ctrl+-" 89 | #define LQD_KBD_SEQ_ZOOM_LVL_DEC_FINE "Ctrl+Shift+-" 90 | #define LQD_KBD_SEQ_ZOOM_LVL_RESET "Ctrl+0" 91 | #define LQD_KBD_SEQ_ZOOM_LVL_RESET_2 "Ctrl+Shift+0" 92 | -------------------------------------------------------------------------------- /inc/mainwindow.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | class MainWindow : public QScrollArea 10 | { 11 | public: 12 | MainWindow(); 13 | ~MainWindow(); 14 | 15 | protected: 16 | void closeEvent(QCloseEvent *event) override; 17 | 18 | private: 19 | void bindShortcuts(void); 20 | void flushTable(void); 21 | void populateTable(void); 22 | void runLiquidApp(const QString liquidAppName); 23 | void saveSettings(void); 24 | 25 | QTableWidget* appListTable; 26 | QPushButton* createNewLiquidAppButton; 27 | QSettings* settings; 28 | 29 | QAction* quitAction; 30 | }; 31 | -------------------------------------------------------------------------------- /liquid.pro: -------------------------------------------------------------------------------- 1 | VERSION_MAJOR = 0 2 | VERSION_MINOR = 7 3 | VERSION_PATCH = 11 4 | 5 | VERSION = $${VERSION_MAJOR}.$${VERSION_MINOR}.$${VERSION_PATCH} 6 | 7 | QT += core gui webenginewidgets 8 | CONFIG += c++11 9 | TEMPLATE = app 10 | 11 | DESTDIR = build 12 | PROG_NAME = liquid 13 | 14 | SRC_DIR = src 15 | INC_DIR = inc 16 | FORMS_DIR = ui 17 | 18 | OBJECTS_DIR = .objs 19 | MOC_DIR = .mocs 20 | UI_DIR = .uis 21 | RCC_DIR = .qrcs 22 | 23 | INCLUDEPATH += $${INC_DIR} 24 | 25 | HEADERS += inc/lqd.h \ 26 | inc/liquid.hpp \ 27 | inc/liquidappcookiejar.hpp \ 28 | inc/liquidappconfigwindow.hpp \ 29 | inc/liquidappwebpage.hpp \ 30 | inc/liquidappwindow.hpp \ 31 | inc/mainwindow.hpp \ 32 | 33 | SOURCES += src/liquid.cpp \ 34 | src/liquidappcookiejar.cpp \ 35 | src/liquidappconfigwindow.cpp \ 36 | src/liquidappwebpage.cpp \ 37 | src/liquidappwindow.cpp \ 38 | src/main.cpp \ 39 | src/mainwindow.cpp \ 40 | 41 | RESOURCES = res/resources.qrc 42 | 43 | OTHER_FILES += res/images/$${PROG_NAME}.svg \ 44 | res/styles/base.qss \ 45 | res/styles/dark.qss \ 46 | res/styles/light.qss 47 | 48 | QMAKE_CLEAN += -r $${DESTDIR}/$${PROG_NAME} 49 | 50 | CONFIG += debug 51 | 52 | DEFINES += PROG_NAME=\\\"$${PROG_NAME}\\\" 53 | 54 | DEFINES += "VERSION_MAJOR=$$VERSION_MAJOR" \ 55 | "VERSION_MINOR=$$VERSION_MINOR" \ 56 | "VERSION_PATCH=$$VERSION_PATCH" \ 57 | 58 | DEFINES += VERSION=\\\"$${VERSION}\\\" 59 | 60 | # GNU/Linux, FreeBSD, and similar 61 | unix:!mac { 62 | isEmpty(PREFIX) { 63 | PREFIX = /usr 64 | } 65 | BINDIR = $${PREFIX}/bin 66 | DATADIR =$${PREFIX}/share 67 | 68 | OTHER_FILES += dist/$${PROG_NAME}.desktop 69 | 70 | target.path = $${BINDIR} 71 | 72 | INSTALLS += target 73 | 74 | desktop.path = $${DATADIR}/applications 75 | eval(desktop.files += dist/$${PROG_NAME}.desktop) 76 | 77 | INSTALLS += desktop 78 | 79 | icon.path = $${DATADIR}/icons/hicolor/scalable/apps 80 | eval(icon.files += res/images/$${PROG_NAME}.svg) 81 | 82 | INSTALLS += icon 83 | } 84 | 85 | # macOS 86 | macx: { 87 | HEADERS += inc/SetWindowBackgroundColor.h 88 | OBJECTIVE_SOURCES += src/SetWindowBackgroundColor.mm 89 | 90 | LIBS += -framework Cocoa 91 | 92 | QMAKE_CLEAN += -r $${DESTDIR}/$${TARGET}.app \ 93 | $${DESTDIR}/$${TARGET}.icns \ 94 | $${DESTDIR}/$${TARGET}.iconset 95 | 96 | !exists($${DESTDIR}/$${TARGET}.icns) { 97 | QMAKE_PRE_LINK += mkdir -p $${DESTDIR}/$${TARGET}.iconset && 98 | QMAKE_PRE_LINK += convert -density 5.64705882353 -background none -resize 16x16 $${PWD}/res/images/$${PROG_NAME}.svg $${DESTDIR}/$${TARGET}.iconset/icon_16x16.png && 99 | QMAKE_PRE_LINK += convert -density 11.2941176471 -background none -resize 32x32 $${PWD}/res/images/$${PROG_NAME}.svg $${DESTDIR}/$${TARGET}.iconset/icon_16x16@2x.png && 100 | QMAKE_PRE_LINK += cp $${DESTDIR}/$${TARGET}.iconset/icon_16x16@2x.png $${DESTDIR}/$${TARGET}.iconset/icon_32x32.png && 101 | QMAKE_PRE_LINK += convert -density 22.5882352941 -background none -resize 64x64 $${PWD}/res/images/$${PROG_NAME}.svg $${DESTDIR}/$${TARGET}.iconset/icon_32x32@2x.png && 102 | QMAKE_PRE_LINK += cp $${DESTDIR}/$${TARGET}.iconset/icon_32x32@2x.png $${DESTDIR}/$${TARGET}.iconset/icon_64x64.png && 103 | QMAKE_PRE_LINK += convert -density 45.1764705882 -background none -resize 128x128 $${PWD}/res/images/$${PROG_NAME}.svg $${DESTDIR}/$${TARGET}.iconset/icon_64x64@2x.png && 104 | QMAKE_PRE_LINK += cp $${DESTDIR}/$${TARGET}.iconset/icon_64x64@2x.png $${DESTDIR}/$${TARGET}.iconset/icon_128x128.png && 105 | QMAKE_PRE_LINK += convert -density 90.3529411765 -background none -resize 256x256 $${PWD}/res/images/$${PROG_NAME}.svg $${DESTDIR}/$${TARGET}.iconset/icon_128x128@2x.png && 106 | QMAKE_PRE_LINK += cp $${DESTDIR}/$${TARGET}.iconset/icon_128x128@2x.png $${DESTDIR}/$${TARGET}.iconset/icon_256x256.png && 107 | QMAKE_PRE_LINK += iconutil -c icns $${DESTDIR}/$${TARGET}.iconset 108 | } 109 | 110 | QMAKE_POST_LINK += cp $${DESTDIR}/$${TARGET}.icns $${DESTDIR}/$${TARGET}.app/Contents/Resources/ 111 | } 112 | -------------------------------------------------------------------------------- /res/images/checkers.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 28 | 35 | 38 | 45 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /res/images/liquid.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 18 | 22 | 26 | 27 | 36 | 43 | 46 | 47 | 48 | 51 | 55 | 63 | 71 | 79 | 80 | 85 | 89 | 93 | 97 | 98 | 100 | 101 | 103 | 104 | image/svg+xml 105 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /res/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | images/checkers.svg 5 | images/liquid.svg 6 | scripts/html2svg.js 7 | styles/base.qss 8 | styles/dark.qss 9 | styles/light.qss 10 | 11 | 12 | -------------------------------------------------------------------------------- /res/scripts/html2svg.js: -------------------------------------------------------------------------------- 1 | // html2svg.js, the engine behind Liquid's vector screenshot functionality 2 | 3 | const snapshot = { 4 | width: %1, 5 | height: %2, 6 | backgroundColor: "%3", 7 | fullPage: %4, 8 | }; 9 | 10 | // const irrelevantNodeNames = [ 11 | // "STYLE", 12 | // "SCRIPT", 13 | // ]; 14 | 15 | const svgDoc = document.implementation.createDocument("http://www.w3.org/2000/svg", "svg", null); 16 | 17 | const svgNode = svgDoc.rootElement; 18 | svgNode.setAttribute("version", "1.1"); 19 | svgNode.setAttribute("width", snapshot.width); 20 | svgNode.setAttribute("height", snapshot.height); 21 | 22 | { 23 | const svgRectNode = document.createElementNS(null, "rect"); 24 | svgRectNode.setAttribute("width", snapshot.width); 25 | svgRectNode.setAttribute("height", snapshot.height); 26 | svgRectNode.setAttribute("fill", snapshot.backgroundColor); 27 | svgNode.appendChild(svgRectNode); 28 | } 29 | 30 | function htmlElementPosition(htmlElement) { 31 | var rect = htmlElement.getBoundingClientRect(), 32 | scrollLeft = (snapshot.fullPage) ? 0 : (window.scrollX || document.documentElement.scrollLeft), 33 | scrollTop = (snapshot.fullPage) ? 0 : (window.scrollY || document.documentElement.scrollTop); 34 | 35 | return { 36 | left: rect.left + scrollLeft, 37 | top: rect.top + scrollTop, 38 | width: rect.width, 39 | height: rect.height, 40 | }; 41 | } 42 | 43 | function processElement(htmlElement) { 44 | const style = window.getComputedStyle(htmlElement); 45 | 46 | if (style.visibility != "hidden" && style.display != "none" && style.opacity > 0) { 47 | const parameters = htmlElementPosition(htmlElement); 48 | 49 | if (parameters.width > 0 && parameters.height > 0) { 50 | const svgRectNode = document.createElementNS(null, "rect"); 51 | svgRectNode.setAttribute("x", parameters.left); 52 | svgRectNode.setAttribute("y", parameters.top); 53 | svgRectNode.setAttribute("width", parameters.width); 54 | svgRectNode.setAttribute("height", parameters.height) ; 55 | if (style.backgroundColor != "rgba(0, 0, 0, 0)") { 56 | svgRectNode.setAttribute("fill", style.backgroundColor); 57 | } else { 58 | svgRectNode.setAttribute("fill", "none"); 59 | } 60 | svgNode.appendChild(svgRectNode); 61 | } 62 | } 63 | } 64 | 65 | function processTextNode(htmlTextNode) { 66 | const text = htmlTextNode.nodeValue; 67 | 68 | if (text.trim().length > 0) { 69 | const htmlParentElement = htmlTextNode.parentElement; 70 | const style = window.getComputedStyle(htmlParentElement); 71 | const parameters = htmlElementPosition(htmlParentElement); 72 | 73 | const svgTextNode = document.createElementNS(null, "text"); 74 | svgTextNode.setAttribute("x", parameters.left); 75 | svgTextNode.setAttribute("y", parameters.top); 76 | svgTextNode.setAttribute("color", style.color); 77 | 78 | var textNode = document.createTextNode(text); 79 | svgTextNode.appendChild(textNode); 80 | 81 | svgNode.appendChild(svgTextNode); 82 | } 83 | } 84 | 85 | function walkDom(htmlNode) { 86 | if (htmlNode.nodeType == Node.ELEMENT_NODE) { 87 | processElement(htmlNode); 88 | 89 | for (let childNode of htmlNode.childNodes) { 90 | walkDom(childNode); 91 | } 92 | } else if (htmlNode.nodeType == Node.TEXT_NODE) { 93 | processTextNode(htmlNode); 94 | } 95 | } 96 | 97 | ////////////////////////////////////////////////////////////////////////////// 98 | 99 | /* 100 | const elementsOfInterest = []; 101 | 102 | for (let x = (snapshot.fullPage) ? 0 : window.scrollX; x <= %1; x++) { 103 | for (let y = (snapshot.fullPage) ? 0 : window.scrollY; y <= %1; y++) { 104 | const elements = document.elementsFromPoint(x, y); 105 | if (elements.length > 0) { 106 | for (let z = 0; z < elements.length; z++) { 107 | if (elementsOfInterest.indexOf(elements[z]) == -1) { 108 | elementsOfInterest.push(elements[z]); 109 | } 110 | } 111 | } 112 | } 113 | } 114 | 115 | for (let i = 0; i < elementsOfInterest.length; i++) { 116 | processElement(elementsOfInterest[0]); 117 | } 118 | */ 119 | 120 | processElement(document.documentElement); 121 | walkDom(document.body); 122 | 123 | ////////////////////////////////////////////////////////////////////////////// 124 | 125 | return '' + new XMLSerializer().serializeToString(svgDoc.documentElement); 126 | -------------------------------------------------------------------------------- /res/styles/base.qss: -------------------------------------------------------------------------------- 1 | QScrollArea { 2 | border: 0; 3 | } 4 | 5 | QTableWidget QPushButton { 6 | background-color: transparent; 7 | border-radius: 0; 8 | height: 100%; 9 | } 10 | 11 | .monospace { 12 | font-family: 'SF Mono', SFMono-Regular, ui-monospace, 'DejaVu Sans Mono', Menlo, Consolas, monospace; 13 | } 14 | -------------------------------------------------------------------------------- /res/styles/dark.qss: -------------------------------------------------------------------------------- 1 | QWidget { 2 | background: #111; 3 | color: white; 4 | } 5 | 6 | QTableWidget { 7 | border: 1px solid #181818; 8 | } 9 | 10 | QTableWidget QPushButton { 11 | color: #fff; 12 | } 13 | QTableWidget QPushButton.liquidAppsListButtonDelete:hover { 14 | color: rgba(255, 0, 0, 0.8); 15 | } 16 | QTableWidget QPushButton.liquidAppsListButtonEdit:hover { 17 | color: rgba(0, 255, 0, 0.8); 18 | } 19 | QTableWidget QPushButton.liquidAppsListButtonRun:hover { 20 | color: rgba(0, 125, 255, 0.8); 21 | } 22 | 23 | QTableWidget::item { 24 | background-color: qlineargradient(spread:pad, x1:1, y1:1, x2:1, y2:0, stop:0 rgba(0, 0, 0, 255), stop:1 rgba(20, 20, 20, 255)); 25 | border-bottom: 1px solid #181818; 26 | color: #fff; 27 | } 28 | 29 | QLineEdit[readOnly="true"], 30 | QPlainTextEdit[readOnly="true"] { 31 | color: rgba(255, 255, 255, 0.7); 32 | } 33 | /* Empty (placeholder) */ 34 | QLineEdit[text=""], 35 | QPlainTextEdit[plainText=""] { 36 | color: rgba(255, 255, 255, 0.4); 37 | } 38 | 39 | QScrollBar:vertical { 40 | background: #383838; 41 | border: none; 42 | width: 11px; 43 | margin: 0; 44 | } 45 | QScrollBar::handle:vertical { 46 | background: rgba(0, 0, 0, 0.67); 47 | min-height: 0; 48 | border-radius: 4px; 49 | margin: 1px; 50 | } 51 | QScrollBar::add-line:vertical { 52 | background: transparent; 53 | height: 0; 54 | subcontrol-position: bottom; 55 | subcontrol-origin: margin; 56 | } 57 | QScrollBar::sub-line:vertical { 58 | background: transparent; 59 | height: 0; 60 | subcontrol-position: top; 61 | subcontrol-origin: margin; 62 | } 63 | -------------------------------------------------------------------------------- /res/styles/light.qss: -------------------------------------------------------------------------------- 1 | QTableWidget { 2 | border: 1px solid #ccc; 3 | } 4 | 5 | QTableWidget::item { 6 | background-color: qlineargradient(spread:pad, x1:1, y1:1, x2:1, y2:0, stop:0 rgba(245, 245, 245, 255), stop:1 rgba(255, 255, 255, 255)); 7 | border-bottom: 1px solid #ccc; 8 | color: #000; 9 | } 10 | 11 | QLineEdit[readOnly="true"], 12 | QPlainTextEdit[readOnly="true"] { 13 | color: rgba(0, 0, 0, 0.7); 14 | } 15 | /* Empty (placeholder) */ 16 | QLineEdit[text=""], 17 | QPlainTextEdit[plainText=""] { 18 | color: rgba(0, 0, 0, 0.4); 19 | } 20 | 21 | QTableWidget QPushButton { 22 | color: #000; 23 | } 24 | QTableWidget QPushButton.liquidAppsListButtonDelete:hover { 25 | background-color: rgba(255, 0, 0, 0.1); 26 | } 27 | QTableWidget QPushButton.liquidAppsListButtonEdit:hover { 28 | background-color: rgba(0, 255, 0, 0.1); 29 | } 30 | QTableWidget QPushButton.liquidAppsListButtonRun:hover { 31 | background-color: rgba(0, 0, 255, 0.1); 32 | } 33 | -------------------------------------------------------------------------------- /sample-apps/README.md: -------------------------------------------------------------------------------- 1 | # Sample Liquid apps 2 | 3 | Copy files from directory `apps` into `~/.config/liquid/apps/` 4 | and then load them by running `liquid `, e.g.: 5 | 6 | mkdir -p ~/.config/liquid/apps/ 7 | cp -n apps/* ~/.config/liquid/apps/ 8 | liquid hackernews 9 | -------------------------------------------------------------------------------- /sample-apps/fccid.ini: -------------------------------------------------------------------------------- 1 | [General] 2 | AllowCookies=false 3 | AllowThirdPartyCookies=false 4 | EnableJS=false 5 | LockWindowGeometry=true 6 | Title=FCCID 7 | URL=https://fccid.io/ 8 | WindowGeometry=01d9d0cb00030000000005c50000005a000009c100000710000005c5000000a4000009c10000071000000000000000000f00000005c5000000a4000009c100000710 9 | ZoomLevel=1.0000000238418578 10 | -------------------------------------------------------------------------------- /sample-apps/github.ini: -------------------------------------------------------------------------------- 1 | [General] 2 | AllowCookies=true 3 | AllowThirdPartyCookies=false 4 | BackgroundColor=#24292e 5 | EnableJS=true 6 | Title=GitHub 7 | URL=https://github.com 8 | WindowGeometry=01d9d0cb000200000000017900000206000005aa0000048b0000017d00000223000005a60000048700000000000000000780 9 | ZoomLevel=0.9 10 | -------------------------------------------------------------------------------- /sample-apps/hackernews.ini: -------------------------------------------------------------------------------- 1 | [General] 2 | AdditionalDomains=hn.algolia.com www.ycombinator.com 3 | AllowCookies=true 4 | AllowThirdPartyCookies=false 5 | BackgroundColor=#f6f6ef 6 | CookiesAllowed=true 7 | EnableJS=true 8 | HideScrollBars=true 9 | Icon=000000220051005000690078006d0061007000490063006f006e0045006e00670069006e0065000000010000000189504e470d0a1a0a0000000d49484452000001000000010008060000005c72a866000000097048597300000ec400000ec401952b0e1b00000c9249444154789ceddd797094f51dc7f1cf86209701f1e2926a295539542ce2550a08111052abad7574dadae9d47a4debc1a82897a1da2a6a6b3dc68ead54c769c75694706a02088a1c55393c10040b05410e112160082124db3f9ed6b1a340b2bfefee3ebbdff7eb4f073fbb4392f73e64f7799e44f23a3da73afd500592920290ef1292ea2535d1a444f25a25f9c1071c4a4805d97e0e00b2a780577fc0a9244700806b0400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c23008063040070ac30db4f20a7346b25b56863b75753255557daede5aa264da5a2e36cb6aa2ba3bf57340801688ca3bf268d7b572a6862b3b76d8d54da5daaafb3d9cb55436e97be774ff84e7d9d34be87b47575f89613fc13a031b6ac92163f6db7d7ee64a9cf95767bb9a87991543cc2666be1447ef81b890034d6f4f152ed3ebbbde163a484e32fc3a09ba5564787efd4ee93661a1c4538e3f83b2f453b374af31eb3db6b778ad4e70abbbd5cd2bc482abec5666bdea3d1d7068d420052517e9fed2fef86393d0a281e21b56c1bbeb36fb7543e217cc72187df7506aa764815f7dbed75e8269d75b9dd5e2e68d1263afcb7503e21fa9aa0d10840aae63e2cedde6ab7e7ed28a07884d4f2a8f09ddddba2af0552e2e83bce584d55f40b412b1d7b48bd2fb3db8bb3966da44137d96ccd28e57dff000420c4c289d2f6b5767bc3c7fa380ab8f0569b0f547db24e5a30317cc73107df6d6954572b4d196db7d7b1a774e6f7edf6e2a8655b69e08d365b53c6445f03a48c00845a3a49dab8dc6e2fdf8f0206df2a356f1dbeb3e91d69c93fc2779ccbe3efb40c49d64b93efb0db3be174a9d725767b71d2aaad74c1af6cb6268f8cfeee1184005858394b5a3dcf6e2f5f8f0206df1e7df827d407af49ef9587ef800098b13c0ae8dc4b3ae362bbbd3868758c74c12f6db6ca0cffae9d230056d6bf212d7bc16eaf649cdd561c0cb94d6a7664f8cedb53a5b58bc277208900d89a3adaeed4dece67e6cf51c091c748030c5efd93f5d16ffe61860058daba5a5af494dddef03c390a183232ba984aa8c5cf489b5784efe07304c0da8c52bbd3854fec2d9d5e62b3952d45c749fd6f08df39b05f9a61f8c94b482200f6767e64fbd9f45c3f0ab07af57ff57169c7faf01dfc1f02900e1513ec4e173ea98fd47398cd56a6b56e27f5bf3e7ca7e633e9a5df86efe04b08403a54ed8cae19602557df111832523aa265f8ceac07a53ddbc377f02504205de63e2c556eb1d9fafa39528fa1365b99d2babdd4ffbaf09dcf3e91e6fc3e7c075f8900a4cbfe6adb5f5ae5da51c0d03ba4a62dc27766de2deddb13be83af4400d269c193d2c71fd86c75394fea3ed8662bdddab497fa5d1bbef3e987d2fc27c277705004209deaeba4a9861f5c29b9cb6e2b9d2e1a25356d1ebe3375ac74a0267c07074500d26de9f3d287cb6cb6be71bed4add8662b5d8eea28f5fd45f8ce9695d2eb7f0ddfc1211180744bd647a7ae5a89fb5180d5ab7fd99d9cee9b0104201356cd91de9f6bb3d5b5af74ea409b2d6b6d3b497daf0edf59f74fe9ed69e13b382c02902996a7b0c6f528e0a2d15261b3f01d4ef7cd18029029ebdf94963d6fb3f5cd7ed2c9036cb6acb4ed2c7dfbe7e13b2b5e94d6bc1abe830621009934658c547fc066ebbb313b0a18365a2a3c226c239994ca0c2fb28ac3220099b46db5b4f02f365b270f888e04e2e0e813a5f37f16bef3e6b3d2a6b7c277d0600420d366fc5aaaadb6d98acbef02868d0a7ff5afab95a6e5d8a71df30001c8b45d1f49731fb1d93a7560f4ae40361d7392cdabff6b7fb2bdc90a1a84006443f90469ef2e9bad6c1f050c1b2d35691ab6b17faf34f31e9be783462100d9b0d7f074e16ec5d12704b3e1d82ed2793f0ddf99f390ed8d56d16004205be63e22edda6cb395ada3008b57ffaa4fa5590fd83c1f341a01c8965ac3d385bb0f96ba9c63b3d550c77691cebb2a7ca7fc5ebbab27a1d10840362d9c286d5b63b355526ab3d350c3c74a0585611bbb3e92e63d6af37c901202904df575767717ee31543ae96c9badc339beab74ee8fc377a6dd25d572ba6f3611806c5b3e59dab0c4662b539f0eb478f5dfb65a5afcb4c9d341ea0840b625eba3535f2df41c16dd4b209d8eef2a9dfda3f09d2986775142ca08401cac9a23ad9a6db395eedf0594dc25153409dbd8b0c4f63e8a481901888bb23ba39361429d5e92bea38076a7487dae0cdfb13ae241300210171b964a4b27d96ca5eb6e4225e3c25ffd57cd8e8e78100b04204ea68db5395df88c8ba3bb0b5b6a7f8ad4e78af01d5efd638500c4c9b635d28289365b25636d763edf2b951281df2e4b2745473a880d021037338d4e173ee312e9845ee13b92d4a1bb74d6e5611bf575d165be112b04206e766d965e36b8bb702261771450322efcd57fe1c4e8bd7fc40a0188a38afba2330643f5ba54ea745ad846c7ee52efc057ffda7dd18550103b04208ef6564a2fdd1bbe934884df53b0a434da0931f7e1e873ff881d021057f31eb3f9a139f30752c79ea9fdbf9d4e93be7559d8e357574a1513c23690360420ae6aaba5e9a5e13b894474cdbe54948c0b7ff5af98205519fc7306694100e26cd15336bf38eb7d79f419fec6e8d8333a7a08b17babf4f21fc23690560420ceac4e172e68220d6de4dd76868f0d7ff59f3e5eda6f740564a4050188bb652f4477150a75ce4fa4a33a35eccf76e826f50efcb7fff6b5d15b7f883502900b2cee95577884547c4bc3feec45a3c2dff79f3a26bad63f628d00e482f7e74a2b6785ef7ce71aa9659b43ff99e3bb869ff1b7f12d69c973611bc80802902b2c4e176e5e24f5bbfed07f66e81de167fc95dd195de804b1470072c587cba4a506afaa036f940a0f7229efb69da57303aff4bbe655e9bdf2b00d640c01c825530dee2edca68374f6412ee839e4b6f0ebfc978d0cfbff91510420977cfc2f69c193e13b178ef8f27f2b3a4eea7b75d8ee5b65d2bad7c33690510420d7cc181fdd4b2f44c79ed1cd44be68d02d52d316a96f26eba52963c29e17328e00e49a4aa34fd77df12dc1e645d2801bc2f6163f236d5919b6818c2300b9a8e2fee89e7a21ba0f892ef32549fd6f905a1ce6edc14339506373de02328e00e4a2eacae89e7a21120969e04d526133a9f8e6b0ad571e973edd10b681ac2000b96adea3d2ce4d611be75e250dba496add3ef58d7d7ba4977e13f63c9035042057d51a1c76376b255d1a782431fb41e9b31d611bc81a0290cb163f2d6d7d3f6c23e433ff7bb64bb37f17f6f8c82a0290cb2cef2e9c8a17ef916aaab2f7f808460072ddf2c9d2bfb3f0e19b1deba5f97fccfce3c21401c807930d4e176eac69e3a4039cee9beb08403e58f38af45e45e61e6ff30ae9f5bf65eef1903604205f58dd5db8418f358ad37df30401c8171b974b4bfe9efec759bb487a677afa1f07194100f2c9d4b1e9bf0c1777f7cd2b04209f6c5f2bbdf6e7f4edbf3b53fa607efaf691710420dfccbc3b3defcd27eba52929de6004b14500f24dba6ec6f1c6b3d2a677ec77915504201fcd7a40aa32fc7c7e5d6df4be3ff20e01c847d5467717fe9ff94f489facb3db436c10807cf5ca63d2ce8de13b3555d167fe91970840beaaad9156cf0bdf79f92169f7b6f01dc41201c0a1ade01afff98c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c712c96b94ccf69300901d1c01008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e01801001c230080630400708c00008e1100c031020038460000c70800e0588112d97e0a00b222c11100e05a81129aa4a4c49100e04442faefcffca4ff002a6d8284ddf2bc970000000049454e44ae426082ffffffff00000100000001000000000000000001 10 | URL=https://news.ycombinator.com/ 11 | WindowGeometry=01d9d0cb00030000000002f50000001b000004c5000003ef000002f500000040000004c5000003ef00000000000000000780000002f500000040000004c5000003ef 12 | LockWindowGeometry=false 13 | ZoomLevel=0.9 14 | -------------------------------------------------------------------------------- /sample-apps/http-auth-demo.ini: -------------------------------------------------------------------------------- 1 | [General] 2 | AllowCookies=false 3 | AllowThirdPartyCookies=false 4 | EnableJS=false 5 | URL=https://authenticationtest.com/HTTPAuth/ 6 | -------------------------------------------------------------------------------- /src/SetWindowBackgroundColor.mm: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | void SetWindowBackgroundColor(long winId, float red, float green, float blue, float alpha) 4 | { 5 | NSView* view = (NSView*)winId; 6 | NSWindow* window = [view window]; 7 | 8 | // window.titlebarAppearsTransparent = NO; 9 | // window.hasShadow = NO; 10 | // [window setAlphaValue:1.0]; 11 | [window setOpaque:NO]; 12 | [window setBackgroundColor:[NSColor colorWithCalibratedRed:red green:green blue:blue alpha:alpha]]; 13 | } 14 | -------------------------------------------------------------------------------- /src/liquid.cpp: -------------------------------------------------------------------------------- 1 | #include "lqd.h" 2 | #include "liquid.hpp" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #if defined(Q_OS_MAC) 14 | #include 15 | #include 16 | #endif 17 | 18 | void Liquid::applyQtStyleSheets(QWidget* widget) 19 | { 20 | QString styleSheet; 21 | 22 | // Load built-in base stylesheet 23 | { 24 | QFile styleSheetFile(":/styles/base.qss"); 25 | styleSheetFile.open(QFile::ReadOnly); 26 | styleSheet = QLatin1String(styleSheetFile.readAll()); 27 | styleSheetFile.close(); 28 | } 29 | // Load built-in color theme stylesheet 30 | { 31 | QFile colorThemeStyleSheetFile(QString(":/styles/%1.qss").arg((Liquid::detectDarkMode()) ? "dark" : "light")); 32 | colorThemeStyleSheetFile.open(QFile::ReadOnly); 33 | styleSheet += QLatin1String(colorThemeStyleSheetFile.readAll()); 34 | colorThemeStyleSheetFile.close(); 35 | } 36 | 37 | // Load user-defined stylesheet 38 | { 39 | QFile customStyleSheetFile(Liquid::getConfigDir().absolutePath() + QDir::separator() + PROG_NAME ".qss"); 40 | if (customStyleSheetFile.open(QFile::ReadOnly)) { 41 | styleSheet += QLatin1String(customStyleSheetFile.readAll()); 42 | customStyleSheetFile.close(); 43 | } 44 | } 45 | 46 | widget->setStyleSheet(styleSheet); 47 | } 48 | 49 | void Liquid::createDesktopFile(const QString liquidAppName, const QString liquidAppStartingUrl) 50 | { 51 | #if defined(Q_OS_LINUX) 52 | // Compose content 53 | QString context = "#!/usr/bin/env xdg-open\n\n"; 54 | context += "[Desktop Entry]\n"; 55 | context += "Type=Application\n"; 56 | context += "Name=" + liquidAppName + "\n"; 57 | context += "Icon=internet-web-browser\n"; 58 | context += "Exec=liquid " + liquidAppName + "\n"; 59 | context += "Comment=" + liquidAppStartingUrl + "\n"; 60 | context += "Categories=Network;WebBrowser;\n"; 61 | 62 | // Construct directory path 63 | // QString desktopPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); 64 | QString desktopPath = QDir::homePath() + QDir::separator() + "Desktop"; 65 | 66 | // Check if the desktop path exists 67 | QDir dir(desktopPath); 68 | if (dir.exists()) { 69 | // Create file 70 | QFile file(desktopPath + QDir::separator() + liquidAppName + ".desktop"); 71 | file.open(QIODevice::WriteOnly); 72 | file.write(context.toStdString().c_str()); 73 | file.setPermissions(QFileDevice::ReadUser 74 | |QFileDevice::WriteUser 75 | |QFileDevice::ExeUser 76 | |QFileDevice::ReadGroup 77 | |QFileDevice::ReadOther); 78 | file.flush(); 79 | file.close(); 80 | } 81 | #else 82 | Q_UNUSED(liquidAppName); 83 | Q_UNUSED(liquidAppStartingUrl); 84 | #endif 85 | } 86 | 87 | bool Liquid::detectDarkMode(void) 88 | { 89 | #if defined(Q_OS_LINUX) 90 | QProcess process; 91 | process.start("gsettings", QStringList() << "get" << "org.gnome.desktop.interface" << "gtk-theme"); 92 | process.waitForFinished(1000); 93 | QString output = process.readAllStandardOutput(); 94 | if (output.size() > 0) { 95 | return output.endsWith("-dark'\n", Qt::CaseInsensitive); 96 | } 97 | #elif defined(Q_OS_MACOS) 98 | static const QString macOsVer = QSysInfo::productVersion(); 99 | static const QStringList macOsVerParts = macOsVer.split('.'); 100 | if (macOsVerParts.at(0).toInt() > 10 || (macOsVerParts.at(0).toInt() >= 10 && macOsVerParts.at(1).toInt() >= 14)) { 101 | bool macOsUserInterfaceIsUsingDarkMode = false; 102 | CFStringRef darkStr = CFSTR("Dark"); 103 | CFStringRef macOsUserInterfaceStyleStr = CFSTR("AppleInterfaceStyle"); 104 | CFStringRef macOsUserInterfaceStyle = (CFStringRef)CFPreferencesCopyAppValue(macOsUserInterfaceStyleStr, kCFPreferencesCurrentApplication); 105 | if (macOsUserInterfaceStyle != Q_NULLPTR) { 106 | macOsUserInterfaceIsUsingDarkMode = (CFStringCompare(macOsUserInterfaceStyle, darkStr, 0) == kCFCompareEqualTo); 107 | CFRelease(macOsUserInterfaceStyle); 108 | return macOsUserInterfaceIsUsingDarkMode; 109 | } 110 | } 111 | #elif defined(Q_OS_WIN) 112 | return (QSettings("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", QSettings::NativeFormat) 113 | .value("AppsUseLightTheme", 1) == 0); 114 | #endif 115 | 116 | return QApplication::palette().text().color().lightnessF() > QApplication::palette().window().color().lightnessF(); 117 | } 118 | 119 | QByteArray Liquid::generateRandomByteArray(const int byteLength) 120 | { 121 | std::vector buf; 122 | 123 | srand(QTime::currentTime().msec()); 124 | 125 | for (int i = 0; i < byteLength; ++i) { 126 | buf.push_back(rand()); 127 | } 128 | 129 | return QByteArray(reinterpret_cast(buf.data()), byteLength); 130 | } 131 | 132 | QDir Liquid::getAppsDir(void) 133 | { 134 | return QDir(getConfigDir().absolutePath() + QDir::separator() + LQD_APPS_DIR_NAME + QDir::separator()); 135 | } 136 | 137 | QDir Liquid::getConfigDir(void) 138 | { 139 | const QSettings* settings = new QSettings(QSettings::IniFormat, 140 | QSettings::UserScope, 141 | PROG_NAME, 142 | PROG_NAME, 143 | Q_NULLPTR); 144 | 145 | QFileInfo settingsFileInfo(settings->fileName()); 146 | 147 | return QDir(settingsFileInfo.absolutePath() + QDir::separator()); 148 | } 149 | 150 | QString Liquid::getDefaultUserAgentString(void) 151 | { 152 | return QWebEngineProfile().httpUserAgent(); 153 | } 154 | 155 | QStringList Liquid::getLiquidAppsList(void) 156 | { 157 | const QFileInfoList liquidAppsFileList = getAppsDir().entryInfoList(QStringList() << "*.ini", 158 | QDir::Files | QDir::NoDotAndDotDot, 159 | QDir::Name | QDir::IgnoreCase); 160 | QStringList liquidAppsNames; 161 | foreach (QFileInfo liquidAppFileInfo, liquidAppsFileList) { 162 | liquidAppsNames << liquidAppFileInfo.completeBaseName(); 163 | } 164 | return liquidAppsNames; 165 | } 166 | 167 | QString Liquid::getReadableDateTimeString(void) 168 | { 169 | return QDateTime::currentDateTimeUtc().toString(QLocale().dateTimeFormat()); 170 | } 171 | 172 | void Liquid::removeDesktopFile(const QString liquidAppName) 173 | { 174 | #if defined(Q_OS_LINUX) 175 | // Construct directory path 176 | // QString desktopPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); 177 | const QString desktopPath = QDir::homePath() + QDir::separator() + "Desktop"; 178 | 179 | // Check if the desktop path exists 180 | QDir dir(desktopPath); 181 | if (dir.exists()) { 182 | // Unlink file 183 | QFile file(desktopPath + QDir::separator() + liquidAppName + ".desktop"); 184 | file.remove(); 185 | } 186 | #else 187 | Q_UNUSED(liquidAppName); 188 | #endif 189 | } 190 | 191 | void Liquid::runLiquidApp(const QString liquidAppName) 192 | { 193 | QProcess::startDetached(QCoreApplication::applicationFilePath(), QStringList() << QStringLiteral("%1").arg(liquidAppName)); 194 | } 195 | 196 | void Liquid::sleep(const int ms) 197 | { 198 | const QTime proceedAfter = QTime::currentTime().addMSecs(ms); 199 | 200 | while (QTime::currentTime() < proceedAfter) { 201 | QCoreApplication::processEvents(QEventLoop::AllEvents, ms / 4); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/liquidappconfigwindow.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "liquid.hpp" 6 | #include "liquidappconfigwindow.hpp" 7 | #include "lqd.h" 8 | #include "mainwindow.hpp" 9 | 10 | LiquidAppConfigDialog::LiquidAppConfigDialog(QWidget* parent, QString liquidAppName) : QDialog(parent) 11 | { 12 | setWindowFlags(windowFlags() | Qt::Window); 13 | 14 | liquidAppName = liquidAppName.replace(QDir::separator(), "_"); 15 | 16 | Liquid::applyQtStyleSheets(this); 17 | 18 | // Attempt to load liquid app's config file 19 | QSettings* existingLiquidAppConfig = new QSettings(QSettings::IniFormat, 20 | QSettings::UserScope, 21 | QString(PROG_NAME "%1" LQD_APPS_DIR_NAME).arg(QDir::separator()), 22 | liquidAppName, 23 | Q_NULLPTR); 24 | 25 | // Check to see if Liquid app by this name already has config file 26 | if (liquidAppName.size() > 0) { 27 | isEditingExistingBool = existingLiquidAppConfig->contains(LQD_CFG_KEY_NAME_URL); 28 | } else { 29 | delete existingLiquidAppConfig; 30 | } 31 | 32 | if (isEditingExistingBool) { 33 | setWindowTitle(tr("Editing existing Liquid App “%1”").arg(liquidAppName)); 34 | } else { 35 | setWindowTitle(tr("Adding new Liquid App")); 36 | } 37 | 38 | backgroundColor = new QColor(LQD_DEFAULT_BG_COLOR); 39 | 40 | QVBoxLayout* mainLayout = new QVBoxLayout(); 41 | mainLayout->setSpacing(4); 42 | mainLayout->setContentsMargins(4, 4, 4, 4); 43 | mainLayout->setSizeConstraint(QLayout::SetFixedSize); 44 | 45 | QGridLayout* basicLayout = new QGridLayout(); 46 | 47 | QWidget* advancedWidget = new QWidget(this); 48 | QVBoxLayout* advancedLayout = new QVBoxLayout(); 49 | advancedWidget->setContentsMargins(0, 0, 0, 0); 50 | advancedWidget->setLayout(advancedLayout); 51 | 52 | QTabWidget* tabWidget = new QTabWidget; 53 | tabWidget->tabBar()->setCursor(Qt::PointingHandCursor); 54 | 55 | mainLayout->addLayout(basicLayout); 56 | 57 | // Name input 58 | { 59 | QLabel* nameInputLabel = new QLabel(tr("Name:"), this); 60 | nameInput = new QLineEdit; 61 | nameInput->setMinimumSize(480, 0); 62 | nameInput->setPlaceholderText("my-liquid-app-name"); 63 | nameInput->setText(liquidAppName); 64 | connect(nameInput, &QLineEdit::textChanged, [=]{ style()->polish(nameInput); }); 65 | 66 | if (isEditingExistingBool) { 67 | // TODO: make it possible to edit names for existing Liquid apps 68 | nameInput->setReadOnly(true); 69 | } 70 | 71 | basicLayout->addWidget(nameInputLabel, 0, 0); 72 | basicLayout->addWidget(nameInput, 0, 1); 73 | } 74 | 75 | // "URL" input 76 | { 77 | QLabel* addressInputLabel = new QLabel(tr("URL:"), this); 78 | addressInput = new QLineEdit; 79 | addressInput->setPlaceholderText("https://example.com"); 80 | connect(addressInput, &QLineEdit::textChanged, [=]{ style()->polish(addressInput); }); 81 | 82 | if (isEditingExistingBool) { 83 | addressInput->setText(existingLiquidAppConfig->value(LQD_CFG_KEY_NAME_URL).toString()); 84 | } 85 | 86 | basicLayout->addWidget(addressInputLabel, 1, 0); 87 | basicLayout->addWidget(addressInput, 1, 1); 88 | } 89 | 90 | // Extra checkboxes visible only in "Create" mode 91 | if (!isEditingExistingBool) { 92 | QHBoxLayout* extraCheckboxesLayout = new QHBoxLayout(); 93 | 94 | // "Create desktop icon" checkbox 95 | { 96 | createIconCheckBox = new QCheckBox(tr("Create desktop icon"), this); 97 | createIconCheckBox->setCursor(Qt::PointingHandCursor); 98 | extraCheckboxesLayout->addWidget(createIconCheckBox); 99 | } 100 | 101 | // "Run after creation" checkbox 102 | { 103 | planningToRunCheckBox = new QCheckBox(tr("Run after creation"), this); 104 | planningToRunCheckBox->setChecked(isPlanningToRunBool); 105 | planningToRunCheckBox->setCursor(Qt::PointingHandCursor); 106 | extraCheckboxesLayout->addWidget(planningToRunCheckBox, 0, Qt::AlignRight); 107 | } 108 | 109 | basicLayout->addLayout(extraCheckboxesLayout, 2, 1); 110 | } 111 | 112 | QPushButton* advancedButton; 113 | QPushButton* cancelButton; 114 | QPushButton* saveButton; 115 | 116 | // Horizontal buttons ("Advanced", "Cancel", "Create"/"Save") 117 | { 118 | QHBoxLayout* buttonsLayout = new QHBoxLayout(); 119 | buttonsLayout->setSpacing(4); 120 | buttonsLayout->setContentsMargins(0, 0, 0, 0); 121 | 122 | { 123 | advancedButton = new QPushButton(tr("Advanced"), this); 124 | advancedButton->setCursor(Qt::PointingHandCursor); 125 | advancedButton->setCheckable(true); 126 | buttonsLayout->addWidget(advancedButton); 127 | 128 | connect(advancedButton, SIGNAL(toggled(bool)), advancedWidget, SLOT(setVisible(bool))); 129 | } 130 | 131 | { 132 | cancelButton = new QPushButton(tr("Cancel"), this); 133 | cancelButton->setCursor(Qt::PointingHandCursor); 134 | buttonsLayout->addWidget(cancelButton); 135 | 136 | connect(cancelButton, &QPushButton::clicked, [&]() { 137 | close(); 138 | }); 139 | } 140 | 141 | { 142 | saveButton = new QPushButton(tr((isEditingExistingBool) ? "Save" : "Add"), this); 143 | saveButton->setCursor(Qt::PointingHandCursor); 144 | saveButton->setDefault(true); 145 | buttonsLayout->addWidget(saveButton); 146 | 147 | connect(saveButton, &QPushButton::clicked, [&]() { 148 | save(); 149 | }); 150 | } 151 | 152 | mainLayout->addLayout(buttonsLayout); 153 | } 154 | 155 | advancedLayout->addWidget(tabWidget); 156 | 157 | ///////////////// 158 | // General tab // 159 | ///////////////// 160 | 161 | { 162 | QWidget* generalTabWidget = new QWidget(this); 163 | QVBoxLayout* generalTabWidgetLayout = new QVBoxLayout(); 164 | generalTabWidget->setLayout(generalTabWidgetLayout); 165 | tabWidget->addTab(generalTabWidget, tr("General")); 166 | 167 | // Title text input 168 | { 169 | QHBoxLayout* titleLayout = new QHBoxLayout(); 170 | 171 | // Title label 172 | { 173 | QLabel* textLabel = new QLabel(tr("Title:"), this); 174 | 175 | titleLayout->addWidget(textLabel); 176 | } 177 | 178 | // Title text input 179 | { 180 | titleInput = new QLineEdit(this); 181 | titleInput->setPlaceholderText(tr("Application Title")); 182 | connect(titleInput, &QLineEdit::textChanged, [=]{ style()->polish(titleInput); }); 183 | 184 | if (isEditingExistingBool) { 185 | titleInput->setText(existingLiquidAppConfig->value(LQD_CFG_KEY_NAME_TITLE).toString()); 186 | } 187 | 188 | titleLayout->addWidget(titleInput); 189 | } 190 | 191 | generalTabWidgetLayout->addLayout(titleLayout); 192 | } 193 | 194 | // Additional domains list view 195 | { 196 | generalTabWidgetLayout->addWidget(separator()); 197 | 198 | // Additional domains label 199 | { 200 | QLabel* additionalDomainsListLabel = new QLabel(tr("Additional domains:"), this); 201 | generalTabWidgetLayout->addWidget(additionalDomainsListLabel); 202 | } 203 | 204 | // Editable list of additional domains 205 | { 206 | additionalDomainsListView = new QListView(this); 207 | additionalDomainsModel = new QStandardItemModel(this); 208 | 209 | // Assign model 210 | additionalDomainsListView->setModel(additionalDomainsModel); 211 | 212 | // Fill model items 213 | if (isEditingExistingBool) { 214 | if (existingLiquidAppConfig->contains(LQD_CFG_KEY_NAME_ADDITIONAL_DOMAINS)) { 215 | const QStringList additionalDomainsList = existingLiquidAppConfig->value(LQD_CFG_KEY_NAME_ADDITIONAL_DOMAINS).toString().split(" "); 216 | 217 | for (int i = 0; i < additionalDomainsList.size(); i++) { 218 | QStandardItem* item = new QStandardItem(additionalDomainsList[i]); 219 | additionalDomainsModel->appendRow(item); 220 | } 221 | } 222 | } 223 | 224 | // Append empty row 225 | additionalDomainsModel->appendRow(new QStandardItem()); 226 | 227 | connect(additionalDomainsModel, &QStandardItemModel::itemChanged, [&](QStandardItem* item){ 228 | const int itemIndex = item->row(); 229 | const bool isLastItem = itemIndex == additionalDomainsModel->rowCount() - 1; 230 | static const QRegularExpression allowedCharacters = QRegularExpression("[^a-z0-9\\.:\\-]"); 231 | 232 | // Format domain name 233 | item->setText(item->text().toLower().remove(allowedCharacters)); 234 | 235 | if (item->text().size() == 0) { 236 | // Automatically remove empty rows from the list 237 | if (!isLastItem) { 238 | additionalDomainsModel->removeRows(itemIndex, 1); 239 | } 240 | } else { 241 | if (isLastItem) { 242 | // Append empty row 243 | additionalDomainsModel->appendRow(new QStandardItem()); 244 | } 245 | } 246 | }); 247 | } 248 | 249 | generalTabWidgetLayout->addWidget(additionalDomainsListView); 250 | } 251 | 252 | // Custom user-agent text input 253 | { 254 | generalTabWidgetLayout->addWidget(separator()); 255 | 256 | QHBoxLayout* customUserAgentLayout = new QHBoxLayout(); 257 | 258 | // Custom user-agent label 259 | { 260 | QLabel* customUserAgentLabel = new QLabel(tr("Custom user-agent string:"), this); 261 | 262 | customUserAgentLayout->addWidget(customUserAgentLabel); 263 | } 264 | 265 | // Custom user-agent text input 266 | { 267 | userAgentInput = new QLineEdit(this); 268 | // Set placeholder to what QWebEngineProfile has by default 269 | userAgentInput->setPlaceholderText(Liquid::getDefaultUserAgentString()); 270 | connect(userAgentInput, &QLineEdit::textChanged, [=]{ style()->polish(userAgentInput); }); 271 | 272 | if (isEditingExistingBool) { 273 | userAgentInput->setText(existingLiquidAppConfig->value(LQD_CFG_KEY_NAME_USER_AGENT).toString()); 274 | } 275 | 276 | customUserAgentLayout->addWidget(userAgentInput); 277 | } 278 | 279 | generalTabWidgetLayout->addLayout(customUserAgentLayout); 280 | } 281 | 282 | // Notes text area 283 | { 284 | generalTabWidgetLayout->addWidget(separator()); 285 | 286 | QLabel* notesLabel = new QLabel(tr("Notes:"), this); 287 | notesTextArea = new QPlainTextEdit(this); 288 | notesTextArea->setPlaceholderText(tr("Intentionally left blank")); 289 | connect(notesTextArea, &QPlainTextEdit::textChanged, [=]{ style()->polish(notesTextArea); }); 290 | 291 | if (isEditingExistingBool) { 292 | notesTextArea->setPlainText(existingLiquidAppConfig->value(LQD_CFG_KEY_NAME_NOTES).toString()); 293 | } 294 | 295 | generalTabWidgetLayout->addWidget(notesLabel); 296 | generalTabWidgetLayout->addWidget(notesTextArea); 297 | } 298 | } 299 | 300 | //////////////////// 301 | // Appearance tab // 302 | //////////////////// 303 | 304 | { 305 | QWidget* appearanceTabWidget = new QWidget(this); 306 | QVBoxLayout* appearanceTabWidgetLayout = new QVBoxLayout(); 307 | appearanceTabWidget->setLayout(appearanceTabWidgetLayout); 308 | tabWidget->addTab(appearanceTabWidget, tr("Appearance")); 309 | 310 | // Hide scrollbars checkbox 311 | #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) 312 | { 313 | hideScrollBarsCheckBox = new QCheckBox(tr("Hide scrollbars"), this); 314 | hideScrollBarsCheckBox->setCursor(Qt::PointingHandCursor); 315 | 316 | if (isEditingExistingBool) { 317 | if (existingLiquidAppConfig->contains(LQD_CFG_KEY_NAME_HIDE_SCROLLBARS)) { 318 | hideScrollBarsCheckBox->setChecked( 319 | existingLiquidAppConfig->value(LQD_CFG_KEY_NAME_HIDE_SCROLLBARS).toBool() 320 | ); 321 | } 322 | } 323 | 324 | appearanceTabWidgetLayout->addWidget(hideScrollBarsCheckBox); 325 | } 326 | #endif 327 | 328 | // Remove window frame 329 | { 330 | appearanceTabWidgetLayout->addWidget(separator()); 331 | 332 | removeWindowFrameCheckBox = new QCheckBox(tr("Remove window frame"), this); 333 | removeWindowFrameCheckBox->setCursor(Qt::PointingHandCursor); 334 | 335 | if (isEditingExistingBool) { 336 | if (existingLiquidAppConfig->contains(LQD_CFG_KEY_NAME_REMOVE_WINDOW_FRAME)) { 337 | removeWindowFrameCheckBox->setChecked( 338 | existingLiquidAppConfig->value(LQD_CFG_KEY_NAME_REMOVE_WINDOW_FRAME).toBool() 339 | ); 340 | } 341 | } 342 | 343 | appearanceTabWidgetLayout->addWidget(removeWindowFrameCheckBox); 344 | } 345 | 346 | // Custom background color 347 | { 348 | appearanceTabWidgetLayout->addWidget(separator()); 349 | 350 | QHBoxLayout* customBackgroundColorButtonLayout = new QHBoxLayout(); 351 | 352 | // Use custom background checkbox 353 | { 354 | useCustomBackgroundCheckBox = new QCheckBox(tr("Use custom background color:"), this); 355 | useCustomBackgroundCheckBox->setCursor(Qt::PointingHandCursor); 356 | 357 | customBackgroundColorButtonLayout->addWidget(useCustomBackgroundCheckBox); 358 | } 359 | 360 | // Custom background color button 361 | { 362 | customBackgroundColorButton = new QPushButton("█"); 363 | customBackgroundColorButton->setCursor(Qt::PointingHandCursor); 364 | customBackgroundColorButton->setFlat(true); 365 | customBackgroundColorButton->setFixedSize(customBackgroundColorButton->width(), 24); 366 | 367 | static const QString buttonStyle = QString("background-image: url(:/images/checkers.svg); border-radius: 4px; padding: 0; color: %1; font-size: %2px;"); 368 | // TODO: animate background pattern 369 | static const int fontSize = customBackgroundColorButton->width() * 0.9; 370 | 371 | if (isEditingExistingBool && existingLiquidAppConfig->contains(LQD_CFG_KEY_NAME_CUSTOM_BG_COLOR)) { 372 | backgroundColor = new QColor(existingLiquidAppConfig->value(LQD_CFG_KEY_NAME_CUSTOM_BG_COLOR).toString()); 373 | customBackgroundColorButton->setStyleSheet(buttonStyle.arg(backgroundColor->name(QColor::HexArgb)).arg(fontSize)); 374 | } else { 375 | static const QColor defaultColor = QColor(LQD_DEFAULT_BG_COLOR); 376 | customBackgroundColorButton->setStyleSheet(buttonStyle.arg(defaultColor.name(QColor::HexArgb)).arg(fontSize)); 377 | } 378 | 379 | customBackgroundColorButtonLayout->addWidget(customBackgroundColorButton); 380 | 381 | connect(customBackgroundColorButton, &QPushButton::clicked, [&]() { 382 | const QColorDialog::ColorDialogOptions options = QFlag(QColorDialog::ShowAlphaChannel); 383 | QColor color = QColorDialog::getColor(*backgroundColor, this, tr("Pick custom background color"), options); 384 | 385 | if (color.isValid()) { 386 | if (!useCustomBackgroundCheckBox->isChecked()) { 387 | useCustomBackgroundCheckBox->setChecked(true); 388 | } 389 | 390 | *backgroundColor = color; 391 | customBackgroundColorButton->setStyleSheet(buttonStyle.arg(backgroundColor->name(QColor::HexArgb)).arg(fontSize)); 392 | } 393 | }); 394 | } 395 | 396 | appearanceTabWidgetLayout->addLayout(customBackgroundColorButtonLayout); 397 | 398 | if (isEditingExistingBool) { 399 | bool enabledInConfig = existingLiquidAppConfig->value(LQD_CFG_KEY_NAME_USE_CUSTOM_BG, false).toBool(); 400 | useCustomBackgroundCheckBox->setChecked(enabledInConfig); 401 | } 402 | } 403 | 404 | // Additional CSS text area 405 | { 406 | appearanceTabWidgetLayout->addWidget(separator()); 407 | 408 | { 409 | QLabel* additionalCssLabel = new QLabel(tr("Additional CSS:"), this); 410 | appearanceTabWidgetLayout->addWidget(additionalCssLabel); 411 | } 412 | additionalCssTextArea = new QPlainTextEdit(this); 413 | additionalCssTextArea->setProperty("class", "monospace"); 414 | additionalCssTextArea->setPlaceholderText(tr("/* put your custom CSS here */")); 415 | connect(additionalCssTextArea, &QPlainTextEdit::textChanged, [=]{ style()->polish(additionalCssTextArea); }); 416 | 417 | if (isEditingExistingBool) { 418 | additionalCssTextArea->setPlainText(existingLiquidAppConfig->value(LQD_CFG_KEY_NAME_ADDITIONAL_CSS).toString()); 419 | } 420 | 421 | appearanceTabWidgetLayout->addWidget(additionalCssTextArea); 422 | } 423 | } 424 | 425 | //////////////////// 426 | // JavaScript tab // 427 | //////////////////// 428 | 429 | { 430 | QWidget* jsTabWidget = new QWidget(this); 431 | QVBoxLayout* jsTabWidgetLayout = new QVBoxLayout(); 432 | jsTabWidget->setLayout(jsTabWidgetLayout); 433 | tabWidget->addTab(jsTabWidget, tr("JavaScript")); 434 | 435 | // Enable JavaScript checkbox 436 | { 437 | enableJavaScriptCheckBox = new QCheckBox(tr("Enable JavaScript"), this); 438 | enableJavaScriptCheckBox->setCursor(Qt::PointingHandCursor); 439 | 440 | if (isEditingExistingBool) { 441 | bool isChecked = existingLiquidAppConfig->value(LQD_CFG_KEY_NAME_ENABLE_JS).toBool(); 442 | enableJavaScriptCheckBox->setChecked(isChecked); 443 | } else { 444 | // Checked by default (when creating new Liquid app) 445 | enableJavaScriptCheckBox->setChecked(true); 446 | } 447 | 448 | jsTabWidgetLayout->addWidget(enableJavaScriptCheckBox); 449 | } 450 | 451 | // Additonal JavaScript code text area 452 | { 453 | jsTabWidgetLayout->addWidget(separator()); 454 | 455 | additionalJsLabel = new QLabel(tr("Additonal JavaScript code:"), this); 456 | additionalJsTextArea = new QPlainTextEdit(this); 457 | additionalJsTextArea->setProperty("class", "monospace"); 458 | additionalJsTextArea->setPlaceholderText(tr("// This code will run even when JS is disabled")); 459 | connect(additionalJsTextArea, &QPlainTextEdit::textChanged, [=]{ style()->polish(additionalJsTextArea); }); 460 | 461 | if (isEditingExistingBool) { 462 | additionalJsTextArea->setPlainText(existingLiquidAppConfig->value(LQD_CFG_KEY_NAME_ADDITIONAL_JS).toString()); 463 | } 464 | 465 | jsTabWidgetLayout->addWidget(additionalJsLabel); 466 | jsTabWidgetLayout->addWidget(additionalJsTextArea); 467 | } 468 | } 469 | 470 | ///////////////// 471 | // Cookies tab // 472 | ///////////////// 473 | 474 | { 475 | QWidget* cookiesTabWidget = new QWidget(this); 476 | QVBoxLayout* cookiesTabWidgetLayout = new QVBoxLayout(); 477 | cookiesTabWidget->setLayout(cookiesTabWidgetLayout); 478 | tabWidget->addTab(cookiesTabWidget, tr("Cookies")); 479 | 480 | // Allow cookies & allow third-party cookies checkboxes 481 | { 482 | // Allow cookies checkbox 483 | { 484 | allowCookiesCheckBox = new QCheckBox(tr("Allow cookies"), this); 485 | allowCookiesCheckBox->setCursor(Qt::PointingHandCursor); 486 | 487 | if (isEditingExistingBool) { 488 | bool isChecked = existingLiquidAppConfig->value(LQD_CFG_KEY_NAME_ALLOW_COOKIES).toBool(); 489 | allowCookiesCheckBox->setChecked(isChecked); 490 | } else { 491 | // Checked by default (when creating new Liquid App) 492 | allowCookiesCheckBox->setChecked(true); 493 | } 494 | 495 | cookiesTabWidgetLayout->addWidget(allowCookiesCheckBox); 496 | } 497 | 498 | // Allow third-party cookies checkbox 499 | { 500 | QHBoxLayout* allowThirdPartyCookiesLayout = new QHBoxLayout(); 501 | allowThirdPartyCookiesLayout->setContentsMargins(LQD_UI_MARGIN, 0, 0, 0); 502 | 503 | allowThirdPartyCookiesCheckBox = new QCheckBox(tr("Allow third-party cookies"), this); 504 | allowThirdPartyCookiesCheckBox->setCursor(Qt::PointingHandCursor); 505 | 506 | if (isEditingExistingBool) { 507 | bool isChecked = existingLiquidAppConfig->value(LQD_CFG_KEY_NAME_ALLOW_3RD_PARTY_COOKIES).toBool(); 508 | allowThirdPartyCookiesCheckBox->setChecked(isChecked); 509 | } 510 | 511 | allowThirdPartyCookiesLayout->addWidget(allowThirdPartyCookiesCheckBox); 512 | 513 | connect(allowCookiesCheckBox, &QCheckBox::toggled, [&](const bool isOn){ 514 | if (!isOn) { 515 | allowThirdPartyCookiesCheckBox->setChecked(false); 516 | } 517 | }); 518 | connect(allowThirdPartyCookiesCheckBox, &QCheckBox::toggled, [&](const bool isOn){ 519 | if (isOn) { 520 | allowCookiesCheckBox->setChecked(isOn); 521 | } 522 | }); 523 | 524 | cookiesTabWidgetLayout->addLayout(allowThirdPartyCookiesLayout); 525 | } 526 | } 527 | 528 | // Cookies list view 529 | { 530 | cookiesTabWidgetLayout->addWidget(separator()); 531 | 532 | // Cookies list label 533 | { 534 | QLabel* cookiesListLabel = new QLabel(tr("Cookie jar:"), this); 535 | cookiesTabWidgetLayout->addWidget(cookiesListLabel); 536 | } 537 | 538 | // Cookies list 539 | { 540 | cookiesTableView = new QTableView(this); 541 | cookiesModel = new QStandardItemModel(this); 542 | 543 | cookiesModel->setHorizontalHeaderItem(0, new QStandardItem(tr("Name"))); 544 | cookiesModel->setHorizontalHeaderItem(1, new QStandardItem(tr("Value"))); 545 | cookiesModel->setHorizontalHeaderItem(2, new QStandardItem(tr("Domain"))); 546 | cookiesModel->setHorizontalHeaderItem(3, new QStandardItem(tr("Path"))); 547 | cookiesModel->setHorizontalHeaderItem(4, new QStandardItem(tr("Expires"))); 548 | cookiesModel->setHorizontalHeaderItem(5, new QStandardItem(tr("HttpOnly"))); 549 | cookiesModel->setHorizontalHeaderItem(6, new QStandardItem(tr("Secure"))); 550 | 551 | // Assign model 552 | cookiesTableView->setModel(cookiesModel); 553 | 554 | // Fill model items 555 | if (isEditingExistingBool) { 556 | existingLiquidAppConfig->beginGroup(LQD_CFG_GROUP_NAME_COOKIES); 557 | int i = 0; 558 | foreach(QString cookieId, existingLiquidAppConfig->allKeys()) { 559 | const QByteArray rawCookie = existingLiquidAppConfig->value(cookieId).toByteArray(); 560 | QList cookies = QNetworkCookie::parseCookies(rawCookie); 561 | if (cookies.size() > 0) { 562 | QNetworkCookie cookie = cookies[0]; 563 | 564 | cookiesModel->appendRow(new QStandardItem()); 565 | cookiesModel->setItem(i, 0, new QStandardItem(QString(cookie.name()))); 566 | cookiesModel->setItem(i, 1, new QStandardItem(QString(cookie.value()))); 567 | cookiesModel->setItem(i, 2, new QStandardItem(cookie.domain())); 568 | cookiesModel->setItem(i, 3, new QStandardItem(cookie.path())); 569 | cookiesModel->setItem(i, 4, new QStandardItem(cookie.expirationDate().toString())); 570 | cookiesModel->setItem(i, 5, new QStandardItem(cookie.isHttpOnly() ? "true" : "false")); 571 | cookiesModel->setItem(i, 6, new QStandardItem(cookie.isSecure() ? "true" : "false")); 572 | 573 | i++; 574 | } 575 | } 576 | existingLiquidAppConfig->endGroup(); 577 | } 578 | } 579 | 580 | cookiesTabWidgetLayout->addWidget(cookiesTableView); 581 | } 582 | } 583 | 584 | ///////////////// 585 | // Network tab // 586 | ///////////////// 587 | 588 | { 589 | QWidget* networkTabWidget = new QWidget(this); 590 | QVBoxLayout* networkTabWidgetLayout = new QVBoxLayout(); 591 | networkTabWidget->setLayout(networkTabWidgetLayout); 592 | tabWidget->addTab(networkTabWidget, tr("Network")); 593 | 594 | // Proxy 595 | { 596 | // Option 1: Use global system proxy settings (default) 597 | { 598 | proxyModeSystemRadioButton = new QRadioButton(tr("Use global system settings"), this); 599 | proxyModeSystemRadioButton->setCursor(Qt::PointingHandCursor); 600 | 601 | networkTabWidgetLayout->addWidget(proxyModeSystemRadioButton); 602 | } 603 | 604 | // Option 2: Use direct internet connection 605 | { 606 | proxyModeDirectRadioButton = new QRadioButton(tr("Direct internet connection"), this); 607 | proxyModeDirectRadioButton->setCursor(Qt::PointingHandCursor); 608 | 609 | networkTabWidgetLayout->addWidget(proxyModeDirectRadioButton); 610 | } 611 | 612 | // Option 3: Use custom proxy configuration 613 | { 614 | QHBoxLayout* customProxyModeLayout = new QHBoxLayout(); 615 | 616 | // Radio box 617 | proxyModeCustomRadioButton = new QRadioButton(tr("Custom proxy configuration:"), this); 618 | proxyModeCustomRadioButton->setCursor(Qt::PointingHandCursor); 619 | 620 | customProxyModeLayout->addWidget(proxyModeCustomRadioButton, 0, Qt::AlignTop); 621 | 622 | // Custom proxy configuration 623 | { 624 | QVBoxLayout* proxyConfigLayout = new QVBoxLayout(); 625 | 626 | // Row 1 (type, host, port) 627 | { 628 | QHBoxLayout* proxyTypeHostPortLayout = new QHBoxLayout(); 629 | 630 | // Proxy type 631 | { 632 | useSocksSelectBox = new QComboBox(this); 633 | useSocksSelectBox->addItem(tr("HTTP"), false); 634 | useSocksSelectBox->addItem(tr("SOCKS"), true); 635 | 636 | if (isEditingExistingBool && existingLiquidAppConfig->contains(LQD_CFG_KEY_NAME_PROXY_USE_SOCKS)) { 637 | const bool useSocks = existingLiquidAppConfig->value(LQD_CFG_KEY_NAME_PROXY_USE_SOCKS, false).toBool(); 638 | 639 | if (useSocks) { 640 | useSocksSelectBox->setCurrentIndex(1); 641 | } 642 | } 643 | 644 | proxyTypeHostPortLayout->addWidget(useSocksSelectBox); 645 | } 646 | 647 | // Proxy host 648 | { 649 | proxyHostInput = new QLineEdit(this); 650 | proxyHostInput->setPlaceholderText(LQD_DEFAULT_PROXY_HOST); 651 | connect(proxyHostInput, &QLineEdit::textChanged, [=]{ style()->polish(proxyHostInput); }); 652 | 653 | if (isEditingExistingBool && existingLiquidAppConfig->contains(LQD_CFG_KEY_NAME_PROXY_HOST)) { 654 | proxyHostInput->setText(existingLiquidAppConfig->value(LQD_CFG_KEY_NAME_PROXY_HOST).toString()); 655 | } 656 | 657 | proxyTypeHostPortLayout->addWidget(proxyHostInput); 658 | } 659 | 660 | // Proxy port 661 | { 662 | proxyPortInput = new QSpinBox(this); 663 | proxyPortInput->setRange(0, 65535); 664 | proxyPortInput->setValue(LQD_DEFAULT_PROXY_PORT); 665 | 666 | if (isEditingExistingBool && existingLiquidAppConfig->contains(LQD_CFG_KEY_NAME_PROXY_PORT)) { 667 | proxyPortInput->setValue(existingLiquidAppConfig->value(LQD_CFG_KEY_NAME_PROXY_PORT).toInt()); 668 | } 669 | 670 | proxyTypeHostPortLayout->addWidget(proxyPortInput); 671 | } 672 | 673 | proxyConfigLayout->addLayout(proxyTypeHostPortLayout); 674 | } 675 | 676 | // Row 2 (use authentication checkbox, username, password) 677 | { 678 | QHBoxLayout* proxyCredentialsLayout = new QHBoxLayout(); 679 | 680 | // Use credentials 681 | { 682 | proxyUseAuthCheckBox = new QCheckBox(tr("Authenticate with credentials:"), this); 683 | 684 | proxyCredentialsLayout->addWidget(proxyUseAuthCheckBox); 685 | } 686 | 687 | // Username 688 | { 689 | proxyUsernameInput = new QLineEdit(this); 690 | proxyUsernameInput->setPlaceholderText(tr("Username")); 691 | connect(proxyUsernameInput, &QLineEdit::textChanged, [=]{ style()->polish(proxyUsernameInput); }); 692 | 693 | if (isEditingExistingBool && existingLiquidAppConfig->contains(LQD_CFG_KEY_NAME_PROXY_USER_NAME)) { 694 | proxyUsernameInput->setText(existingLiquidAppConfig->value(LQD_CFG_KEY_NAME_PROXY_USER_NAME).toString()); 695 | } 696 | 697 | proxyCredentialsLayout->addWidget(proxyUsernameInput); 698 | } 699 | 700 | // Password 701 | { 702 | proxyPasswordInput = new QLineEdit(this); 703 | proxyPasswordInput->setPlaceholderText(tr("Password")); 704 | proxyPasswordInput->setEchoMode(QLineEdit::Password); 705 | connect(proxyPasswordInput, &QLineEdit::textChanged, [=]{ style()->polish(proxyPasswordInput); }); 706 | 707 | if (isEditingExistingBool && existingLiquidAppConfig->contains(LQD_CFG_KEY_NAME_PROXY_USER_NAME)) { 708 | proxyPasswordInput->setText(existingLiquidAppConfig->value(LQD_CFG_KEY_NAME_PROXY_USER_NAME).toString()); 709 | } 710 | 711 | proxyCredentialsLayout->addWidget(proxyPasswordInput); 712 | } 713 | 714 | proxyConfigLayout->addLayout(proxyCredentialsLayout); 715 | } 716 | 717 | if (isEditingExistingBool && existingLiquidAppConfig->contains(LQD_CFG_KEY_NAME_USE_PROXY)) { 718 | const bool proxyEnabled = existingLiquidAppConfig->value(LQD_CFG_KEY_NAME_USE_PROXY, false).toBool(); 719 | 720 | if (proxyEnabled) { 721 | proxyModeCustomRadioButton->setChecked(true); 722 | } else { 723 | proxyModeDirectRadioButton->setChecked(true); 724 | } 725 | } else { 726 | proxyModeSystemRadioButton->setChecked(true); 727 | } 728 | 729 | if (isEditingExistingBool && existingLiquidAppConfig->value(LQD_CFG_KEY_NAME_PROXY_USE_AUTH, false).toBool()) { 730 | proxyUseAuthCheckBox->setChecked(true); 731 | } 732 | 733 | customProxyModeLayout->addLayout(proxyConfigLayout); 734 | } 735 | 736 | connect(useSocksSelectBox, &QComboBox::currentTextChanged, [&](){ 737 | proxyModeCustomRadioButton->setChecked(true); 738 | }); 739 | connect(proxyHostInput, &QLineEdit::textChanged, [&](const QString value){ 740 | if (value.size() > 0) { 741 | proxyModeCustomRadioButton->setChecked(true); 742 | } else { 743 | proxyModeSystemRadioButton->setChecked(true); 744 | } 745 | }); 746 | connect(proxyPortInput, QOverload::of(&QSpinBox::valueChanged), [&](){ 747 | proxyModeCustomRadioButton->setChecked(true); 748 | }); 749 | connect(proxyUseAuthCheckBox, &QCheckBox::stateChanged, [&](){ 750 | proxyModeCustomRadioButton->setChecked(true); 751 | }); 752 | connect(proxyUsernameInput, &QLineEdit::textChanged, [&](const QString value){ 753 | proxyModeCustomRadioButton->setChecked(true); 754 | proxyUseAuthCheckBox->setChecked(value.size() > 0); 755 | }); 756 | connect(proxyPasswordInput, &QLineEdit::textChanged, [&](const QString value){ 757 | proxyModeCustomRadioButton->setChecked(true); 758 | proxyUseAuthCheckBox->setChecked(value.size() > 0); 759 | }); 760 | 761 | networkTabWidgetLayout->addLayout(customProxyModeLayout); 762 | } 763 | } 764 | 765 | // Spacer 766 | { 767 | QWidget* spacer = new QWidget(this); 768 | spacer->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); 769 | networkTabWidgetLayout->addWidget(spacer); 770 | } 771 | } 772 | 773 | mainLayout->addWidget(advancedWidget); 774 | 775 | setLayout(mainLayout); 776 | 777 | if (isEditingExistingBool) { 778 | // Force advanced section to be visible in edit mode 779 | advancedButton->toggle(); 780 | } else { 781 | advancedWidget->hide(); 782 | } 783 | 784 | // Reveal and bring to front 785 | { 786 | show(); 787 | raise(); 788 | activateWindow(); 789 | } 790 | 791 | // Connect keyboard shortcuts 792 | bindShortcuts(); 793 | } 794 | 795 | LiquidAppConfigDialog::~LiquidAppConfigDialog(void) 796 | { 797 | } 798 | 799 | void LiquidAppConfigDialog::save() 800 | { 801 | bool isFormValid = nameInput->text().size() > 0 && addressInput->text().size() > 0; 802 | 803 | if (!isFormValid) { 804 | return; 805 | } 806 | 807 | QString appName = nameInput->text(); 808 | // Replace directory separators (slashes) with underscores 809 | // to ensure no sub-directories would get created 810 | appName = appName.replace(QDir::separator(), "_"); 811 | 812 | // Check if given Liquid App name is already in use 813 | if (!isEditingExistingBool && Liquid::getLiquidAppsList().contains(appName)) { 814 | return; 815 | } 816 | 817 | QSettings* tempLiquidAppConfig = new QSettings(QSettings::IniFormat, 818 | QSettings::UserScope, 819 | QString(PROG_NAME) + QDir::separator() + LQD_APPS_DIR_NAME, 820 | appName, 821 | Q_NULLPTR); 822 | 823 | // URL 824 | { 825 | QUrl url(QUrl::fromUserInput(addressInput->text())); 826 | // TODO: if was given only hostname and prepending http:// didn't help, prepend https:// 827 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_URL, url.toString()); 828 | } 829 | 830 | // Enable JS 831 | { 832 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_ENABLE_JS, enableJavaScriptCheckBox->isChecked()); 833 | } 834 | 835 | // Allow cookies 836 | { 837 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_ALLOW_COOKIES, allowCookiesCheckBox->isChecked()); 838 | } 839 | 840 | // Allow third-party cookies 841 | { 842 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_ALLOW_3RD_PARTY_COOKIES, allowThirdPartyCookiesCheckBox->isChecked()); 843 | } 844 | 845 | #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) 846 | // Hide scrollbars 847 | { 848 | if (isEditingExistingBool) { 849 | if (tempLiquidAppConfig->contains(LQD_CFG_KEY_NAME_HIDE_SCROLLBARS) && !hideScrollBarsCheckBox->isChecked()) { 850 | tempLiquidAppConfig->remove(LQD_CFG_KEY_NAME_HIDE_SCROLLBARS); 851 | } else { 852 | if (hideScrollBarsCheckBox->isChecked()) { 853 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_HIDE_SCROLLBARS, true); 854 | } 855 | } 856 | } else { 857 | if (hideScrollBarsCheckBox->isChecked()) { 858 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_HIDE_SCROLLBARS, true); 859 | } 860 | } 861 | } 862 | #endif 863 | 864 | // Remove window frame 865 | { 866 | if (isEditingExistingBool) { 867 | if (tempLiquidAppConfig->contains(LQD_CFG_KEY_NAME_REMOVE_WINDOW_FRAME) && !removeWindowFrameCheckBox->isChecked()) { 868 | tempLiquidAppConfig->remove(LQD_CFG_KEY_NAME_REMOVE_WINDOW_FRAME); 869 | } else { 870 | if (removeWindowFrameCheckBox->isChecked()) { 871 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_REMOVE_WINDOW_FRAME, true); 872 | } 873 | } 874 | } else { 875 | if (removeWindowFrameCheckBox->isChecked()) { 876 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_REMOVE_WINDOW_FRAME, true); 877 | } 878 | } 879 | } 880 | 881 | // Custom window title 882 | { 883 | if (isEditingExistingBool) { 884 | if (tempLiquidAppConfig->contains(LQD_CFG_KEY_NAME_TITLE) && titleInput->text().size() == 0) { 885 | tempLiquidAppConfig->remove(LQD_CFG_KEY_NAME_TITLE); 886 | } else { 887 | if (tempLiquidAppConfig->value(LQD_CFG_KEY_NAME_TITLE).toString().size() > 0 888 | || titleInput->text().size() > 0 889 | ) { 890 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_TITLE, titleInput->text()); 891 | } 892 | } 893 | } else { 894 | if (titleInput->text().size() > 0) { 895 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_TITLE, titleInput->text()); 896 | } 897 | } 898 | } 899 | 900 | // Custom CSS 901 | { 902 | if (isEditingExistingBool) { 903 | if (tempLiquidAppConfig->contains(LQD_CFG_KEY_NAME_ADDITIONAL_CSS) && additionalCssTextArea->toPlainText().size() == 0) { 904 | tempLiquidAppConfig->remove(LQD_CFG_KEY_NAME_ADDITIONAL_CSS); 905 | } else { 906 | if (tempLiquidAppConfig->value(LQD_CFG_KEY_NAME_ADDITIONAL_CSS).toString().size() > 0 907 | || additionalCssTextArea->toPlainText().size() > 0 908 | ) { 909 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_ADDITIONAL_CSS, additionalCssTextArea->toPlainText()); 910 | } 911 | } 912 | } else { 913 | if (additionalCssTextArea->toPlainText().size() > 0) { 914 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_ADDITIONAL_CSS, additionalCssTextArea->toPlainText()); 915 | } 916 | } 917 | } 918 | 919 | // Custom JS 920 | { 921 | if (isEditingExistingBool) { 922 | if (tempLiquidAppConfig->contains(LQD_CFG_KEY_NAME_ADDITIONAL_JS) && additionalJsTextArea->toPlainText().size() == 0) { 923 | tempLiquidAppConfig->remove(LQD_CFG_KEY_NAME_ADDITIONAL_JS); 924 | } else { 925 | if (tempLiquidAppConfig->value(LQD_CFG_KEY_NAME_ADDITIONAL_JS).toString().size() > 0 926 | || additionalJsTextArea->toPlainText().size() > 0 927 | ) { 928 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_ADDITIONAL_JS, additionalJsTextArea->toPlainText()); 929 | } 930 | } 931 | } else { 932 | if (additionalJsTextArea->toPlainText().size() > 0) { 933 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_ADDITIONAL_JS, additionalJsTextArea->toPlainText()); 934 | } 935 | } 936 | } 937 | 938 | // Custom user-agent 939 | { 940 | if (isEditingExistingBool) { 941 | if (tempLiquidAppConfig->contains(LQD_CFG_KEY_NAME_USER_AGENT) && userAgentInput->text().size() == 0) { 942 | tempLiquidAppConfig->remove(LQD_CFG_KEY_NAME_USER_AGENT); 943 | } else { 944 | if (tempLiquidAppConfig->value(LQD_CFG_KEY_NAME_USER_AGENT).toString().size() > 0 945 | || userAgentInput->text().size() > 0 946 | ) { 947 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_USER_AGENT, userAgentInput->text()); 948 | } 949 | } 950 | } else { 951 | if (userAgentInput->text().size() > 0) { 952 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_USER_AGENT, userAgentInput->text()); 953 | } 954 | } 955 | } 956 | 957 | // Notes 958 | { 959 | if (isEditingExistingBool) { 960 | if (tempLiquidAppConfig->contains(LQD_CFG_KEY_NAME_NOTES) && notesTextArea->toPlainText().size() == 0) { 961 | tempLiquidAppConfig->remove(LQD_CFG_KEY_NAME_NOTES); 962 | } else { 963 | if (tempLiquidAppConfig->value(LQD_CFG_KEY_NAME_NOTES).toString().size() > 0 964 | || notesTextArea->toPlainText().size() > 0 965 | ) { 966 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_NOTES, notesTextArea->toPlainText()); 967 | } 968 | } 969 | } else { 970 | if (notesTextArea->toPlainText().size() > 0) { 971 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_NOTES, notesTextArea->toPlainText()); 972 | } 973 | } 974 | } 975 | 976 | // Create desktop icon 977 | { 978 | // TODO: make it possible to remove and (re-)create desktop icons for existing Liquid Apps 979 | 980 | if (!isEditingExistingBool) { 981 | if (createIconCheckBox->isChecked()) { 982 | QUrl url(QUrl::fromUserInput(addressInput->text())); 983 | Liquid::createDesktopFile(appName, url.toString()); 984 | } 985 | } 986 | } 987 | 988 | // Custom background color 989 | { 990 | // Use custom background checkbox 991 | { 992 | if (isEditingExistingBool) { 993 | if (tempLiquidAppConfig->contains(LQD_CFG_KEY_NAME_USE_CUSTOM_BG) && !useCustomBackgroundCheckBox->isChecked()) { 994 | tempLiquidAppConfig->remove(LQD_CFG_KEY_NAME_USE_CUSTOM_BG); 995 | } else { 996 | if (useCustomBackgroundCheckBox->isChecked()) { 997 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_USE_CUSTOM_BG, true); 998 | } 999 | } 1000 | } else { 1001 | if (useCustomBackgroundCheckBox->isChecked()) { 1002 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_USE_CUSTOM_BG, true); 1003 | } 1004 | } 1005 | } 1006 | 1007 | // Custom background color 1008 | { 1009 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_CUSTOM_BG_COLOR, backgroundColor->name(QColor::HexArgb)); 1010 | } 1011 | } 1012 | 1013 | // Additional domains 1014 | { 1015 | if (isEditingExistingBool && tempLiquidAppConfig->contains(LQD_CFG_KEY_NAME_ADDITIONAL_DOMAINS) && additionalDomainsModel->rowCount() == 1) { 1016 | tempLiquidAppConfig->remove(LQD_CFG_KEY_NAME_ADDITIONAL_DOMAINS); 1017 | } else { 1018 | QString additionalDomains; 1019 | 1020 | for (int i = 0, ilen = additionalDomainsModel->rowCount() - 1; i < ilen; i++) { 1021 | if (i > 0) { 1022 | additionalDomains += " "; 1023 | } 1024 | 1025 | additionalDomains += additionalDomainsModel->data(additionalDomainsModel->index(i, 0)).toString(); 1026 | } 1027 | 1028 | if (additionalDomains.size() > 0) { 1029 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_ADDITIONAL_DOMAINS, additionalDomains); 1030 | } 1031 | } 1032 | } 1033 | 1034 | // Proxy 1035 | { 1036 | // Proxy mode 1037 | { 1038 | if (proxyModeSystemRadioButton->isChecked()) { 1039 | if (isEditingExistingBool) { 1040 | if (tempLiquidAppConfig->contains(LQD_CFG_KEY_NAME_USE_PROXY)) { 1041 | tempLiquidAppConfig->remove(LQD_CFG_KEY_NAME_USE_PROXY); 1042 | } 1043 | } 1044 | } else if (proxyModeDirectRadioButton->isChecked()) { 1045 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_USE_PROXY, false); 1046 | } else if (proxyModeCustomRadioButton->isChecked()) { 1047 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_USE_PROXY, true); 1048 | } 1049 | } 1050 | 1051 | // Proxy type 1052 | { 1053 | if (useSocksSelectBox->currentIndex() == 0) { 1054 | if (isEditingExistingBool) { 1055 | if (tempLiquidAppConfig->contains(LQD_CFG_KEY_NAME_PROXY_USE_SOCKS)) { 1056 | tempLiquidAppConfig->remove(LQD_CFG_KEY_NAME_PROXY_USE_SOCKS); 1057 | } 1058 | } 1059 | } else { 1060 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_PROXY_USE_SOCKS, true); 1061 | } 1062 | } 1063 | 1064 | // Proxy host 1065 | { 1066 | if (isEditingExistingBool) { 1067 | if (tempLiquidAppConfig->contains(LQD_CFG_KEY_NAME_PROXY_HOST) && proxyHostInput->text().size() == 0) { 1068 | tempLiquidAppConfig->remove(LQD_CFG_KEY_NAME_PROXY_HOST); 1069 | } else { 1070 | if (tempLiquidAppConfig->value(LQD_CFG_KEY_NAME_PROXY_HOST).toString().size() > 0 1071 | || proxyHostInput->text().size() > 0 1072 | ) { 1073 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_PROXY_HOST, proxyHostInput->text()); 1074 | } 1075 | } 1076 | } else { 1077 | if (proxyHostInput->text().size() > 0) { 1078 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_PROXY_HOST, proxyHostInput->text()); 1079 | } 1080 | } 1081 | } 1082 | 1083 | // Proxy port number 1084 | { 1085 | if (isEditingExistingBool) { 1086 | if (tempLiquidAppConfig->contains(LQD_CFG_KEY_NAME_PROXY_PORT) && proxyPortInput->value() == LQD_DEFAULT_PROXY_PORT) { 1087 | tempLiquidAppConfig->remove(LQD_CFG_KEY_NAME_PROXY_PORT); 1088 | } else { 1089 | if (proxyPortInput->value() != LQD_DEFAULT_PROXY_PORT) { 1090 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_PROXY_PORT, proxyPortInput->value()); 1091 | } 1092 | } 1093 | } else { 1094 | if (proxyPortInput->value() != LQD_DEFAULT_PROXY_PORT) { 1095 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_PROXY_PORT, proxyPortInput->value()); 1096 | } 1097 | } 1098 | } 1099 | 1100 | // Proxy authentication 1101 | { 1102 | if (isEditingExistingBool) { 1103 | if (tempLiquidAppConfig->contains(LQD_CFG_KEY_NAME_PROXY_USE_AUTH) && !proxyUseAuthCheckBox->isChecked()) { 1104 | tempLiquidAppConfig->remove(LQD_CFG_KEY_NAME_PROXY_USE_AUTH); 1105 | } else { 1106 | if (proxyUseAuthCheckBox->isChecked()) { 1107 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_PROXY_USE_AUTH, true); 1108 | } 1109 | } 1110 | } else { 1111 | if (proxyUseAuthCheckBox->isChecked()) { 1112 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_PROXY_USE_AUTH, true); 1113 | } 1114 | } 1115 | } 1116 | 1117 | // Proxy username 1118 | { 1119 | if (isEditingExistingBool) { 1120 | if (tempLiquidAppConfig->contains(LQD_CFG_KEY_NAME_PROXY_USER_NAME) && proxyUsernameInput->text().size() == 0) { 1121 | tempLiquidAppConfig->remove(LQD_CFG_KEY_NAME_PROXY_USER_NAME); 1122 | } else { 1123 | if (tempLiquidAppConfig->value(LQD_CFG_KEY_NAME_PROXY_USER_NAME).toString().size() > 0 1124 | || proxyUsernameInput->text().size() > 0 1125 | ) { 1126 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_PROXY_USER_NAME, proxyUsernameInput->text()); 1127 | } 1128 | } 1129 | } else { 1130 | if (proxyUsernameInput->text().size() > 0) { 1131 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_PROXY_USER_NAME, proxyUsernameInput->text()); 1132 | } 1133 | } 1134 | } 1135 | 1136 | // Proxy password 1137 | { 1138 | if (isEditingExistingBool) { 1139 | if (tempLiquidAppConfig->contains(LQD_CFG_KEY_NAME_PROXY_USER_PASSWORD) && proxyPasswordInput->text().size() == 0) { 1140 | tempLiquidAppConfig->remove(LQD_CFG_KEY_NAME_PROXY_USER_PASSWORD); 1141 | } else { 1142 | if (tempLiquidAppConfig->value(LQD_CFG_KEY_NAME_PROXY_USER_PASSWORD).toString().size() > 0 1143 | || proxyPasswordInput->text().size() > 0 1144 | ) { 1145 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_PROXY_USER_PASSWORD, proxyPasswordInput->text()); 1146 | } 1147 | } 1148 | } else { 1149 | if (proxyPasswordInput->text().size() > 0) { 1150 | tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_PROXY_USER_PASSWORD, proxyPasswordInput->text()); 1151 | } 1152 | } 1153 | } 1154 | } 1155 | 1156 | tempLiquidAppConfig->sync(); 1157 | 1158 | accept(); 1159 | } 1160 | 1161 | void LiquidAppConfigDialog::bindShortcuts(void) 1162 | { 1163 | // Connect keyboard shortcut that closes the dialog 1164 | quitAction = new QAction(); 1165 | quitAction->setShortcut(QKeySequence(tr(LQD_KBD_SEQ_QUIT))); 1166 | addAction(quitAction); 1167 | connect(quitAction, SIGNAL(triggered()), this, SLOT(close())); 1168 | } 1169 | 1170 | bool LiquidAppConfigDialog::isPlanningToRun(void) 1171 | { 1172 | return !isEditingExistingBool && planningToRunCheckBox->isChecked(); 1173 | } 1174 | 1175 | QString LiquidAppConfigDialog::getName(void) 1176 | { 1177 | return nameInput->text(); 1178 | } 1179 | 1180 | QFrame* LiquidAppConfigDialog::separator(void) 1181 | { 1182 | QFrame* separatorFrame = new QFrame; 1183 | 1184 | separatorFrame->setFrameShape(QFrame::HLine); 1185 | separatorFrame->setFrameShadow(QFrame::Sunken); 1186 | 1187 | return separatorFrame; 1188 | } 1189 | 1190 | void LiquidAppConfigDialog::setPlanningToRun(const bool state) 1191 | { 1192 | if (!isEditingExistingBool) { 1193 | isPlanningToRunBool = state; 1194 | if (planningToRunCheckBox != Q_NULLPTR) { 1195 | planningToRunCheckBox->setChecked(state); 1196 | } 1197 | } 1198 | } 1199 | -------------------------------------------------------------------------------- /src/liquidappcookiejar.cpp: -------------------------------------------------------------------------------- 1 | #include "lqd.h" 2 | #include "liquidappcookiejar.hpp" 3 | #include "liquidappwindow.hpp" 4 | 5 | LiquidAppCookieJar::LiquidAppCookieJar(QObject *parent) : QNetworkCookieJar(parent) 6 | { 7 | liquidAppWindow = (LiquidAppWindow*)parent; 8 | liquidAppConfig = liquidAppWindow->liquidAppConfig; 9 | } 10 | 11 | LiquidAppCookieJar::~LiquidAppCookieJar(void) 12 | { 13 | } 14 | 15 | bool LiquidAppCookieJar::upsertCookie(const QNetworkCookie &cookie) 16 | { 17 | if (!liquidAppConfig->value(LQD_CFG_KEY_NAME_ALLOW_COOKIES).toBool()) { 18 | return false; 19 | } 20 | 21 | const bool isThirdParty = !validateCookie(cookie, liquidAppWindow->url()); 22 | if (isThirdParty && !liquidAppConfig->value(LQD_CFG_KEY_NAME_ALLOW_3RD_PARTY_COOKIES).toBool()) { 23 | return false; 24 | } 25 | 26 | foreach(QNetworkCookie existingCookie, allCookies()) { 27 | if (existingCookie.hasSameIdentifier(cookie)) { 28 | deleteCookie(existingCookie); 29 | } 30 | } 31 | 32 | const bool inserted = insertCookie(cookie); 33 | 34 | if (inserted) { 35 | save(); 36 | } 37 | 38 | return inserted; 39 | } 40 | 41 | bool LiquidAppCookieJar::removeCookie(const QNetworkCookie &cookie) 42 | { 43 | if (!liquidAppConfig->value(LQD_CFG_KEY_NAME_ALLOW_COOKIES).toBool()) { 44 | return false; 45 | } 46 | 47 | const bool isThirdParty = !validateCookie(cookie, liquidAppWindow->url()); 48 | if (isThirdParty && !liquidAppConfig->value(LQD_CFG_KEY_NAME_ALLOW_3RD_PARTY_COOKIES).toBool()) { 49 | return false; 50 | } 51 | 52 | const bool deleted = deleteCookie(cookie); 53 | 54 | if (deleted) { 55 | save(); 56 | } 57 | 58 | return deleted; 59 | } 60 | 61 | void LiquidAppCookieJar::restoreCookies(QWebEngineCookieStore *cookieStore) { 62 | if (liquidAppConfig->value(LQD_CFG_KEY_NAME_ALLOW_COOKIES).toBool()) { 63 | liquidAppConfig->beginGroup(LQD_CFG_GROUP_NAME_COOKIES); 64 | foreach(QString cookieId, liquidAppConfig->allKeys()) { 65 | const QByteArray rawCookie = liquidAppConfig->value(cookieId).toByteArray(); 66 | const QList cookies = QNetworkCookie::parseCookies(rawCookie); 67 | if (cookies.size() > 0) { 68 | QNetworkCookie cookie = cookies[0]; 69 | 70 | // Construct origin URL based on cookie data 71 | QString scheme("http"); 72 | if (cookie.isSecure()) { 73 | scheme += "s"; 74 | } 75 | QString domain(cookie.domain()); 76 | while (domain.startsWith(".")) { 77 | domain = domain.right(domain.size() - 1); 78 | } 79 | QUrl url(scheme + "://" + domain + cookie.path()); 80 | 81 | // Avoid prepending leading dot (https://bugreports.qt.io/browse/QTBUG-64732) 82 | if (!cookie.domain().startsWith(".")) { 83 | cookie.setDomain(""); 84 | } 85 | cookieStore->setCookie(cookie, url); 86 | } 87 | } 88 | liquidAppConfig->endGroup(); 89 | } 90 | } 91 | 92 | void LiquidAppCookieJar::save(void) 93 | { 94 | if (!liquidAppConfig->value(LQD_CFG_KEY_NAME_ALLOW_COOKIES).toBool()) { 95 | return; 96 | } 97 | 98 | liquidAppConfig->beginGroup(LQD_CFG_GROUP_NAME_COOKIES); 99 | // Remove all cookies 100 | foreach(QString cookieName, liquidAppConfig->allKeys()) { 101 | liquidAppConfig->remove(cookieName); 102 | } 103 | // Save all cookies 104 | foreach(QNetworkCookie cookie, allCookies()) { 105 | QByteArray cookieId = QByteArray(cookie.domain().toLatin1() + 106 | "_" + cookie.path().toLatin1() + 107 | "_" + cookie.name()); 108 | QString rawCookie = QString(cookie.toRawForm(QNetworkCookie::Full)); 109 | // TODO: save Session cookies? 110 | liquidAppConfig->setValue(cookieId, rawCookie); 111 | } 112 | liquidAppConfig->endGroup(); 113 | liquidAppConfig->sync(); 114 | } 115 | -------------------------------------------------------------------------------- /src/liquidappwebpage.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "liquidappwebpage.hpp" 10 | #include "lqd.h" 11 | 12 | LiquidAppWebPage::LiquidAppWebPage(QWebEngineProfile* profile, LiquidAppWindow* parent) : QWebEnginePage(profile, parent) 13 | { 14 | liquidAppWindow = parent; 15 | 16 | // Set this profile's web settings to default 17 | setWebSettingsToDefault(profile->settings()); 18 | 19 | // Allow page-level full-screen requests to happen 20 | connect(this, &QWebEnginePage::fullScreenRequested, this, [](QWebEngineFullScreenRequest request) { 21 | // This doesn't let JS maximize the OS window, just ensures that this action is always allowed to happen. 22 | // We basically trick web apps into thinking that they went full-screen, while never really allowing them to (they instead go "full-window"). 23 | request.accept(); 24 | }); 25 | 26 | connect(this, &QWebEnginePage::authenticationRequired, this, &LiquidAppWebPage::authenticationRequired); 27 | } 28 | 29 | void LiquidAppWebPage::addAllowedDomain(const QString domain) { 30 | // TODO: check if already there 31 | allowedDomainsList->append(domain); 32 | } 33 | 34 | void LiquidAppWebPage::addAllowedDomains(const QStringList domainsList) { 35 | allowedDomainsList->append(domainsList); 36 | // TODO: remove duplicates from allowedDomainsList 37 | } 38 | 39 | bool LiquidAppWebPage::acceptNavigationRequest(const QUrl& reqUrl, const QWebEnginePage::NavigationType navReqType, const bool isMainFrame) 40 | { 41 | const bool isDomainAllowed = allowedDomainsList->contains(reqUrl.host()); 42 | const bool isKeyModifierActive = QGuiApplication::keyboardModifiers().testFlag(Qt::ControlModifier); 43 | 44 | // Top-level window 45 | switch (navReqType) { 46 | // Open external websites using system's default browser 47 | case QWebEnginePage::NavigationTypeLinkClicked: 48 | // QWebEnginePage::NavigationTypeLinkClicked is the same type for both JS-induced clicks and actual physical clicks made by the user (go figure...) 49 | // isMainFrame is the only thing that indicates that it was the user who clicked the link, not some JS code (e.g. to redirect / pop some window up) 50 | if (isMainFrame && (!isDomainAllowed || isKeyModifierActive)) { 51 | QDesktopServices::openUrl(reqUrl); 52 | liquidAppWindow->setForgiveNextPageLoadError(true); 53 | return false; 54 | } 55 | break; 56 | 57 | #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) 58 | // Prevent redirects 59 | case QWebEnginePage::NavigationTypeRedirect: 60 | #endif 61 | // Prevent form submissions to other hosts 62 | case QWebEnginePage::NavigationTypeFormSubmitted: 63 | if (!isDomainAllowed) { 64 | liquidAppWindow->setForgiveNextPageLoadError(true); 65 | return false; 66 | } 67 | break; 68 | 69 | default:; 70 | } 71 | 72 | return true; 73 | } 74 | 75 | void LiquidAppWebPage::authenticationRequired(const QUrl& requestUrl, QAuthenticator* authenticator) 76 | { 77 | dialogWidget = new QDialog(liquidAppWindow, Qt::FramelessWindowHint); 78 | dialogWidget->setAttribute(Qt::WA_DeleteOnClose); 79 | dialogWidget->setObjectName("liquidAppHttpBasicAuthDialogPrompt"); 80 | dialogWidget->setWindowModality(Qt::ApplicationModal); 81 | 82 | QVBoxLayout* httpBasicAuthPromptDialogLayout = new QVBoxLayout(dialogWidget); 83 | dialogWidget->setLayout(httpBasicAuthPromptDialogLayout); 84 | 85 | httpBasicAuthPromptDialogLayout->addWidget(new QLabel(requestUrl.host(), dialogWidget)); 86 | 87 | QLineEdit* httpBasicAuthPromptDialogUsernameTextInput = new QLineEdit("", dialogWidget); 88 | httpBasicAuthPromptDialogUsernameTextInput->setPlaceholderText(tr("Username")); 89 | httpBasicAuthPromptDialogLayout->addWidget(httpBasicAuthPromptDialogUsernameTextInput); 90 | 91 | QLineEdit* httpBasicAuthPromptDialogPasswordTextInput = new QLineEdit("", dialogWidget); 92 | httpBasicAuthPromptDialogPasswordTextInput->setPlaceholderText(tr("Password")); 93 | httpBasicAuthPromptDialogLayout->addWidget(httpBasicAuthPromptDialogPasswordTextInput); 94 | 95 | QPushButton* dialogPromptButton = new QPushButton(tr("Sign in"), dialogWidget); 96 | httpBasicAuthPromptDialogLayout->addWidget(dialogPromptButton); 97 | 98 | connect(dialogPromptButton, &QPushButton::clicked, dialogWidget, &QDialog::accept); 99 | 100 | if (dialogWidget->exec() == QDialog::Accepted) { 101 | authenticator->setUser(httpBasicAuthPromptDialogUsernameTextInput->text()); 102 | authenticator->setPassword(httpBasicAuthPromptDialogPasswordTextInput->text()); 103 | } else { 104 | *authenticator = QAuthenticator(); 105 | } 106 | 107 | dialogWidget = Q_NULLPTR; 108 | } 109 | 110 | bool LiquidAppWebPage::certificateError(const QWebEngineCertificateError& error) 111 | { 112 | Q_UNUSED(error); 113 | 114 | emit liquidAppWindow->certificateError(); 115 | 116 | return true; 117 | } 118 | 119 | void LiquidAppWebPage::closeJsDialog() 120 | { 121 | if (dialogWidget != Q_NULLPTR) { 122 | dialogWidget->reject(); 123 | } 124 | } 125 | 126 | void LiquidAppWebPage::javaScriptAlert(const QUrl& securityOrigin, const QString& msg) 127 | { 128 | Q_UNUSED(securityOrigin); 129 | 130 | dialogWidget = new QDialog(liquidAppWindow, Qt::FramelessWindowHint); 131 | dialogWidget->setAttribute(Qt::WA_DeleteOnClose); 132 | dialogWidget->setObjectName("liquidAppJsDialogAlert"); 133 | dialogWidget->setWindowModality(Qt::ApplicationModal); 134 | 135 | QVBoxLayout* jsAlertDialogLayout = new QVBoxLayout(dialogWidget); 136 | 137 | dialogWidget->setLayout(jsAlertDialogLayout); 138 | 139 | jsAlertDialogLayout->addWidget(new QLabel(msg)); 140 | 141 | dialogWidget->exec(); 142 | dialogWidget = Q_NULLPTR; 143 | } 144 | 145 | bool LiquidAppWebPage::javaScriptConfirm(const QUrl& securityOrigin, const QString& msg) 146 | { 147 | Q_UNUSED(securityOrigin); 148 | 149 | dialogWidget = new QDialog(liquidAppWindow, Qt::FramelessWindowHint); 150 | dialogWidget->setAttribute(Qt::WA_DeleteOnClose); 151 | dialogWidget->setObjectName("liquidAppJsDialogConfirm"); 152 | dialogWidget->setWindowModality(Qt::ApplicationModal); 153 | 154 | QVBoxLayout* jsConfirmDialogLayout = new QVBoxLayout(dialogWidget); 155 | 156 | dialogWidget->setLayout(jsConfirmDialogLayout); 157 | 158 | jsConfirmDialogLayout->addWidget(new QLabel(msg, dialogWidget)); 159 | 160 | QPushButton* jsConfirmDialogButton = new QPushButton(tr("Confirm"), dialogWidget); 161 | jsConfirmDialogLayout->addWidget(jsConfirmDialogButton); 162 | 163 | connect(jsConfirmDialogButton, &QPushButton::clicked, dialogWidget, &QDialog::accept); 164 | 165 | if (dialogWidget->exec() == QDialog::Accepted) { 166 | dialogWidget = Q_NULLPTR; 167 | return true; 168 | } 169 | 170 | dialogWidget = Q_NULLPTR; 171 | return false; 172 | } 173 | 174 | bool LiquidAppWebPage::javaScriptPrompt(const QUrl& securityOrigin, const QString& msg, const QString& defaultValue, QString* result) 175 | { 176 | Q_UNUSED(securityOrigin); 177 | 178 | dialogWidget = new QDialog(liquidAppWindow, Qt::FramelessWindowHint); 179 | dialogWidget->setAttribute(Qt::WA_DeleteOnClose); 180 | dialogWidget->setObjectName("liquidAppJsDialogPrompt"); 181 | dialogWidget->setWindowModality(Qt::ApplicationModal); 182 | 183 | QHBoxLayout* jsPromptDialogLayout = new QHBoxLayout(dialogWidget); 184 | 185 | dialogWidget->setLayout(jsPromptDialogLayout); 186 | 187 | jsPromptDialogLayout->addWidget(new QLabel(msg, dialogWidget)); 188 | 189 | QLineEdit* jsPromptDialogTextInput = new QLineEdit(defaultValue, dialogWidget); 190 | jsPromptDialogTextInput->setPlaceholderText(tr("Value")); 191 | jsPromptDialogLayout->addWidget(jsPromptDialogTextInput); 192 | 193 | QPushButton* dialogPromptButton = new QPushButton(tr("Submit"), dialogWidget); 194 | jsPromptDialogLayout->addWidget(dialogPromptButton); 195 | 196 | connect(dialogPromptButton, &QPushButton::clicked, dialogWidget, &QDialog::accept); 197 | 198 | if (dialogWidget->exec() == QDialog::Accepted) { 199 | *result = jsPromptDialogTextInput->text(); 200 | dialogWidget = Q_NULLPTR; 201 | return true; 202 | } 203 | 204 | dialogWidget = Q_NULLPTR; 205 | return false; 206 | } 207 | 208 | void LiquidAppWebPage::setWebSettingsToDefault(QWebEngineSettings* webSettings) 209 | { 210 | // Default starting web settings for all Liquid apps 211 | webSettings->setAttribute(QWebEngineSettings::AutoLoadImages, true); 212 | #if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) 213 | webSettings->setAttribute(QWebEngineSettings::DnsPrefetchEnabled, false); 214 | #endif 215 | webSettings->setAttribute(QWebEngineSettings::FullScreenSupportEnabled, true); 216 | webSettings->setAttribute(QWebEngineSettings::HyperlinkAuditingEnabled, false); 217 | webSettings->setAttribute(QWebEngineSettings::JavascriptCanAccessClipboard, false); 218 | webSettings->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, false); 219 | #if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0) 220 | webSettings->setAttribute(QWebEngineSettings::JavascriptCanPaste, false); 221 | #endif 222 | webSettings->setAttribute(QWebEngineSettings::JavascriptEnabled, false); 223 | webSettings->setAttribute(QWebEngineSettings::LocalStorageEnabled, true); 224 | #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) 225 | webSettings->setAttribute(QWebEngineSettings::PdfViewerEnabled, false); 226 | #endif 227 | webSettings->setAttribute(QWebEngineSettings::PluginsEnabled, false); 228 | webSettings->setAttribute(QWebEngineSettings::ScrollAnimatorEnabled, true); 229 | #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) 230 | webSettings->setAttribute(QWebEngineSettings::ShowScrollBars, true); 231 | #endif 232 | } 233 | -------------------------------------------------------------------------------- /src/liquidappwindow.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "liquid.hpp" 15 | #include "liquidappcookiejar.hpp" 16 | #include "liquidappwebpage.hpp" 17 | #include "liquidappwindow.hpp" 18 | #include "lqd.h" 19 | #ifdef Q_OS_MAC 20 | #include "SetWindowBackgroundColor.h" 21 | #endif 22 | 23 | LiquidAppWindow::LiquidAppWindow(const QString* name) : QWebEngineView() 24 | { 25 | // Prevent window from getting way too tiny 26 | setMinimumSize(LQD_APP_WIN_MIN_SIZE_W, LQD_APP_WIN_MIN_SIZE_H); 27 | 28 | // Tune web engine 29 | qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--enable-features=AutoplayIgnoreWebAudio"); 30 | qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--enable-accelerated-video-decode"); 31 | qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--enable-gpu-compositing"); 32 | qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--enable-gpu-rasterization"); 33 | qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--enable-smooth-scrolling"); 34 | qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--ignore-gpu-blocklist"); 35 | qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--num-raster-threads=4"); 36 | qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--use-fake-ui-for-media-stream"); 37 | 38 | #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) 39 | // Ensure dark mode is enabled on the web page in case the system theme is dark 40 | if (Liquid::detectDarkMode()) { 41 | qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--force-dark-mode"); 42 | qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--blink-settings=forceDarkModeEnabled=true"); 43 | qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--blink-settings=darkMode=4"); 44 | qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--blink-settings=darkModeEnabled=true"); 45 | } 46 | #endif 47 | 48 | // Set default icon 49 | #if !defined(Q_OS_LINUX) // This doesn't work on X11 50 | setWindowIcon(QIcon(":/images/" PROG_NAME ".svg")); 51 | #endif 52 | 53 | // Disable default QWebEngineView's context menu 54 | setContextMenuPolicy(Qt::PreventContextMenu); 55 | 56 | liquidAppName = (QString*)name; 57 | 58 | liquidAppConfig = new QSettings(QSettings::IniFormat, 59 | QSettings::UserScope, 60 | QString(PROG_NAME "%1" LQD_APPS_DIR_NAME).arg(QDir::separator()), 61 | *name, 62 | Q_NULLPTR); 63 | 64 | // These default settings affect everything (including sub-frames) 65 | #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 66 | LiquidAppWebPage::setWebSettingsToDefault(QWebEngineSettings::globalSettings()); 67 | #endif 68 | 69 | liquidAppWebProfile = new QWebEngineProfile(QString(), this); 70 | liquidAppWebProfile->setHttpCacheType(QWebEngineProfile::MemoryHttpCache); 71 | liquidAppWebProfile->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies); 72 | 73 | if (!liquidAppWebProfile->isOffTheRecord()) { 74 | qDebug().noquote() << "Web profile is not off-the-record!"; 75 | // Privacy is paramount for this program, separate apps need to be completely siloed 76 | exit(EXIT_FAILURE); 77 | } 78 | 79 | Liquid::applyQtStyleSheets(this); 80 | 81 | liquidAppWebPage = new LiquidAppWebPage(liquidAppWebProfile, this); 82 | setPage(liquidAppWebPage); 83 | 84 | liquidAppWebSettings = liquidAppWebPage->settings(); 85 | 86 | // Set default window title 87 | liquidAppWindowTitle = *liquidAppName; 88 | 89 | updateWindowTitle(*liquidAppName); 90 | 91 | // Pre-fill all possible zoom factors to snap desired zoom level to 92 | { 93 | for (qreal z = 1.0 - LQD_ZOOM_LVL_STEP_FINE; z >= LQD_ZOOM_LVL_MIN - LQD_ZOOM_LVL_STEP_FINE && z > 0; z -= LQD_ZOOM_LVL_STEP_FINE) { 94 | if (z >= LQD_ZOOM_LVL_MIN) { 95 | zoomFactors.prepend(z); 96 | } else { 97 | zoomFactors.prepend(LQD_ZOOM_LVL_MIN); 98 | } 99 | } 100 | 101 | if (LQD_ZOOM_LVL_MIN <= 1 && LQD_ZOOM_LVL_MAX >= 1) { 102 | zoomFactors.append(1.0); 103 | } 104 | 105 | for (qreal z = 1.0 + LQD_ZOOM_LVL_STEP_FINE; z <= LQD_ZOOM_LVL_MAX + LQD_ZOOM_LVL_STEP_FINE; z += LQD_ZOOM_LVL_STEP_FINE) { 106 | if (z <= LQD_ZOOM_LVL_MAX) { 107 | zoomFactors.append(z); 108 | } else { 109 | zoomFactors.append(LQD_ZOOM_LVL_MAX); 110 | } 111 | } 112 | } 113 | 114 | const QUrl startingUrl(liquidAppConfig->value(LQD_CFG_KEY_NAME_URL).toString()); 115 | 116 | if (!startingUrl.isValid()) { 117 | qDebug().noquote() << "Invalid Liquid application URL:" << startingUrl; 118 | return; 119 | } 120 | 121 | liquidAppWebPage->addAllowedDomain(startingUrl.host()); 122 | 123 | loadLiquidAppConfig(); 124 | 125 | // Reveal Liquid app's window and bring it to front 126 | show(); 127 | #ifdef Q_OS_MAC 128 | // This can only have effect after the window is revealed 129 | SetWindowBackgroundColor(effectiveWinId(), 130 | page()->backgroundColor().redF(), 131 | page()->backgroundColor().greenF(), 132 | page()->backgroundColor().blueF(), 133 | page()->backgroundColor().alphaF()); 134 | #endif 135 | raise(); 136 | activateWindow(); 137 | 138 | // Connect keyboard shortcuts 139 | bindKeyboardShortcuts(); 140 | 141 | // Initialize context menu 142 | setupContextMenu(); 143 | 144 | // Trigger window title update if changes 145 | connect(this, &QWebEngineView::titleChanged, this, &LiquidAppWindow::updateWindowTitle); 146 | 147 | // Update Liquid app's icon using the one provided by the website 148 | connect(liquidAppWebPage, &QWebEnginePage::iconChanged, this, &LiquidAppWindow::onIconChanged); 149 | 150 | // Catch loading's start 151 | connect(liquidAppWebPage, &QWebEnginePage::loadStarted, this, &LiquidAppWindow::loadStarted); 152 | 153 | // Catch loading's end 154 | connect(liquidAppWebPage, &QWebEnginePage::loadFinished, this, &LiquidAppWindow::loadFinished); 155 | 156 | // Load Liquid app's starting URL 157 | load(startingUrl); 158 | } 159 | 160 | LiquidAppWindow::~LiquidAppWindow(void) 161 | { 162 | saveLiquidAppConfig(); 163 | 164 | delete liquidAppWebPage; 165 | delete liquidAppWebProfile; 166 | } 167 | 168 | void LiquidAppWindow::attemptToSetZoomFactorTo(const qreal desiredZoomFactor) 169 | { 170 | int i = 0; 171 | const int ilen = zoomFactors.size(); 172 | 173 | for (; i < ilen; i++) { 174 | if (qFuzzyCompare(zoomFactors[i], desiredZoomFactor)) { 175 | setZoomFactor(zoomFactors[i]); 176 | return; 177 | } 178 | } 179 | 180 | // Attempt to determine closest zoom level to snap to 181 | for (i = 0; i < ilen; i++) { 182 | if ((i == 0 || zoomFactors[i - 1] < desiredZoomFactor) && (i == ilen - 1 || zoomFactors[i + 1] > desiredZoomFactor)) { 183 | setZoomFactor(zoomFactors[i]); 184 | return; 185 | } 186 | } 187 | } 188 | 189 | void LiquidAppWindow::bindKeyboardShortcuts(void) 190 | { 191 | // Connect window geometry lock shortcut 192 | toggleGeometryLockAction = new QAction; 193 | toggleGeometryLockAction->setShortcut(QKeySequence(tr(LQD_KBD_SEQ_TOGGLE_WIN_GEOM_LOCK))); 194 | addAction(toggleGeometryLockAction); 195 | connect(toggleGeometryLockAction, SIGNAL(triggered()), this, SLOT(toggleWindowGeometryLock())); 196 | 197 | // Connect "mute audio" shortcut 198 | muteAudioAction = new QAction; 199 | muteAudioAction->setShortcut(QKeySequence(tr(LQD_KBD_SEQ_MUTE_AUDIO))); 200 | addAction(muteAudioAction); 201 | connect(muteAudioAction, &QAction::triggered, this, [this](){ 202 | page()->setAudioMuted(!page()->isAudioMuted()); 203 | updateWindowTitle(title()); 204 | }); 205 | 206 | // Connect "go back" shortcut 207 | backAction = new QAction; 208 | backAction->setShortcut(QKeySequence(tr(LQD_KBD_SEQ_GO_BACK))); 209 | addAction(backAction); 210 | connect(backAction, SIGNAL(triggered()), this, SLOT(back())); 211 | 212 | // Connect "go back" shortcut (backspace) 213 | backAction2 = new QAction; 214 | backAction2->setShortcut(QKeySequence(tr(LQD_KBD_SEQ_GO_BACK_2))); 215 | addAction(backAction2); 216 | connect(backAction2, SIGNAL(triggered()), this, SLOT(back())); 217 | 218 | // Connect "go forward" shortcut 219 | forwardAction = new QAction; 220 | forwardAction->setShortcut(QKeySequence(tr(LQD_KBD_SEQ_GO_FORWARD))); 221 | addAction(forwardAction); 222 | connect(forwardAction, SIGNAL(triggered()), this, SLOT(forward())); 223 | 224 | // Connect "reload" shortcut 225 | reloadAction = new QAction; 226 | reloadAction->setShortcut(QKeySequence(tr(LQD_KBD_SEQ_RELOAD))); 227 | addAction(reloadAction); 228 | connect(reloadAction, SIGNAL(triggered()), this, SLOT(reload())); 229 | // Connect "alternative reload" shortcut (there can be only one QKeySequence per QAction) 230 | reloadAction2 = new QAction; 231 | reloadAction2->setShortcut(QKeySequence(tr(LQD_KBD_SEQ_RELOAD_2))); 232 | addAction(reloadAction2); 233 | connect(reloadAction2, SIGNAL(triggered()), this, SLOT(reload())); 234 | 235 | // Connect "hard reload" shortcut 236 | hardReloadAction = new QAction; 237 | hardReloadAction->setShortcut(QKeySequence(tr(LQD_KBD_SEQ_HARD_RELOAD))); 238 | addAction(hardReloadAction); 239 | connect(hardReloadAction, SIGNAL(triggered()), this, SLOT(hardReload())); 240 | 241 | // Connect "toggle full-screen" shortcut 242 | toggleFullScreenModeAction = new QAction; 243 | toggleFullScreenModeAction->setShortcut(QKeySequence(tr(LQD_KBD_SEQ_TOGGLE_FS_MODE))); 244 | addAction(toggleFullScreenModeAction); 245 | connect(toggleFullScreenModeAction, SIGNAL(triggered()), this, SLOT(toggleFullScreenMode())); 246 | // Connect "alternative toggle full-screen" shortcut (there can be only one QKeySequence per QAction) 247 | toggleFullScreenModeAction2 = new QAction; 248 | toggleFullScreenModeAction2->setShortcut(QKeySequence(tr(LQD_KBD_SEQ_TOGGLE_FS_MODE_2))); 249 | addAction(toggleFullScreenModeAction2); 250 | connect(toggleFullScreenModeAction2, SIGNAL(triggered()), this, SLOT(toggleFullScreenMode())); 251 | 252 | // Connect "stop loading" / "exit full-screen mode" shortcut 253 | stopLoadingOrExitFullScreenModeAction = new QAction; 254 | stopLoadingOrExitFullScreenModeAction->setShortcut(QKeySequence(tr(LQD_KBD_SEQ_STOP_OR_EXIT_FS_MODE))); 255 | addAction(stopLoadingOrExitFullScreenModeAction); 256 | connect(stopLoadingOrExitFullScreenModeAction, SIGNAL(triggered()), this, SLOT(stopLoadingOrExitFullScreenMode())); 257 | 258 | // Connect "zoom in" shortcut 259 | zoomInAction = new QAction; 260 | zoomInAction->setShortcut(QKeySequence(tr(LQD_KBD_SEQ_ZOOM_LVL_INC))); 261 | addAction(zoomInAction); 262 | connect(zoomInAction, &QAction::triggered, this, [this](){ 263 | zoomIn(false); 264 | }); 265 | 266 | // Connect "zoom out" shortcut 267 | zoomOutAction = new QAction; 268 | zoomOutAction->setShortcut(QKeySequence(tr(LQD_KBD_SEQ_ZOOM_LVL_DEC))); 269 | addAction(zoomOutAction); 270 | connect(zoomOutAction, &QAction::triggered, this, [this](){ 271 | zoomOut(false); 272 | }); 273 | 274 | // Connect "fine zoom in" shortcut 275 | zoomInFineAction = new QAction; 276 | zoomInFineAction->setShortcut(QKeySequence(tr(LQD_KBD_SEQ_ZOOM_LVL_INC_FINE))); 277 | addAction(zoomInFineAction); 278 | connect(zoomInFineAction, &QAction::triggered, this, [this](){ 279 | zoomIn(true); 280 | }); 281 | 282 | // Connect "fine zoom out" shortcut 283 | zoomOutFineAction = new QAction; 284 | zoomOutFineAction->setShortcut(QKeySequence(tr(LQD_KBD_SEQ_ZOOM_LVL_DEC_FINE))); 285 | addAction(zoomOutFineAction); 286 | connect(zoomOutFineAction, &QAction::triggered, this, [this](){ 287 | zoomOut(true); 288 | }); 289 | 290 | // Connect "reset zoom" shortcut 291 | zoomResetAction = new QAction; 292 | zoomResetAction->setShortcut(QKeySequence(tr(LQD_KBD_SEQ_ZOOM_LVL_RESET))); 293 | addAction(zoomResetAction); 294 | connect(zoomResetAction, SIGNAL(triggered()), this, SLOT(zoomReset())); 295 | 296 | // Connect "alternative reset zoom" shortcut 297 | zoomResetAltAction = new QAction; 298 | zoomResetAltAction->setShortcut(QKeySequence(tr(LQD_KBD_SEQ_ZOOM_LVL_RESET_2))); 299 | addAction(zoomResetAltAction); 300 | connect(zoomResetAltAction, SIGNAL(triggered()), this, SLOT(zoomReset())); 301 | 302 | // Connect "exit" shortcut 303 | quitAction = new QAction; 304 | quitAction->setShortcut(QKeySequence(tr(LQD_KBD_SEQ_QUIT))); 305 | addAction(quitAction); 306 | connect(quitAction, SIGNAL(triggered()), this, SLOT(close())); 307 | 308 | // Connect "alternative exit" shortcut 309 | quitAction2 = new QAction; 310 | quitAction2->setShortcut(QKeySequence(tr(LQD_KBD_SEQ_QUIT_2))); 311 | addAction(quitAction2); 312 | connect(quitAction2, SIGNAL(triggered()), this, SLOT(close())); 313 | 314 | // Connect "take snapshot" shortcut 315 | takeSnapshotAction = new QAction; 316 | takeSnapshotAction->setShortcut(QKeySequence(tr(LQD_KBD_SEQ_TAKE_SNAPSHOT))); 317 | addAction(takeSnapshotAction); 318 | connect(takeSnapshotAction, SIGNAL(triggered()), this, SLOT(takeSnapshotSlot())); 319 | 320 | // Connect "take full-page snapshot" shortcut 321 | takeSnapshotFullPageAction = new QAction; 322 | takeSnapshotFullPageAction->setShortcut(QKeySequence(tr(LQD_KBD_SEQ_TAKE_SNAPSHOT_FULL))); 323 | addAction(takeSnapshotFullPageAction); 324 | connect(takeSnapshotFullPageAction, SIGNAL(triggered()), this, SLOT(takeSnapshotFullPageSlot())); 325 | 326 | // Connect "save page" shortcut 327 | savePageAction = new QAction; 328 | savePageAction->setShortcut(QKeySequence(tr(LQD_KBD_SEQ_SAVE_PAGE))); 329 | addAction(savePageAction); 330 | connect(savePageAction, &QAction::triggered, this, [this](){ 331 | // TODO: make it save the page along with its custom CSS/JS, as one monolithic HTML file (instead of MHT) 332 | page()->save(QString("%1 (%2).mhtml").arg(*liquidAppName).arg(Liquid::getReadableDateTimeString())); 333 | }); 334 | 335 | // Make it possible to intercept zoom events 336 | QApplication::instance()->installEventFilter(this); 337 | } 338 | 339 | void LiquidAppWindow::certificateError(void) 340 | { 341 | const bool updateTitle = !pageHasCertificateError; 342 | 343 | pageHasCertificateError = true; 344 | 345 | if (updateTitle) { 346 | updateWindowTitle(title()); 347 | } 348 | } 349 | 350 | void LiquidAppWindow::closeEvent(QCloseEvent* event) 351 | { 352 | event->accept(); 353 | deleteLater(); 354 | } 355 | 356 | const QString LiquidAppWindow::colorToRgba(const QColor color) 357 | { 358 | return QString("rgba(%1, %2, %3, %4)") 359 | .arg(color.red()) 360 | .arg(color.green()) 361 | .arg(color.blue()) 362 | .arg(color.alphaF()); 363 | } 364 | 365 | void LiquidAppWindow::contextMenuEvent(QContextMenuEvent* event) 366 | { 367 | Q_UNUSED(event); 368 | 369 | contextMenuBackAction->setEnabled(history()->canGoBack()); 370 | contextMenuForwardAction->setEnabled(history()->canGoForward()); 371 | 372 | contextMenu->exec(QCursor::pos()); 373 | } 374 | 375 | bool LiquidAppWindow::eventFilter(QObject* watched, QEvent* event) 376 | { 377 | if (watched->parent() == this) { 378 | switch (event->type()) { 379 | case QEvent::Wheel: 380 | if (handleWheelEvent(static_cast<QWheelEvent*>(event))) { 381 | return true; 382 | } 383 | break; 384 | 385 | case QEvent::MouseButtonPress: 386 | liquidAppWebPage->closeJsDialog(); 387 | break; 388 | 389 | default: 390 | break; 391 | } 392 | } 393 | 394 | return QWebEngineView::eventFilter(watched, event); 395 | } 396 | 397 | void LiquidAppWindow::exitFullScreenMode(void) 398 | { 399 | // Exit from full-screen mode 400 | setWindowState(windowState() & ~Qt::WindowFullScreen); 401 | 402 | if (windowGeometryIsLocked) { 403 | // Pause here to wait for any kind of window resize animations to finish 404 | Liquid::sleep(200); 405 | 406 | setMinimumSize(width(), height()); 407 | setMaximumSize(width(), height()); 408 | } 409 | } 410 | 411 | bool LiquidAppWindow::handleWheelEvent(QWheelEvent *event) 412 | { 413 | const bool isCtrlActive = event->modifiers() & Qt::ControlModifier; 414 | const bool isShiftActive = event->modifiers() & Qt::ShiftModifier; 415 | 416 | if (isCtrlActive) { 417 | (event->inverted()) ? zoomIn(isShiftActive) : zoomOut(isShiftActive); 418 | event->accept(); 419 | return true; 420 | } 421 | 422 | return false; 423 | } 424 | 425 | void LiquidAppWindow::hardReload(void) 426 | { 427 | // TODO: if JS enabled, stop all currently running JS (destroy web workers, promises, etc) 428 | 429 | // Synchronously wipe all document contents (page's setContent() and setHtml() are aynchrnonous, can't use them here) 430 | const QString js = QString("(()=>{"\ 431 | "let e=document.firstElementChild;"\ 432 | "if(e){"\ 433 | "e.remove()"\ 434 | "}"\ 435 | "})();"); 436 | page()->runJavaScript(js, QWebEngineScript::ApplicationWorld); 437 | 438 | // Ensure that while this Liquid App is being reset, the window title remains to be set to this Liquid App's name 439 | // to mimic the same experience that happens when the user initially runs this Liquid app 440 | if (!liquidAppWindowTitleIsReadOnly) { 441 | liquidAppWindowTitle = *liquidAppName; 442 | 443 | const QString js = QString("(()=>{"\ 444 | "let e=document.createElement('title');"\ 445 | "e.innerText='%1';"\ 446 | "document.appendChild(e)"\ 447 | "})();").arg(liquidAppWindowTitle.replace("'", "\\'")); 448 | page()->runJavaScript(js, QWebEngineScript::ApplicationWorld); 449 | } 450 | 451 | updateWindowTitle(title()); 452 | 453 | // TODO: reset localStorage / Cookies in case they're disabled? 454 | 455 | // TODO: clear any type of cache, if possible 456 | 457 | QUrl url(liquidAppConfig->value(LQD_CFG_KEY_NAME_URL).toString(), QUrl::StrictMode); 458 | setUrl(url); 459 | } 460 | 461 | void LiquidAppWindow::loadFinished(bool ok) 462 | { 463 | pageIsLoading = false; 464 | 465 | if (ok) { 466 | pageHasError = false; 467 | } else { 468 | if (forgiveNextPageLoadError) { 469 | pageHasError = false; 470 | } else { 471 | pageHasError = true; 472 | } 473 | } 474 | 475 | // Unset forgiveNextPageLoadError 476 | if (forgiveNextPageLoadError) { 477 | forgiveNextPageLoadError = false; 478 | } 479 | 480 | updateWindowTitle(title()); 481 | } 482 | 483 | void LiquidAppWindow::loadLiquidAppConfig(void) 484 | { 485 | if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_TITLE)) { 486 | liquidAppWindowTitle = liquidAppConfig->value(LQD_CFG_KEY_NAME_TITLE).toString(); 487 | // Make sure the window title never gets changed 488 | liquidAppWindowTitleIsReadOnly = true; 489 | } 490 | 491 | // Apply network proxy configuration 492 | if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_USE_PROXY)) { 493 | proxy = new QNetworkProxy; 494 | 495 | if (liquidAppConfig->value(LQD_CFG_KEY_NAME_USE_PROXY, false).toBool()) { 496 | const bool isSocks = liquidAppConfig->value(LQD_CFG_KEY_NAME_PROXY_USE_SOCKS).toBool(); 497 | 498 | proxy->setType((isSocks) ? QNetworkProxy::Socks5Proxy : QNetworkProxy::HttpProxy); 499 | 500 | if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_PROXY_HOST)) { 501 | proxy->setHostName(liquidAppConfig->value(LQD_CFG_KEY_NAME_PROXY_HOST).toString()); 502 | } 503 | 504 | if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_PROXY_PORT)) { 505 | proxy->setPort(liquidAppConfig->value(LQD_CFG_KEY_NAME_PROXY_PORT).toInt()); 506 | } 507 | 508 | if (liquidAppConfig->value(LQD_CFG_KEY_NAME_PROXY_USE_AUTH, false).toBool()) { 509 | if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_PROXY_USER_NAME)) { 510 | proxy->setUser(liquidAppConfig->value(LQD_CFG_KEY_NAME_PROXY_USER_NAME).toString()); 511 | } 512 | 513 | if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_PROXY_USER_PASSWORD)) { 514 | proxy->setPassword(liquidAppConfig->value(LQD_CFG_KEY_NAME_PROXY_USER_PASSWORD).toString()); 515 | } 516 | } 517 | } else { 518 | proxy->setType(QNetworkProxy::NoProxy); 519 | } 520 | 521 | QNetworkProxy::setApplicationProxy(*proxy); 522 | } 523 | 524 | // Remove window manager's frame 525 | { 526 | if (liquidAppConfig->value(LQD_CFG_KEY_NAME_REMOVE_WINDOW_FRAME, false).toBool()) { 527 | setWindowFlags(windowFlags() | Qt::FramelessWindowHint); 528 | } 529 | } 530 | 531 | // Set the page's background color behind the document's body 532 | { 533 | if (liquidAppConfig->value(LQD_CFG_KEY_NAME_USE_CUSTOM_BG, false).toBool() && liquidAppConfig->contains(LQD_CFG_KEY_NAME_CUSTOM_BG_COLOR)) { 534 | const QColor backgroundColor = QColor(liquidAppConfig->value(LQD_CFG_KEY_NAME_CUSTOM_BG_COLOR).toString()); 535 | 536 | if (backgroundColor.alpha() < 255) { 537 | // Make window background transparent 538 | setAttribute(Qt::WA_TranslucentBackground, true); 539 | } 540 | 541 | page()->setBackgroundColor(backgroundColor); 542 | } else { 543 | page()->setBackgroundColor(LQD_DEFAULT_BG_COLOR); 544 | } 545 | } 546 | 547 | // Determine where this Liquid app is allowed to navigate, and what should be opened in external browser 548 | if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_ADDITIONAL_DOMAINS)) { 549 | liquidAppWebPage->addAllowedDomains( 550 | liquidAppConfig->value(LQD_CFG_KEY_NAME_ADDITIONAL_DOMAINS).toString().split(" ") 551 | ); 552 | } 553 | 554 | // Deal with Cookies 555 | { 556 | LiquidAppCookieJar *liquidAppCookieJar = new LiquidAppCookieJar(this); 557 | QWebEngineCookieStore *cookieStore = page()->profile()->cookieStore(); 558 | 559 | connect(cookieStore, &QWebEngineCookieStore::cookieAdded, liquidAppCookieJar, &LiquidAppCookieJar::upsertCookie); 560 | connect(cookieStore, &QWebEngineCookieStore::cookieRemoved, liquidAppCookieJar, &LiquidAppCookieJar::removeCookie); 561 | 562 | liquidAppCookieJar->restoreCookies(cookieStore); 563 | } 564 | 565 | // Restore window geometry 566 | if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_WIN_GEOM)) { 567 | restoreGeometry(QByteArray::fromHex( 568 | liquidAppConfig->value(LQD_CFG_KEY_NAME_WIN_GEOM).toByteArray() 569 | )); 570 | } else { 571 | const QRect currentScreenSize = QGuiApplication::primaryScreen()->availableGeometry(); 572 | const int currentScreenWidth = currentScreenSize.width(); 573 | const int currentScreenHeight = currentScreenSize.height(); 574 | setGeometry(currentScreenWidth / 4, currentScreenHeight / 4, currentScreenWidth / 2, currentScreenHeight / 2); 575 | } 576 | 577 | // Toggle JavaScript on if enabled in application config 578 | if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_ENABLE_JS)) { 579 | settings()->setAttribute( 580 | QWebEngineSettings::JavascriptEnabled, 581 | liquidAppConfig->value(LQD_CFG_KEY_NAME_ENABLE_JS).toBool() 582 | ); 583 | } 584 | 585 | #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) 586 | // Hide scrollbars 587 | if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_HIDE_SCROLLBARS)) { 588 | settings()->setAttribute( 589 | QWebEngineSettings::ShowScrollBars, 590 | !liquidAppConfig->value(LQD_CFG_KEY_NAME_HIDE_SCROLLBARS).toBool() 591 | ); 592 | } 593 | #endif 594 | 595 | // Mute audio if muted in application config 596 | if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_MUTE_AUDIO)) { 597 | page()->setAudioMuted(liquidAppConfig->value(LQD_CFG_KEY_NAME_MUTE_AUDIO).toBool()); 598 | } 599 | 600 | // Restore web view zoom level 601 | if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_ZOOM_LVL)) { 602 | attemptToSetZoomFactorTo(liquidAppConfig->value(LQD_CFG_KEY_NAME_ZOOM_LVL).toDouble()); 603 | 604 | // There's a bug in Qt, using QTimer seems to be the only solution 605 | QTimer::singleShot(1000, [&](){ 606 | if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_ZOOM_LVL)) { 607 | attemptToSetZoomFactorTo(liquidAppConfig->value(LQD_CFG_KEY_NAME_ZOOM_LVL).toDouble()); 608 | } 609 | }); 610 | } 611 | 612 | // Lock for the app's window's geometry 613 | if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_LOCK_WIN_GEOM)) { 614 | if (liquidAppConfig->value(LQD_CFG_KEY_NAME_LOCK_WIN_GEOM).toBool()) { 615 | toggleWindowGeometryLock(); 616 | windowGeometryIsLocked = true; 617 | } 618 | } 619 | 620 | // Custom user-agent string 621 | if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_USER_AGENT)) { 622 | liquidAppWebProfile->setHttpUserAgent(liquidAppConfig->value(LQD_CFG_KEY_NAME_USER_AGENT).toString()); 623 | } 624 | 625 | // Additional user-defined CSS (does't require JavaScript enabled in order to work) 626 | if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_ADDITIONAL_CSS)) { 627 | QString additionalCss = liquidAppConfig->value(LQD_CFG_KEY_NAME_ADDITIONAL_CSS).toString(); 628 | const QString js = QString("(()=>{"\ 629 | "const styleEl = document.createElement('style');"\ 630 | "const cssTextNode = document.createTextNode('%1');"\ 631 | "styleEl.appendChild(cssTextNode);"\ 632 | "document.head.appendChild(styleEl)"\ 633 | "})();").arg(additionalCss.replace("\n", " ").replace("'", "\\'")); 634 | QWebEngineScript script; 635 | script.setInjectionPoint(QWebEngineScript::DocumentReady); 636 | script.setRunsOnSubFrames(false); 637 | script.setSourceCode(js); 638 | script.setWorldId(QWebEngineScript::ApplicationWorld); 639 | liquidAppWebPage->scripts().insert(script); 640 | } 641 | 642 | // Additional user-defined JS (does't require JavaScript enabled in order to work) 643 | if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_ADDITIONAL_JS)) { 644 | QString js = liquidAppConfig->value(LQD_CFG_KEY_NAME_ADDITIONAL_JS).toString(); 645 | QWebEngineScript script; 646 | script.setInjectionPoint(QWebEngineScript::DocumentReady); 647 | script.setRunsOnSubFrames(false); 648 | script.setSourceCode(js); 649 | script.setWorldId(QWebEngineScript::ApplicationWorld); 650 | liquidAppWebPage->scripts().insert(script); 651 | } 652 | 653 | #if !defined(Q_OS_LINUX) // This doesn't work on X11 654 | // Set window icon 655 | if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_ICON)) { 656 | QPixmap pixmap; 657 | pixmap.loadFromData(QByteArray::fromBase64(liquidAppConfig->value(LQD_CFG_KEY_NAME_ICON).toByteArray().remove(0, 22)), "PNG"); 658 | window()->setWindowIcon(QIcon(pixmap)); 659 | } 660 | #endif 661 | } 662 | 663 | void LiquidAppWindow::loadStarted(void) 664 | { 665 | pageIsLoading = true; 666 | pageHasCertificateError = false; 667 | pageHasError = false; 668 | 669 | updateWindowTitle(title()); 670 | } 671 | 672 | void LiquidAppWindow::moveEvent(QMoveEvent *event) 673 | { 674 | // Remember window position 675 | liquidAppWindowGeometry = saveGeometry(); 676 | 677 | QWebEngineView::moveEvent(event); 678 | } 679 | 680 | void LiquidAppWindow::onIconChanged(QIcon icon) 681 | { 682 | // Set window icon 683 | setWindowIcon(icon); 684 | 685 | iconToSave = icon; 686 | } 687 | 688 | void LiquidAppWindow::resizeEvent(QResizeEvent* event) 689 | { 690 | // Remember window size (unless in full-screen mode) 691 | if (event->spontaneous() && !isFullScreen()) { 692 | // Pause here to wait for any kind of window resize animations to finish 693 | Liquid::sleep(200); 694 | 695 | liquidAppWindowGeometry = saveGeometry(); 696 | } 697 | 698 | QWebEngineView::resizeEvent(event); 699 | } 700 | 701 | void LiquidAppWindow::saveLiquidAppConfig(void) 702 | { 703 | if (qFuzzyCompare(zoomFactor(), 1.0)) { 704 | if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_ZOOM_LVL)) { 705 | liquidAppConfig->remove(LQD_CFG_KEY_NAME_ZOOM_LVL); 706 | } 707 | } else { 708 | liquidAppConfig->setValue(LQD_CFG_KEY_NAME_ZOOM_LVL, zoomFactor()); 709 | } 710 | 711 | if (page()->isAudioMuted()) { 712 | liquidAppConfig->setValue(LQD_CFG_KEY_NAME_MUTE_AUDIO, true); 713 | } else { 714 | if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_MUTE_AUDIO)) { 715 | liquidAppConfig->remove(LQD_CFG_KEY_NAME_MUTE_AUDIO); 716 | } 717 | } 718 | 719 | // Save icon data as base64 string 720 | if (!iconToSave.isNull()) { 721 | QBuffer buffer; 722 | buffer.open(QIODevice::WriteOnly); 723 | iconToSave.pixmap(iconToSave.availableSizes()[0]).save(&buffer, "PNG"); 724 | liquidAppConfig->setValue(LQD_CFG_KEY_NAME_ICON, QString("data:image/png;base64,%1").arg(QString(buffer.data().toBase64()))); 725 | } 726 | 727 | if (!isFullScreen()) { 728 | liquidAppConfig->setValue(LQD_CFG_KEY_NAME_WIN_GEOM, QString(liquidAppWindowGeometry.toHex())); 729 | } 730 | 731 | if (windowGeometryIsLocked) { 732 | liquidAppConfig->setValue(LQD_CFG_KEY_NAME_LOCK_WIN_GEOM, true); 733 | } else { 734 | if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_LOCK_WIN_GEOM)) { 735 | liquidAppConfig->remove(LQD_CFG_KEY_NAME_LOCK_WIN_GEOM); 736 | } 737 | } 738 | 739 | liquidAppConfig->sync(); 740 | } 741 | 742 | void LiquidAppWindow::setupContextMenu(void) 743 | { 744 | contextMenu = new QMenu; 745 | 746 | contextMenuCopyUrlAction = new QAction(QIcon::fromTheme(QStringLiteral("internet-web-browser")), tr("Copy Current URL")); 747 | contextMenuReloadAction = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), tr("Refresh")); 748 | contextMenuBackAction = new QAction(QIcon::fromTheme(QStringLiteral("go-previous")), tr("Go Back")); 749 | contextMenuForwardAction = new QAction(QIcon::fromTheme(QStringLiteral("go-next")), tr("Go Forward")); 750 | contextMenuCloseAction = new QAction(QIcon::fromTheme(QStringLiteral("process-stop")), tr("Quit")); 751 | 752 | contextMenu->addAction(contextMenuCopyUrlAction); 753 | contextMenu->addAction(contextMenuReloadAction); 754 | contextMenu->addAction(contextMenuBackAction); 755 | contextMenu->addAction(contextMenuForwardAction); 756 | contextMenu->addAction(contextMenuCloseAction); 757 | 758 | connect(contextMenuCopyUrlAction, &QAction::triggered, this, [this](){ 759 | QApplication::clipboard()->setText(page()->url().toString()); 760 | }); 761 | connect(contextMenuReloadAction, &QAction::triggered, this, &QWebEngineView::reload); 762 | connect(contextMenuBackAction, &QAction::triggered, this, &QWebEngineView::back); 763 | connect(contextMenuForwardAction, &QAction::triggered, this, &QWebEngineView::forward); 764 | connect(contextMenuCloseAction, SIGNAL(triggered()), this, SLOT(close())); 765 | 766 | setContextMenuPolicy(Qt::DefaultContextMenu); 767 | } 768 | 769 | void LiquidAppWindow::setForgiveNextPageLoadError(const bool ok) 770 | { 771 | forgiveNextPageLoadError = ok; 772 | } 773 | 774 | void LiquidAppWindow::stopLoadingOrExitFullScreenMode(void) 775 | { 776 | if (pageIsLoading) { 777 | triggerPageAction(QWebEnginePage::Stop); 778 | } else { 779 | exitFullScreenMode(); 780 | } 781 | } 782 | 783 | void LiquidAppWindow::takeSnapshotSlot(void) 784 | { 785 | takeSnapshot(false); 786 | } 787 | void LiquidAppWindow::takeSnapshotFullPageSlot(void) 788 | { 789 | takeSnapshot(true); 790 | } 791 | 792 | void LiquidAppWindow::takeSnapshot(const bool fullPage) 793 | { 794 | const bool vector = false; // NOTE: experimental feature 795 | const int ratio = QPaintDevice::devicePixelRatio(); 796 | const QSize snapshotSize = (fullPage) ? (page()->contentsSize().toSize() / ratio) : contentsRect().size(); 797 | 798 | // Ensure the target directory exists 799 | const QString path = QDir::homePath() + QDir::separator() + "Pictures"; 800 | { 801 | QDir dir(path); 802 | if (!dir.exists()) { 803 | dir.mkdir("."); 804 | } 805 | } 806 | // Compose snapshot file name 807 | const QString fileName = QString("%1 %2 (%3)") 808 | .arg(*liquidAppName) 809 | .arg(tr((fullPage) ? "full-page snapshot" : "snapshot")) 810 | .arg(Liquid::getReadableDateTimeString()); 811 | 812 | if (vector) { 813 | QFile scriptFile(":/scripts/html2svg.js"); 814 | scriptFile.open(QFile::ReadOnly | QFile::Text); 815 | const QString js = QString(scriptFile.readAll()) 816 | .arg(snapshotSize.width()) 817 | .arg(snapshotSize.height()) 818 | .arg(colorToRgba(page()->backgroundColor())) 819 | .arg(fullPage); 820 | 821 | page()->runJavaScript(QString("(()=>{%1})();").arg(js), QWebEngineScript::ApplicationWorld, [path, fileName](const QVariant& res){ 822 | qDebug().noquote() << res.toString(); 823 | 824 | // Save vector image to disk 825 | { 826 | QFile file(path + QDir::separator() + fileName + ".svg"); 827 | if (file.open(QIODevice::ReadWrite)) { 828 | QTextStream stream(&file); 829 | #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) 830 | stream << res.toString() << Qt::endl; 831 | #else 832 | stream << res.toString() << endl; 833 | #endif 834 | } 835 | } 836 | }); 837 | } else { 838 | const QImage::Format format = QImage::Format_ARGB32; 839 | QImage* image = new QImage(snapshotSize * ratio, format); 840 | image->setDevicePixelRatio(ratio); 841 | image->fill(Qt::transparent); 842 | 843 | QPainter* painter = new QPainter(image); 844 | // TODO: make fonts appear less blurry (potentially use painter->scale(ratio, ratio) 845 | painter->setRenderHint(QPainter::Antialiasing); 846 | painter->setRenderHint(QPainter::TextAntialiasing); 847 | painter->setRenderHint(QPainter::SmoothPixmapTransform); 848 | #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 849 | painter->setRenderHint(QPainter::HighQualityAntialiasing); 850 | painter->setRenderHint(QPainter::NonCosmeticDefaultPen); 851 | #endif 852 | 853 | if (fullPage) { 854 | const QSize origWindowSize = size(); 855 | const bool wasFullScreen = isFullScreen(); 856 | // Remember initial scroll position to be able to come back to it after the whole page is captured 857 | const QPointF origScrollPos = page()->scrollPosition() / ratio; 858 | 859 | // Resize the window to fit all of its content 860 | { 861 | setAttribute(Qt::WA_DontShowOnScreen, true); 862 | show(); 863 | 864 | resize(snapshotSize); 865 | } 866 | 867 | // Render contents of QWidget into QPainter 868 | render(painter); 869 | 870 | // Restore the window back to be exactly how it was 871 | { 872 | // Resize back to what it was 873 | resize(origWindowSize); 874 | 875 | // Reveal the window 876 | hide(); 877 | setAttribute(Qt::WA_DontShowOnScreen, false); 878 | show(); 879 | 880 | // Restore window's full-screen mode 881 | if (wasFullScreen && !isFullScreen()) { 882 | toggleFullScreenMode(); 883 | } 884 | 885 | // Scroll the web view back to where it was before we started taking full-page snapshot 886 | static const QString js = "window.scrollTo(%1, %2);"; 887 | page()->runJavaScript(QString(js).arg(origScrollPos.x()).arg(origScrollPos.y()), QWebEngineScript::ApplicationWorld); 888 | } 889 | } else { 890 | #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) 891 | const bool hadScrollBarsShown = liquidAppWebSettings->testAttribute(QWebEngineSettings::ShowScrollBars); 892 | 893 | if (hadScrollBarsShown) { 894 | // Hide scrollbars before taking snapshot 895 | liquidAppWebSettings->setAttribute(QWebEngineSettings::ShowScrollBars, false); 896 | 897 | // Wait a little bit for scrollbars to disappear 898 | Liquid::sleep(1500); 899 | 900 | // qDebug() << liquidAppWebPage->renderProcessPid; // Qt 5.15 could allow us to trigger immediate page re-render by sending signal to that process 901 | // liquidAppWebPage->setVisible(false); liquidAppWebPage->setVisible(true); // Try this out once Qt 5.14 is more widely available 902 | 903 | // This is another approach for temporarily hiding scrollbars 904 | // static const QString js = "'undefined' != typeof document.styleSheets && 0 < document.styleSheets.length && document.styleSheets[0].addRule('::-webkit-scrollbar', 'width: 0 !important; height: 0 !important', 0);"; 905 | // page()->runJavaScript(QString(js), QWebEngineScript::ApplicationWorld, [&](const QVariant& res){ 906 | // // Liquid::sleep(100); 907 | // }); 908 | } 909 | #endif 910 | 911 | // Render contents of QWidget into QPainter 912 | render(painter); 913 | 914 | #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) 915 | if (hadScrollBarsShown) { 916 | // Bring scrollbars back after taking snapshot 917 | liquidAppWebSettings->setAttribute(QWebEngineSettings::ShowScrollBars, true); 918 | } 919 | #endif 920 | } 921 | 922 | painter->end(); 923 | delete painter; 924 | 925 | // Save raster image to disk 926 | image->scaled(snapshotSize.width(), snapshotSize.height(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation) 927 | .save(path + QDir::separator() + fileName + ".png", "PNG"); 928 | 929 | // TODO: add EXIF? 930 | 931 | delete image; 932 | } 933 | 934 | // TODO: add camera flash visual effect 935 | // TODO: add shutter sound 936 | } 937 | 938 | void LiquidAppWindow::toggleFullScreenMode(void) 939 | { 940 | if (isFullScreen()) { 941 | exitFullScreenMode(); 942 | } else { 943 | // Make it temporarily possible to resize the window if geometry is locked 944 | if (windowGeometryIsLocked) { 945 | setMinimumSize(LQD_APP_WIN_MIN_SIZE_W, LQD_APP_WIN_MIN_SIZE_H); 946 | setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX); 947 | } 948 | // Enter the full-screen mode 949 | setWindowState(windowState() | Qt::WindowFullScreen); 950 | } 951 | } 952 | 953 | void LiquidAppWindow::toggleWindowGeometryLock(void) 954 | { 955 | // Prevent toggling window geometry lock while in full-screen mode 956 | if (!isFullScreen()) { 957 | if (windowGeometryIsLocked) { 958 | // Open up resizing restrictions 959 | setMinimumSize(LQD_APP_WIN_MIN_SIZE_W, LQD_APP_WIN_MIN_SIZE_H); 960 | setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX); 961 | windowGeometryIsLocked = false; 962 | } else { 963 | // Lock down resizing 964 | setMinimumSize(width(), height()); 965 | setMaximumSize(width(), height()); 966 | windowGeometryIsLocked = true; 967 | } 968 | 969 | liquidAppConfig->sync(); 970 | } 971 | 972 | updateWindowTitle(title()); 973 | } 974 | 975 | void LiquidAppWindow::updateWindowTitle(const QString title) 976 | { 977 | QString textIcons; 978 | 979 | if (!liquidAppWindowTitleIsReadOnly) { 980 | if (title.size() > 0) { 981 | liquidAppWindowTitle = title; 982 | } else { 983 | liquidAppWindowTitle = *liquidAppName; 984 | } 985 | } 986 | 987 | // Append unicode icons 988 | if (pageHasCertificateError) { 989 | textIcons.append(LQD_ICON_WARNING); 990 | } 991 | if (windowGeometryIsLocked) { 992 | textIcons.append(LQD_ICON_LOCKED); 993 | } 994 | if (page()->isAudioMuted()) { 995 | textIcons.append(LQD_ICON_MUTED); 996 | } 997 | if (pageIsLoading) { 998 | textIcons.append(LQD_ICON_LOADING); 999 | } else { 1000 | if (pageHasError) { 1001 | textIcons.append(LQD_ICON_ERROR); 1002 | } 1003 | } 1004 | 1005 | if (textIcons != "") { 1006 | textIcons = " " + textIcons; 1007 | } 1008 | 1009 | setWindowTitle(liquidAppWindowTitle + textIcons); 1010 | } 1011 | 1012 | void LiquidAppWindow::zoomIn(const bool fine = false) 1013 | { 1014 | attemptToSetZoomFactorTo(zoomFactor() + ((fine) ? LQD_ZOOM_LVL_STEP_FINE : LQD_ZOOM_LVL_STEP)); 1015 | } 1016 | 1017 | void LiquidAppWindow::zoomOut(const bool fine = false) 1018 | { 1019 | attemptToSetZoomFactorTo(zoomFactor() - ((fine) ? LQD_ZOOM_LVL_STEP_FINE : LQD_ZOOM_LVL_STEP)); 1020 | } 1021 | 1022 | void LiquidAppWindow::zoomReset(void) 1023 | { 1024 | attemptToSetZoomFactorTo(1.0); 1025 | } 1026 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include <csignal> 2 | 3 | #include <QApplication> 4 | #include <QCommandLineParser> 5 | #include <QDir> 6 | #include <QSettings> 7 | 8 | #include "lqd.h" 9 | #include "liquid.hpp" 10 | #include "liquidappconfigwindow.hpp" 11 | #include "liquidappwindow.hpp" 12 | #include "mainwindow.hpp" 13 | 14 | QTextStream cout(stdout); 15 | 16 | static QSharedMemory* sharedMemory = Q_NULLPTR; 17 | 18 | LiquidAppWindow* liquidAppWindow; 19 | MainWindow* mainWindow; 20 | 21 | QString getUserName() 22 | { 23 | QString name = qgetenv("USER"); 24 | 25 | if (name.isEmpty()) { 26 | name = qgetenv("USERNAME"); 27 | } 28 | 29 | return name; 30 | } 31 | 32 | static void onSignalHandler(int signum) 33 | { 34 | if (sharedMemory) { 35 | delete sharedMemory; 36 | sharedMemory = Q_NULLPTR; 37 | } 38 | 39 | if (liquidAppWindow) { 40 | liquidAppWindow->close(); 41 | delete liquidAppWindow; 42 | liquidAppWindow = Q_NULLPTR; 43 | } 44 | 45 | if (mainWindow) { 46 | mainWindow->close(); 47 | delete mainWindow; 48 | mainWindow = Q_NULLPTR; 49 | } 50 | 51 | qDebug() << "Terminated with signal" << signum; 52 | 53 | exit(128 + signum); 54 | } 55 | 56 | int main(int argc, char **argv) 57 | { 58 | int ret = EXIT_SUCCESS; 59 | 60 | #if defined(Q_OS_LINUX) || defined(Q_OS_MAC) 61 | // Handle any further termination signals to ensure 62 | // that the QSharedMemory block is deleted 63 | // even if the process crashes 64 | signal(SIGHUP, onSignalHandler); 65 | signal(SIGINT, onSignalHandler); 66 | signal(SIGQUIT, onSignalHandler); 67 | signal(SIGILL, onSignalHandler); 68 | signal(SIGABRT, onSignalHandler); 69 | signal(SIGFPE, onSignalHandler); 70 | signal(SIGBUS, onSignalHandler); 71 | signal(SIGSEGV, onSignalHandler); 72 | signal(SIGSYS, onSignalHandler); 73 | signal(SIGPIPE, onSignalHandler); 74 | signal(SIGALRM, onSignalHandler); 75 | signal(SIGTERM, onSignalHandler); 76 | signal(SIGXCPU, onSignalHandler); 77 | signal(SIGXFSZ, onSignalHandler); 78 | #endif 79 | 80 | #if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 81 | // Account for running on high-DPI displays 82 | QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 83 | #endif 84 | 85 | QCoreApplication::setAttribute(Qt::AA_UseOpenGLES, true); 86 | 87 | QApplication app(argc, argv); 88 | 89 | if (argc < 2) { 90 | // Allow only one instance 91 | sharedMemory = new QSharedMemory(getUserName() + "_Liquid"); 92 | if (!sharedMemory->create(4, QSharedMemory::ReadOnly)) { 93 | delete sharedMemory; 94 | qDebug().noquote() << QString("Only one instance of Liquid is allowed"); 95 | exit(EXIT_FAILURE); 96 | } 97 | 98 | // Show main program window 99 | mainWindow = new MainWindow; 100 | } else { // App name provided 101 | // CLI flags and options 102 | QCommandLineParser parser; 103 | QCoreApplication::setApplicationName(PROG_NAME); 104 | QCoreApplication::setApplicationVersion(VERSION); 105 | 106 | // parser.setApplicationDescription("Test helper"); 107 | parser.setApplicationDescription("Test helper"); 108 | parser.addHelpOption(); 109 | parser.addVersionOption(); 110 | parser.addPositionalArgument("app-name", QCoreApplication::translate("main", "Liquid App name")); 111 | 112 | // Set up CLI flags and options 113 | const QCommandLineOption listAppsFlag(QStringList() << "l" << "list-apps", 114 | QCoreApplication::translate("main", "List all available Liquid Apps")); 115 | parser.addOption(listAppsFlag); 116 | const QCommandLineOption editAppDialogFlag(QStringList() << "E" << "edit-app-dialog", 117 | QCoreApplication::translate("main", "Open edit Liquid App dialog")); 118 | parser.addOption(editAppDialogFlag); 119 | 120 | // Process the actual command line arguments given by the user 121 | parser.process(app); 122 | 123 | const QStringList args = parser.positionalArguments(); 124 | QString liquidAppName = (args.size() > 0) ? args.at(0) : ""; 125 | 126 | // Replace directory separators (slashes) with underscores 127 | // to ensure no sub-directories would get created 128 | liquidAppName = liquidAppName.replace(QDir::separator(), "_"); 129 | 130 | // TODO: look for -c,-e, -d flags here 131 | 132 | // Process the -l/--list-apps flag 133 | if (parser.isSet(listAppsFlag)) { 134 | #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) 135 | cout << Liquid::getLiquidAppsList().join("\n") << Qt::endl; 136 | #else 137 | cout << Liquid::getLiquidAppsList().join("\n") << endl; 138 | #endif 139 | return ret; 140 | } 141 | 142 | attempt_to_create_or_run_liquid_app: 143 | // Attempt to load Liquid app's config file 144 | QSettings* tempAppSettings = new QSettings(QSettings::IniFormat, 145 | QSettings::UserScope, 146 | QString(PROG_NAME) + QDir::separator() + LQD_APPS_DIR_NAME, 147 | liquidAppName, 148 | Q_NULLPTR); 149 | 150 | // Attempt to load app settings from a config file 151 | if (!parser.isSet(editAppDialogFlag) && tempAppSettings->contains(LQD_CFG_KEY_NAME_URL)) { 152 | // // Allow only one instance 153 | sharedMemory = new QSharedMemory(getUserName() + "_Liquid_app_" + liquidAppName); 154 | if (!sharedMemory->create(4, QSharedMemory::ReadOnly)) { 155 | delete sharedMemory; 156 | qDebug().noquote() << QString("Only one instance of Liquid app “%1” is allowed").arg(liquidAppName); 157 | exit(EXIT_FAILURE); 158 | } 159 | 160 | // Found existing liquid app settings file, show it 161 | liquidAppWindow = new LiquidAppWindow(&liquidAppName); 162 | } else { 163 | // No such Liquid app found, open Liquid app creation dialog 164 | LiquidAppConfigDialog LiquidAppConfigDialog(mainWindow, liquidAppName); 165 | LiquidAppConfigDialog.setPlanningToRun(true); // Make run after it's created 166 | 167 | // Reveal Liquid app creation dialog 168 | LiquidAppConfigDialog.show(); 169 | switch (LiquidAppConfigDialog.exec()) { 170 | case QDialog::Rejected: 171 | // Exit the program 172 | goto done; 173 | break; 174 | 175 | case QDialog::Accepted: 176 | // Ensure the new Liquid app's settings file is named based on the input field, 177 | // and not on what was initially provided via CLI 178 | liquidAppName = LiquidAppConfigDialog.getName(); 179 | // Replace directory separators (slashes) with underscores 180 | // to ensure no sub-directories would get created 181 | liquidAppName = liquidAppName.replace(QDir::separator(), "_"); 182 | 183 | if (LiquidAppConfigDialog.isPlanningToRun()) { 184 | // Run the newly created Liquid App 185 | goto attempt_to_create_or_run_liquid_app; 186 | } else { 187 | goto done; 188 | } 189 | break; 190 | } 191 | } 192 | } 193 | 194 | ret = app.exec(); 195 | 196 | done: 197 | if (sharedMemory != Q_NULLPTR) { 198 | delete sharedMemory; 199 | sharedMemory = Q_NULLPTR; 200 | } 201 | 202 | return ret; 203 | } 204 | -------------------------------------------------------------------------------- /src/mainwindow.cpp: -------------------------------------------------------------------------------- 1 | #include <QDir> 2 | #include <QFileInfo> 3 | #include <QHeaderView> 4 | #include <QLabel> 5 | #include <QMessageBox> 6 | #include <QVBoxLayout> 7 | 8 | #include "liquid.hpp" 9 | #include "liquidappconfigwindow.hpp" 10 | #include "lqd.h" 11 | #include "mainwindow.hpp" 12 | 13 | MainWindow::MainWindow() : QScrollArea() 14 | { 15 | setWindowTitle(LQD_PROG_TITLE); 16 | 17 | setMinimumSize(LQD_WIN_MIN_SIZE_W, LQD_WIN_MIN_SIZE_H); 18 | setWidgetResizable(true); 19 | 20 | // Set window icon 21 | #if !defined(Q_OS_LINUX) // This doesn't work on X11 22 | setWindowIcon(QIcon(":/images/" PROG_NAME ".svg")); 23 | #endif 24 | 25 | settings = new QSettings(PROG_NAME, PROG_NAME); 26 | if (settings->contains(LQD_CFG_KEY_NAME_WIN_GEOM)) { 27 | QByteArray geometry = QByteArray::fromHex( 28 | settings->value(LQD_CFG_KEY_NAME_WIN_GEOM).toByteArray() 29 | ); 30 | restoreGeometry(geometry); 31 | } 32 | 33 | Liquid::applyQtStyleSheets(this); 34 | 35 | QWidget* mainWindowWidget = new QWidget(); 36 | setWidget(mainWindowWidget); 37 | 38 | QVBoxLayout* mainWindowLayout = new QVBoxLayout(); 39 | mainWindowLayout->setSpacing(0); 40 | mainWindowLayout->setContentsMargins(0, 0, 0, 0); 41 | 42 | appListTable = new QTableWidget(); 43 | appListTable->setColumnCount(2); 44 | appListTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); 45 | appListTable->horizontalHeader()->hide(); 46 | appListTable->verticalHeader()->hide(); 47 | appListTable->setShowGrid(false); 48 | appListTable->setFocusPolicy(Qt::NoFocus); 49 | // appListTable->setSelectionMode(QAbstractItemView::SingleSelection); 50 | appListTable->setSelectionBehavior(QAbstractItemView::SelectRows); 51 | mainWindowLayout->addWidget(appListTable); 52 | 53 | // Add new liquid app button 54 | createNewLiquidAppButton = new QPushButton(tr(LQD_ICON_ADD)); 55 | createNewLiquidAppButton->setCursor(Qt::PointingHandCursor); 56 | connect(createNewLiquidAppButton, &QPushButton::clicked, [&]() { 57 | LiquidAppConfigDialog LiquidAppConfigDialog(this, ""); 58 | switch (LiquidAppConfigDialog.exec()) { 59 | case QDialog::Accepted: 60 | // Give some time to the filesystem before scanning for the newly created Liquid App 61 | { 62 | QTime proceedAfter = QTime::currentTime().addMSecs(100); 63 | while (QTime::currentTime() < proceedAfter) { 64 | QCoreApplication::processEvents(QEventLoop::AllEvents, 50); 65 | } 66 | } 67 | flushTable(); 68 | populateTable(); 69 | 70 | if (LiquidAppConfigDialog.isPlanningToRun()) { 71 | Liquid::runLiquidApp(LiquidAppConfigDialog.getName()); 72 | } 73 | break; 74 | } 75 | }); 76 | mainWindowLayout->addWidget(createNewLiquidAppButton); 77 | 78 | mainWindowWidget->setLayout(mainWindowLayout); 79 | 80 | // Run the liquid app upon double-click on its row in the table 81 | connect(appListTable, &QTableWidget::cellDoubleClicked, [&](int row, int col) { 82 | Q_UNUSED(col); 83 | Liquid::runLiquidApp(appListTable->item(row, 0)->text()); 84 | }); 85 | 86 | // Connect keyboard shortcuts 87 | bindShortcuts(); 88 | 89 | show(); 90 | raise(); 91 | activateWindow(); 92 | 93 | // Fill the table 94 | populateTable(); 95 | } 96 | 97 | MainWindow::~MainWindow() 98 | { 99 | } 100 | 101 | void MainWindow::bindShortcuts(void) 102 | { 103 | // Connect the exit shortcut 104 | quitAction = new QAction(); 105 | quitAction->setShortcut(QKeySequence(tr(LQD_KBD_SEQ_QUIT))); 106 | addAction(quitAction); 107 | connect(quitAction, SIGNAL(triggered()), this, SLOT(close())); 108 | } 109 | 110 | void MainWindow::closeEvent(QCloseEvent *event) 111 | { 112 | // Remember window geometry and position 113 | saveSettings(); 114 | 115 | event->accept(); 116 | } 117 | 118 | void MainWindow::flushTable(void) 119 | { 120 | for (int i = appListTable->rowCount(); i > -1 ; i--) { 121 | appListTable->removeRow(i); 122 | } 123 | } 124 | 125 | void MainWindow::populateTable(void) 126 | { 127 | foreach (const QString liquidAppName, Liquid::getLiquidAppsList()) { 128 | const int i = appListTable->rowCount(); 129 | 130 | appListTable->insertRow(i); 131 | 132 | QSettings* liquidAppConfig = new QSettings(QSettings::IniFormat, 133 | QSettings::UserScope, 134 | QString(PROG_NAME "%1" LQD_APPS_DIR_NAME).arg(QDir::separator()), 135 | liquidAppName, 136 | Q_NULLPTR); 137 | 138 | ////////////////// 139 | // First column // 140 | ////////////////// 141 | 142 | QTableWidgetItem* appItemWidgetFirstColumn = new QTableWidgetItem(); 143 | // Make them read-only (no text edit upon double-click) 144 | appItemWidgetFirstColumn->setFlags(appItemWidgetFirstColumn->flags() ^ Qt::ItemIsEditable); 145 | if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_ICON)) { 146 | QPixmap pixmap; 147 | pixmap.loadFromData(QByteArray::fromBase64(liquidAppConfig->value(LQD_CFG_KEY_NAME_ICON).toByteArray().remove(0, 22)), "PNG"); 148 | appItemWidgetFirstColumn->setIcon(QIcon(pixmap)); 149 | } else { 150 | appItemWidgetFirstColumn->setIcon(QIcon(":/images/" PROG_NAME ".svg")); 151 | } 152 | appItemWidgetFirstColumn->setText(liquidAppName); 153 | appListTable->setItem(i, 0, appItemWidgetFirstColumn); 154 | 155 | /////////////////// 156 | // Second column // 157 | /////////////////// 158 | 159 | QWidget* appItemActionButtonsWidget = new QWidget(this); 160 | QHBoxLayout *appItemLayout = new QHBoxLayout(); 161 | appItemLayout->setSpacing(0); 162 | appItemLayout->setContentsMargins(0, 0, 0, 0); 163 | appItemActionButtonsWidget->setLayout(appItemLayout); 164 | 165 | // Delete button 166 | QPushButton* deleteButton = new QPushButton(tr(LQD_ICON_DELETE), this); 167 | deleteButton->setCursor(Qt::PointingHandCursor); 168 | deleteButton->setProperty("class", "liquidAppsListButtonDelete"); 169 | connect(deleteButton, &QPushButton::clicked, [this, liquidAppName, liquidAppConfig]() { 170 | const QString text = QString("Are you sure you want to delete Liquid app “%1”?").arg(liquidAppName); 171 | const QMessageBox::StandardButton reply = QMessageBox::question(this, "Confirmation", text, QMessageBox::Yes | QMessageBox::No); 172 | if (reply == QMessageBox::Yes) { 173 | Liquid::removeDesktopFile(liquidAppName); 174 | 175 | // Shred and unlink Liquid app settings file 176 | QFile liquidAppConfigFile(liquidAppConfig->fileName()); 177 | // Open file handle 178 | if (liquidAppConfigFile.open(QIODevice::ReadWrite)) { 179 | // Determine file length 180 | const int liquidAppConfigFileSize = liquidAppConfigFile.size(); 181 | // Shred (especially important if it contains Cookie data) 182 | for (int i = 0, imax = 5; i < imax; i++) { 183 | // Write randomly generated array of bytes to disk 184 | liquidAppConfigFile.write(Liquid::generateRandomByteArray(liquidAppConfigFileSize), liquidAppConfigFileSize); 185 | // Close file handle 186 | liquidAppConfigFile.close(); 187 | 188 | if (i < imax - 1) { 189 | // Put cursor back to start (to write again across the same byte range instead of appending data upon next iteration) 190 | liquidAppConfigFile.open(QIODevice::ReadWrite); 191 | } else { 192 | // Unlink file 193 | liquidAppConfigFile.remove(); 194 | qDebug().noquote() << QString("Removed config file for Liquid app %1").arg(liquidAppName); 195 | } 196 | } 197 | } else { 198 | qDebug().noquote() << QString("Unable to open file %1 in Read/Write mode").arg(liquidAppConfig->fileName()); 199 | } 200 | 201 | // Refresh table 202 | flushTable(); 203 | populateTable(); 204 | } 205 | }); 206 | appItemLayout->addWidget(deleteButton); 207 | 208 | // Edit button 209 | QPushButton* editButton = new QPushButton(tr(LQD_ICON_EDIT), this); 210 | editButton->setCursor(Qt::PointingHandCursor); 211 | editButton->setProperty("class", "liquidAppsListButtonEdit"); 212 | connect(editButton, &QPushButton::clicked, [this, liquidAppName]() { 213 | LiquidAppConfigDialog LiquidAppConfigDialog(this, liquidAppName); 214 | switch (LiquidAppConfigDialog.exec()) { 215 | case QDialog::Accepted: 216 | // Give some time to the filesystem before scanning for the newly created app 217 | { 218 | QTime proceedAfter = QTime::currentTime().addMSecs(100); 219 | while (QTime::currentTime() < proceedAfter) { 220 | QCoreApplication::processEvents(QEventLoop::AllEvents, 50); 221 | } 222 | } 223 | flushTable(); 224 | populateTable(); 225 | break; 226 | } 227 | }); 228 | appItemLayout->addWidget(editButton); 229 | 230 | // Run button 231 | QPushButton* runButton = new QPushButton(tr(LQD_ICON_RUN), this); 232 | runButton->setCursor(Qt::PointingHandCursor); 233 | runButton->setProperty("class", "liquidAppsListButtonRun"); 234 | appItemLayout->addWidget(runButton); 235 | connect(runButton, &QPushButton::clicked, [liquidAppName]() { 236 | Liquid::runLiquidApp(liquidAppName); 237 | }); 238 | 239 | appListTable->setCellWidget(i, 1, appItemActionButtonsWidget); 240 | } 241 | } 242 | 243 | void MainWindow::saveSettings(void) 244 | { 245 | settings->setValue(LQD_CFG_KEY_NAME_WIN_GEOM, QString(saveGeometry().toHex())); 246 | settings->sync(); 247 | } 248 | -------------------------------------------------------------------------------- /tests/_data_/README.md: -------------------------------------------------------------------------------- 1 | # Liquid Test 2 | 3 | Here's a small liquid app which links to a single-page website 4 | made for performing various acceptance tests. 5 | 6 | 7 | ## How to use: 8 | 9 | 1. copy `apps/test.ini` into `~/.config/liquid/apps/` 10 | 2. run `make server` 11 | 3. run `../bin/liquid test` 12 | -------------------------------------------------------------------------------- /tests/_data_/apps/additional-js.ini: -------------------------------------------------------------------------------- 1 | ; Modifying this file may result in test page displaying incorrect information 2 | 3 | [General] 4 | BackgroundColor=#fff 5 | CookiesAllowed=true 6 | AdditionalJS=document.getElementById('custom-js').innerText = 'Check' 7 | WindowGeometryLocked=true 8 | WindowGeometry=01d9d0cb00020000000004e0000000000000072e00000388000004e40000001d0000072a0000038400000000000000000780 9 | JavaScriptEnabled=true 10 | ThirdPartyCookiesAllowed=false 11 | Title=Liquid Test 12 | URL=http://0.0.0.0:5151 13 | CustomUserAgent=Liquid Browser 14 | Zoom=0.6999999523162842 15 | -------------------------------------------------------------------------------- /tests/_data_/index.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | 3 | <style> 4 | 5 | input { 6 | width: 360px; 7 | } 8 | 9 | </style> 10 | 11 | <h1>Liquid Test</h1> 12 | 13 | 14 | <h2>Links</h2> 15 | 16 | <ol> 17 | <li> 18 | <a href="/">[](/): this link should do nothing</a> 19 | </li> 20 | <li> 21 | <a href="#">[](#): this link should do nothing</a> 22 | </li> 23 | <li> 24 | <a href="http://duckduckgo.com">[](http://duckduckgo.com): this link should open a new browser window</a> 25 | </li> 26 | <li> 27 | <a href="http://duckduckgo.com" target="_blank">[target="_blank"](http://duckduckgo.com): this link should open a new browser window</a> 28 | </li> 29 | <li> 30 | <a href="/" target="_blank">[target="_blank"](/): this link should go to /</a> 31 | </li> 32 | </ol> 33 | 34 | 35 | <h2>Forms</h2> 36 | 37 | <ol> 38 | <li> 39 | <form method="get"> 40 | <input name="q" value="Submitting this should make a GET request" /> 41 | <button type="submit">Submit</button> 42 | </form> 43 | </li> 44 | <li> 45 | <form method="post"> 46 | <input name="q" value="Submitting this should make a POST request" /> 47 | <button type="submit">Submit</button> 48 | </form> 49 | </li> 50 | <li> 51 | <form method="get" action="http://duckduckgo.com"> 52 | <input name="q" value="Submitting this should not do anything" /> 53 | <button type="submit">Submit</button> 54 | </form> 55 | </li> 56 | <li> 57 | <form method="post" action="http://duckduckgo.com"> 58 | <input name="q" value="Submitting this should not do anything" /> 59 | <button type="submit">Submit</button> 60 | </form> 61 | </li> 62 | </ol> 63 | 64 | 65 | <h2>JavaScript</h2> 66 | 67 | <ol> 68 | <li> 69 | Is JavaScript disabled? <noscript><b>Yes, it is.</b></noscript> 70 | </li> 71 | <li> 72 | Is Custom JS injection in check? <b id="custom-js"></b> 73 | </li> 74 | <li> 75 | <a href="javascript:alert('I don\'t know anything')">alert()</a> 76 | </li> 77 | </ol> 78 | 79 | 80 | <h2>User-Agent</h2> 81 | 82 | <ol> 83 | <li> 84 | User-Agent string: <b id="user-agent"></b> 85 | <script> document.getElementById("user-agent").innerText = navigator.userAgent </script> 86 | </li> 87 | </ol> 88 | 89 | 90 | <h2>Sandboxing</h2> 91 | 92 | <ol> 93 | <li> 94 | It's okay to see this remote picture: 95 | <br /> 96 | <img src="https://avatars1.githubusercontent.com/u/1024025?v=3&s=64" /> 97 | </li> 98 | <li> 99 | You should never ever see the contents of this iframe: 100 | <br /> 101 | <iframe src="https://avatars1.githubusercontent.com/u/1024025?v=3&s=240"></iframe> 102 | </li> 103 | </ol> 104 | --------------------------------------------------------------------------------