diff --git a/.flake8.dist b/.flake8.dist new file mode 100644 index 0000000..a5c25a0 --- /dev/null +++ b/.flake8.dist @@ -0,0 +1,156 @@ +[flake8] +# +# flake8 config file docs: https://flake8.pycqa.org/en/latest/user/configuration.html +# +# in venv +# pip install wemake-python-styleguide +# flake8 websiteapp/ +# +# Refactoring hints: +# https://flake8.codes/wemake-python-styleguide/0.15.3/index.html + +# https://pypi.org/project/flake8-noqa/ +noqa-require-code +max-line-length = 132 + +exclude = + .git, + __pycache__, +# This contains our built documentation + build, + dist + +# it's not a bug that we aren't using all of hacking +ignore = + D101, # D101 Missing docstring in public class + D102, # D102 Missing docstring in public method + D107, # D107 Missing docstring in __init__ + D200, # D200 One-line docstring should fit on one line with quotes + D202, # D202 No blank lines allowed after function docstring + D205, # D205 1 blank line required between summary line and description + D400, # D400 First line should end with a period + D401, # D401 First line should be in imperative mood + DAR101, # DAR101 Missing parameter(s) in Docstring + DAR201, # DAR201 Missing "Returns" in Docstring: - return + DAR401, # DAR401 Missing exception(s) in Raises section: -r TypeError + E241, # E241 multiple spaces after ':' + E251, # E251 unexpected spaces around keyword / parameter equals + F821, # F821 undefined name 'PropFile' + I001, # I001 isort found an import in the wrong position + I003, # I003 isort expected 1 blank line in imports, + I004, # I004 isort found an unexpected blank line in imports + I005, # I005 isort found an unexpected missing import + RST213, # RST213 Inline emphasis start-string without end-string. + W503, # W503 line break before binary operator + WPS110, # WPS110 Found wrong variable name: item + WPS114, # WPS114 Found underscored number name pattern: val_1 + WPS115, # WPS115 Found upper-case constant in a class + WPS201, # WPS201 Found module with too many imports: 13 > 12 + WPS204, #WPS204 Found overused expression: config['opening']; used 5 > 4 + WPS210, # WPS210 Found too many local variables: 14 > 5 + WPS211, # WPS211 Found too many arguments: 6 > 5 + WPS213, # WPS213 Found too many expressions: 10 > 9 + WPS214, # WPS214 Found too many methods: 9 > 7 + WPS220, # WPS220 Found too deep nesting: 28 > 20 + WPS221, # WPS221 Found line with high Jones Complexity: 17 > 14 + WPS226, # WPS226 Found string constant over-use: " > 3 + WPS229, # WPS229 Found too long ``try`` body length: 2 > 1 + WPS231, # WPS231 Found function with too much cognitive complexity: 83 > 12 + WPS232, # WPS232 Found module cognitive complexity that is too high: 27.7 > 8 + WPS237, # WPS237 Found a too complex `f` string + WPS238, # WPS238 Found too many raises in a function: 5 > 3 + WPS300, # WPS300 Found local folder import + WPS302, # WPS302 Found unicode string prefix + WPS305, # WPS305 Found `f` string + WPS317, # WPS317 Found incorrect multi-line parameters + WPS318, # WPS318 Found extra indentation + WPS319, # WPS319 Found bracket in wrong position + WPS336, # WPS336 Found explicit string concatenation + WPS338, # WPS338 Found incorrect order of methods in a class + WPS360, # WPS360 Found an unnecessary use of a raw string + WPS402, # WPS402 Found `noqa` comments overuse: 12 + WPS420, # WPS420 Found wrong keyword: pass + WPS442, # WPS442 Found outer scope names shadowing: + WPS600, # WPS600 Found subclassing a builtin: list + WPS602, # WPS602 Found using `@staticmethod` + WPS604, # WPS604 Found incorrect node inside `class` body + WPS605, # WPS605 Found method without arguments + WPS615, # WPS615 Found unpythonic getter or setter + +per-file-ignores = + # WPS420 Found wrong keyword: pass + # WPS604 Found incorrect node inside `class` body + transtool/report/error.py: WPS420, WPS604, + transtool/report/warn.py: WPS420, WPS604, + + # WPS437 Found protected attribute usage + transtool/report/group.py: WPS437, + + # WPS230 Found too many public instance attributes + transtool/config/config.py: WPS230, + + # WPS204 Found overused expression: config['opening']; used 5 > 4 + # WPS213 Found too many expressions + # WPS301 Found dotted raw import: transtool.checks + # WPS421 Found wrong function call: dir, print + # WPS437 Found protected attribute usage + # WPS529 Found implicit `.get()` dict usage + # WPS609 Found direct magic attribute usage: __setattr__, __getattr__, ... + # WPS433 Found nested import + # WPS425 Found boolean non-keyword argument: True, False + transtool/config/reader.py: WPS609, WPS421, WPS204, WPS529, WPS433, + transtool/config/builder.py: WPS609, WPS301, WPS437, WPS213, WPS425, + + # WPS111 Found too short name + # WPS214 Found too many methods + # WPS317 Found incorrect multi-line parameters + # WPS323 Found `%` string formatting + # WPS421 Found wrong function call: print + # WPS437 Found protected attribute usage + # WPS518 Found implicit `enumerate()` call + transtool/log.py: WPS214, WPS437, WPS111, WPS317, WPS421, WPS518, WPS323, + + # WPS201 Found module with too many imports: 13 > 12 + # WPS213 Found too many expressions: 13 > 9 + # WPS317 Found incorrect multi-line parameters + # WPS323 Found `%` string formatting + transtool/main.py: WPS201, WPS213, WPS317, WPS323, + + # S101 Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. + # WPS421 Found wrong function call: dir + # WPS430 Found nested function: overrider + transtool/decorators/overrides.py: WPS430, S101, WPS421, + + # WPS201 Found module with too many imports: 21 > 12 + # WPS230 Found too many public instance attributes + transtool/prop/file.py: WPS201, WPS230, + + # WPS100 Found wrong module name + # WPS421 Found wrong function call: print + transtool/utils.py: WPS421, WPS100, + + # S311 Standard pseudo-random generators are not suitable for security/cryptographic purposes. + # WPS118 Found too long name + # WPS214 Found too many methods + # WPS323 Found `%` string formatting + # WPS432 Found magic number + # WPS437 Found protected attribute usage + # WPS609 Found direct magic attribute usage: __abstractmethods__ + tests/*: S311, WPS323, WPS214, WPS432, WPS609, WPS118, WPS437, + + # WPS431 Found nested class: FakeArgs + tests/report/test_config_builder.py: WPS431, + + # WPS430 Found nested function: log_abort_side_effect + # S311 Standard pseudo-random generators are not suitable for security/cryptographic purposes. + # N802 function name 'assertTranslation' should be lowercase + tests/prop/test_file.py: WPS430, S311, N802 + + # WPS230 Found too many public instance attributes: 8 > 6 + # WPS414 Found incorrect unpacking target + # WPS425 Found boolean non-keyword argument: True, False + # WPS431 Found nested class: FakeArgs + # WPS437 Found protected attribute usage: _validate + # WPS609 Found direct magic attribute usage: __setattr__ + # S311 Standard pseudo-random generators are not suitable for security/cryptographic purposes. + tests/config/test_config_builder.py: WPS437, WPS425, WPS431, WPS230, WPS414, WPS609, S311, diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 0000000..e294bb1 --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,46 @@ +# +# Website As App +# Run any website as standalone desktop application +# +# @author Marcin Orlowski +# @copyright 2023-2024 Marcin Orlowski +# @license https://www.opensource.org/licenses/mit-license.php MIT +# @link https://github.com/MarcinOrlowski/website-as-app +# + +name: "Code lint" + +on: + push: + branches: [ master ] + pull_request: + branches: [ master, dev ] + +jobs: + unittests: + name: "Linting" + runs-on: ubuntu-latest + + steps: + # https://github.com/marketplace/actions/checkout + - name: "Checkout sources" + uses: actions/checkout@v4 + + # https://github.com/marketplace/actions/paths-changes-filter + - name: "Look for changed files..." + uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + srcs: + - '**/*.py' + + - name: "Installing dependencies..." + if: steps.filter.outputs.srcs == 'true' + run: pip install -r requirements-dev.txt + + # https://github.com/marketplace/actions/wemake-python-styleguide + # https://wemake-python-styleguide.readthedocs.io/en/latest/pages/usage/integrations/github-actions.html + - name: "Running linter..." + if: steps.filter.outputs.srcs == 'true' + uses: wemake-services/wemake-python-styleguide@0.18.0 diff --git a/.github/workflows/markdown.yml b/.github/workflows/markdown.yml new file mode 100644 index 0000000..5f78c62 --- /dev/null +++ b/.github/workflows/markdown.yml @@ -0,0 +1,45 @@ +# +# Website As App +# Run any website as standalone desktop application +# +# @author Marcin Orlowski +# @copyright 2023-2024 Marcin Orlowski +# @license https://www.opensource.org/licenses/mit-license.php MIT +# @link https://github.com/MarcinOrlowski/website-as-app +# + +name: "MD Lint" + +on: + push: + branches: [ master ] + pull_request: + branches: [ master, dev ] + +jobs: + markdown_lint: + name: "Markdown linter" + runs-on: ubuntu-latest + + steps: + # https://github.com/marketplace/actions/checkout + - name: "Checkout sources" + uses: actions/checkout@v4 + + # https://github.com/marketplace/actions/paths-changes-filter + - name: "Look for changed doc related files..." + uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + docs: + - '**/*.md' + + # https://github.com/marketplace/actions/my-markdown-linter + - name: "Running markdown linter..." + uses: ruzickap/action-my-markdown-linter@v1 + if: steps.filter.outputs.docs == 'true' + with: + # LICENSE is externally sourced and we're not going to fix it. + exclude: "LICENSE.md" + config_file: .markdownlint.yml.dist diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79fdac7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,108 @@ +.pypirc + +*~ +*.bak +*.swp + + + +# Linters +/.flake8 +/.markdownlint.yml +/.pre-commit-config.yaml +/venv/ + +# IDEA +.idea/ +*.iml + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +upd.sh +testfiles diff --git a/.markdownlint.yml.dist b/.markdownlint.yml.dist new file mode 100644 index 0000000..d796e8d --- /dev/null +++ b/.markdownlint.yml.dist @@ -0,0 +1,245 @@ +################################################################################## +# +# Website As App +# Run any website as standalone desktop application +# +# @author Marcin Orlowski +# @copyright 2023-2024 Marcin Orlowski +# @license https://www.opensource.org/licenses/mit-license.php MIT +# @link https://github.com/MarcinOrlowski/website-as-app +# +##### + +# Modified markdownlint YAML configuration +# Changes from defaults annotated with "# MOR" + +# Default state for all rules +default: true + +# Path to configuration file to extend +extends: null + +# MD001/heading-increment/header-increment - Heading levels should only increment by one level at a time +MD001: true + +# MD002/first-heading-h1/first-header-h1 - First heading should be a top-level heading +MD002: false # MOR (this is because we want the tool's description under the logo to be ### and this is first header). + # Heading level + # level: 1 + + +# MD003/heading-style/header-style - Heading style +MD003: + # Heading style + style: "consistent" + +# MD004/ul-style - Unordered list style +MD004: + # List style + style: "consistent" + +# MD005/list-indent - Inconsistent indentation for list items at the same level +MD005: true + +# MD006/ul-start-left - Consider starting bulleted lists at the beginning of the line +#MD006: true +MD006: false # MOR + +# MD007/ul-indent - Unordered list indentation +MD007: + # Spaces for indent + indent: 2 + # Whether to indent the first level of the list + start_indented: false + +# MD009/no-trailing-spaces - Trailing spaces +MD009: + # Spaces for line break + br_spaces: 2 + # Allow spaces for empty lines in list items + list_item_empty_lines: false + # Include unnecessary breaks + strict: false + +# MD010/no-hard-tabs - Hard tabs +MD010: + # Include code blocks + code_blocks: true + +# MD011/no-reversed-links - Reversed link syntax +MD011: true + +# MD012/no-multiple-blanks - Multiple consecutive blank lines +MD012: + # Consecutive blank lines + maximum: 1 + +# MD013/line-length - Line length +MD013: + # Number of characters + line_length: 130 + # Number of characters for headings + heading_line_length: 130 + # Number of characters for code blocks + code_block_line_length: 130 + # Include code blocks + code_blocks: true + # Include tables + tables: true + # Include headings + headings: true + # Include headings + headers: true + # Strict length checking + strict: false + # Stern length checking + stern: false + +# MD014/commands-show-output - Dollar signs used before commands without showing output +#MD014: true +MD014: false # MOR + +# MD018/no-missing-space-atx - No space after hash on atx style heading +MD018: true + +# MD019/no-multiple-space-atx - Multiple spaces after hash on atx style heading +MD019: true + +# MD020/no-missing-space-closed-atx - No space inside hashes on closed atx style heading +MD020: true + +# MD021/no-multiple-space-closed-atx - Multiple spaces inside hashes on closed atx style heading +MD021: true + +# MD022/blanks-around-headings/blanks-around-headers - Headings should be surrounded by blank lines +MD022: + # Blank lines above heading + lines_above: 1 + # Blank lines below heading + lines_below: 1 + +# MD023/heading-start-left/header-start-left - Headings must start at the beginning of the line +MD023: true + +# MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content +MD024: + # Only check sibling headings + allow_different_nesting: false + # Only check sibling headings + siblings_only: false + +# MD025/single-title/single-h1 - Multiple top-level headings in the same document +MD025: + # Heading level + level: 1 + # RegExp for matching title in front matter + front_matter_title: "^\\s*title\\s*[:=]" + +# MD026/no-trailing-punctuation - Trailing punctuation in heading +#MD026: +# # Punctuation characters +# punctuation: ".,;:!。,;:!" +MD026: false # MOR + +# MD027/no-multiple-space-blockquote - Multiple spaces after blockquote symbol +MD027: true + +# MD028/no-blanks-blockquote - Blank line inside blockquote +MD028: true + +# MD029/ol-prefix - Ordered list item prefix +MD029: + # List style + style: "one_or_ordered" + +# MD030/list-marker-space - Spaces after list markers +MD030: + # Spaces for single-line unordered list items + ul_single: 1 + # Spaces for single-line ordered list items + ol_single: 1 + # Spaces for multi-line unordered list items + ul_multi: 1 + # Spaces for multi-line ordered list items + ol_multi: 1 + +# MD031/blanks-around-fences - Fenced code blocks should be surrounded by blank lines +MD031: + # Include list items + list_items: true + +# MD032/blanks-around-lists - Lists should be surrounded by blank lines +MD032: true + +# MD033/no-inline-html - Inline HTML +MD033: false # MOR +#MD033: +# # Allowed elements +# allowed_elements: [] + +# MD034/no-bare-urls - Bare URL used +MD034: true + +# MD035/hr-style - Horizontal rule style +MD035: + # Horizontal rule style + style: "consistent" + +# MD036/no-emphasis-as-heading/no-emphasis-as-header - Emphasis used instead of a heading +MD036: + # Punctuation characters + punctuation: ".,;:!?。,;:!?" + +# MD037/no-space-in-emphasis - Spaces inside emphasis markers +MD037: true + +# MD038/no-space-in-code - Spaces inside code span elements +MD038: true + +# MD039/no-space-in-links - Spaces inside link text +MD039: true + +# MD040/fenced-code-language - Fenced code blocks should have a language specified +MD040: true + +# MD041/first-line-heading/first-line-h1 - First line in a file should be a top-level heading +MD041: false # MOR +#MD041: +# # Heading level +# level: 1 +# # RegExp for matching title in front matter +# front_matter_title: "^\\s*title\\s*[:=]" + +# MD042/no-empty-links - No empty links +MD042: true + +# MD043/required-headings/required-headers - Required heading structure +MD043: false # MOR +#MD043: +# # List of headings +# headings: [] +# # List of headings +# headers: [] + +# MD044/proper-names - Proper names should have the correct capitalization +MD044: + # List of proper names + names: [] + # Include code blocks + code_blocks: true + +# MD045/no-alt-text - Images should have alternate text (alt text) +MD045: true + +# MD046/code-block-style - Code block style +MD046: + # Block style + style: "consistent" + +# MD047/single-trailing-newline - Files should end with a single newline character +MD047: true + +# MD048/code-fence-style - Code fence style +MD048: + # Code fence syle + style: "consistent" diff --git a/.pre-commit-config.yaml.dist b/.pre-commit-config.yaml.dist new file mode 100644 index 0000000..7fac78c --- /dev/null +++ b/.pre-commit-config.yaml.dist @@ -0,0 +1,64 @@ +################################################################################## +# +# Website As App +# Run any website as standalone desktop application +# +# @author Marcin Orlowski +# @copyright 2023-2024 Marcin Orlowski +# @license https://www.opensource.org/licenses/mit-license.php MIT +# @link https://github.com/MarcinOrlowski/website-as-app +# +##### +# +# Git pre-commit framework config +# +# See https://pre-commit.com for more information about pre-commit. +# See https://pre-commit.com/hooks.html for more available hooks. +# +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + # Prevent giant files from being committed + - id: check-added-large-files + args: ['--maxkb=1555550'] + # This hook checks yaml files for parseable syntax + - id: check-yaml + # forbid files which have a UTF-8 byte-order marker + - id: check-byte-order-marker + # Check for files that would conflict in case-insensitive filesystems + - id: check-case-conflict + # Ensures that (non-binary) executables have a shebang. + - id: check-executables-have-shebangs + # Check for files that contain merge conflict strings + - id: check-merge-conflict + # Prevent addition of new git submodules + - id: forbid-new-submodules + # Replaces or checks mixed line ending + - id: mixed-line-ending + args: ['--fix=no'] + + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.9.0 + hooks: + # Forbid files which have a UTF-8 Unicode replacement character + - id: text-unicode-replacement-char + + - repo: https://github.com/jumanjihouse/pre-commit-hooks + rev: 3.0.0 + hooks: + # Non-executable shell script filename ends in .sh + - id: script-must-have-extension + + - repo: https://github.com/MarcinOrlowski/pre-commit-hooks + rev: 1.3.1 + hooks: + # Checks modified Java files with Checkstyle linter. + # - id: checkstyle-jar + # This hook trims trailing whitespace. + - id: trailing-whitespaces + exclude_types: ['xml'] + args: ['--markdown-linebreak-ext=md', '--fix=yes'] + # Ensures that a file is either empty, or ends with one newline + - id: end-of-file + args: ['--fix=yes'] diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..9718561 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,8 @@ +![WebApp](docs/logo.png) + +# CHANGES + +--- + +* 1.0.0 (2024-01-28) + * Initial release diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..4077f16 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,15 @@ +MIT License + +Copyright (c) 2021 Marcin Orlowski + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c2cb984 --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +![WebApp](docs/logo.png) + +# WebApp + +Run any website as standalone desktop application + +[master](https://github.com/MarcinOrlowski/website-as-app/tree/master) branch: +[![Code lint](https://github.com/MarcinOrlowski/website-as-app/actions/workflows/linter.yml/badge.svg?branch=master)](https://github.com/MarcinOrlowski/website-as-app/actions/workflows/linter.yml) +[![MD Lint](https://github.com/MarcinOrlowski/website-as-app/actions/workflows/markdown.yml/badge.svg?branch=master)](https://github.com/MarcinOrlowski/website-as-app/actions/workflows/markdown.yml) +--- + +Small Python script opening any web page in dedicated window, using embedded QT WebEngine. There are +no visible browser's UI etc., so the that can be useful to turn any website into standalone desktop +application. This is useful if you, as me, would like to have a website run as standalone app, +independently of your main browser which can be beneficial as it gives you separate entry in +window manager or task switcher etc. + +> **IMPORTANT:** This tool is **NOT** turning websites into OFFLINE apps! It's about separating +> each of your key websites i.e. from each other, or gazzilions of your browser's tabs. But you +> still MUST be connected to the Internet for the apps (websites) to work as previously. + +## Installation + +Use `pip` to install the script system-wide: + +```bash +$ pip install website-as-app +``` + +If you want to use virtual environment (which is recommended): + +```bash +$ python -m venv venv +$ source venv/bin/activate +$ pip install website-as-app +``` + +Once app is running, please use `--help` to see all available options, as i.e. custom icon, +window title etc. + +## Usage + +When app is installed system-wide, you can run it from anywhere: + +```bash +$ webapp "https://github.com" +``` + +If you are using virtual environment, there's handy Bash script in [extras/](extras/) directory +which takes care of initializing virtual environment and running the app using that environment. +You simply use `extras/webapp.sh` script instead of `webapp` directly: + +```bash +$ extras/webapp.sh "https://github.com" +``` + +### Configuration + +Available options: + +```bash +webapp -h +usage: webapp [--profile PROFILE] [--name NAME] [--icon ICON] [--zoom ZOOM] [--no-tray] url + +Open any website in standalone window (like it's an app) + +positional arguments: +url The URL to open + +options: +--profile PROFILE Profile name (for cookies isolation etc). Default: "default" +--name NAME, -n NAME Application name (shown as window title) +--geometry GEOMETRY Initial window geometry (in format "WIDTHxHEIGHT+X+Y") +--icon ICON, -i ICON Full path to image file to be used as app icon +--zoom ZOOM, -z ZOOM Initial WebBrowserView zoom factor. Default: 1.0 +--no-tray Disables system tray support (closing window terminates app) +``` + +The most important option is `--profile` which allows you to isolate cookies and app settings +per instance. Any instance using the same profile will have access to the same cookies and +settings. This is useful if you want to run multiple instances of the same app, but with +different accounts. By default `default` profile is used and it's recommended to use different +profile per each app instance. + +## Current limitations + +* Due to security based limitations of embedded `QWebBrowerView` you will not be able + to save any file to your local storage nor filesystem. +* Website's Javascript code cannot write to system clipboard so you might need to manually + select given portion of the site and copy using function from context menu as any buttons + on the page that is using JS to write to the host's clipboard will not currently work. + +## License ## + +* Written and copyrighted ©2023-2024 by Marcin Orlowski +* ResponseBuilder is open-sourced software licensed under + the [MIT license](http://opensource.org/licenses/MIT) diff --git a/artwork/README.md b/artwork/README.md new file mode 100644 index 0000000..3c1fc79 --- /dev/null +++ b/artwork/README.md @@ -0,0 +1,2 @@ +Font: Bebas Neue - Dharma Type +https://www.1001freefonts.com/bebas-neue.font diff --git a/artwork/bebas-neue.zip b/artwork/bebas-neue.zip new file mode 100644 index 0000000..ccc0942 Binary files /dev/null and b/artwork/bebas-neue.zip differ diff --git a/artwork/webapp-square.png b/artwork/webapp-square.png new file mode 100644 index 0000000..0aced20 Binary files /dev/null and b/artwork/webapp-square.png differ diff --git a/artwork/webapp.png b/artwork/webapp.png new file mode 100644 index 0000000..5f4a450 Binary files /dev/null and b/artwork/webapp.png differ diff --git a/artwork/webapp.xcf b/artwork/webapp.xcf new file mode 100644 index 0000000..6625853 Binary files /dev/null and b/artwork/webapp.xcf differ diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..003383b --- /dev/null +++ b/build.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +ACTVATED=0 +if [[ -n "{$VIRTUAL_ENV}" ]]; then + source venv/bin/activate + ACTIVATED=1 +fi + +python3 -m build && + pip uninstall --yes dist/website_as_app-1.0.0-py3-none-any.whl && + pip install dist/website_as_app-1.0.0-py3-none-any.whl + +if [[ ${ACTIVATED} -eq 1 ]]; then + deactivate +fi + diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..b38d60a --- /dev/null +++ b/docs/README.md @@ -0,0 +1,21 @@ +![WebApp](logo.png) + +# WebApp + +Run any website as standalone desktop application + +--- + +Small Python script opening any web page in dedicated window, using embedded QT WebEngine. There are +no visible browser's UI etc., so the that can be useful to turn any website into standalone desktop +application. This is useful if you, as me, would like to have a website run as standalone app, +independently of your main browser which can be beneficial as it gives you separate entry in +window manager or task switcher etc. + +> **IMPORTANT:** This tool is **NOT** turning websites into OFFLINE apps! It's about separating +> each of your key websites i.e. from each other, or gazzilions of your browser's tabs. But you +> still MUST be connected to the Internet for the apps (websites) to work as previously. + +--- + +* [Dev corner](dev.md) diff --git a/docs/dev.md b/docs/dev.md new file mode 100644 index 0000000..8188a67 --- /dev/null +++ b/docs/dev.md @@ -0,0 +1,88 @@ +![WebApp](logo.png) + +# Dev corner + +[« Back to main menu](README.md) +1. [Profiles](#profiles) +2. [Building the package](#building-the-package) + +--- + +## Profiles + +Each instance can be separated from others by using dedicated "profile". Any +instance using the same profile, will have access to the same shared data like +cookies or app settings. + +### Location + +If for any reason you would like to reset the profile, you can do it by removing the profile +directory: + +Windows + + ``` + %USERPROFILE%\AppData\Local\MarcinOrlowski\WebsiteAsApp\\ + ``` + +Linux + + ``` + ~/.local/share/MarcinOrlowski/WebsiteAsApp// + ``` + +MacOS + + ``` + ??? (PLEASE ASSIST) + ``` + +--- + +## Building the package + +1. Checkout the source code: + +```bash +$ cd +$ git clone .... +$ cd website-as-app +``` + +2. Setup its runtime environment (NOTE: if you are using different shell than `bash` (i.e. `fish`), + please use correct `activate` script from `venv/bin/` directory): + +```bash +$ python -m venv venv +$ source venv/bin/activate # User right one for your shell +``` + +3. Install all the dependencies: + +```bash +(venv) $ pip install -r requirements-dev.txt +``` + +4. Build the package: + +```bash +(venv) $ python -m build +``` + +5. Install package locally for testing. + We intentionally ignore `install --upgrade` while planting new build, as we need to ensure no + cached bytecode from previous version remains (which could be the case as we do not increment the + version each build). + +```bash +(venv) $ pip uninstall --yes dist/website_as_app-1.0.0-py3-none-any.whl +(venv) $ pip install dist/website_as_app-1.0.0-py3-none-any.whl +``` + +6. Test the app + +```bash +(venv) $ webapp -h +usage: webapp [-h] [--name TITLE] [--icon ICON] [--profile PROFILE] [--zoom ZOOM] url +... +``` diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 0000000..8e93c33 Binary files /dev/null and b/docs/logo.png differ diff --git a/docs/webapp.png b/docs/webapp.png new file mode 100644 index 0000000..80c6a2a Binary files /dev/null and b/docs/webapp.png differ diff --git a/extras/webapp.sh b/extras/webapp.sh new file mode 100755 index 0000000..6dc3524 --- /dev/null +++ b/extras/webapp.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +################################################################################## +# +# Website As App +# Run any website as standalone desktop application +# +# @author Marcin Orlowski +# @copyright 2023-2024 Marcin Orlowski +# @license https://www.opensource.org/licenses/mit-license.php MIT +# @link https://github.com/MarcinOrlowski/website-as-app +# +##### + +set -uo pipefail + +function runWebAppInVenv { + readonly ROOT_DIR="$(dirname "$(realpath "${0}")")" + readonly VENV_NAME="venv" + readonly VENV_PATH="../venv/" + + pushd "${ROOT_DIR}" > /dev/null + + local ACTIVATED_VENV= + if [[ -z "${VIRTUAL_ENV:-}" ]]; then + if [[ ! -d "${VENV_PATH}" ]]; then + echo "Virtual env ${VENV_NAME} not found in ${VENV_PATH}" + exit 100 + fi + source "${VENV_PATH}/bin/activate" + ACTIVATED_VENV="YES" + fi + + webapp $@ + + if [[ "${ACTIVATED_VENV:-}" == "YES" ]]; then + deactivate + fi + + popd > /dev/null +} + +runWebAppInVenv $@ diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..9be7782 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,5 @@ +-r requirements.txt +twine +wheel +build +wemake-python-styleguide diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..62c3030 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +PySide6>=6.6.1 +PyQtWebEngine>=5.15.6 diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..4910c44 --- /dev/null +++ b/setup.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 + +""" +################################################################################## +# +# Website As App +# Run any website as standalone desktop application +# +# @author Marcin Orlowski +# @copyright 2023-2024 Marcin Orlowski +# @license https://www.opensource.org/licenses/mit-license.php MIT +# @link https://github.com/MarcinOrlowski/website-as-app +# +##### +# +# https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html +# +# python -m venv venv +# source venv/bin/activate +# pip install -r requirements-dev.txt +# python -m build +# # Reinstall the app (do not do "install --upgrade" as cached bytecode can not be updated) +# pip uninstall --yes dist/website_as_app-1.0.0-py3-none-any.whl +# # intentionally no --upgrade for install to endforce conflict if not uninstalled fully first. +# pip install dist/website_as_app-1.0.0-py3-none-any.whl +# twine upload dist/* +# +""" + +from setuptools import setup, find_packages + +from websiteapp.const import Const + +with open('README.md', 'r') as fh: + readme = fh.read() + +setup( + name=Const.APP_PROJECT_NAME, + version=Const.APP_VERSION, + packages=find_packages(), + + install_requires=[ + 'argparse>=1.4.0', + 'PySide6', + 'PyQtWebEngine', + ], + entry_points={ + 'console_scripts': [ + 'webapp = websiteapp.webapp:WebApp.run', + 'runasapp = websiteapp.webapp:WebApp.run', + ], + }, + + package_data={ + 'websiteapp': [ + 'icons/default.png', + 'icons/logo.png', + ], + }, + + author='Marcin Orlowski', + author_email='mail@marcinOrlowski.com', + description=Const.APP_DESCRIPTION, + long_description=readme, + long_description_content_type='text/markdown', + url=Const.APP_URL, + keywords='webapp desktop app', + project_urls={ + 'Bug Tracker': f'{Const.APP_URL}/issues/', + 'Documentation': Const.APP_URL, + 'Source Code': Const.APP_URL, + }, + # https://choosealicense.com/ + license='MIT License', + classifiers=[ + 'Programming Language :: Python :: 3', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + ], +) diff --git a/websiteapp/__init__.py b/websiteapp/__init__.py new file mode 100644 index 0000000..e4a300e --- /dev/null +++ b/websiteapp/__init__.py @@ -0,0 +1,13 @@ +""" +################################################################################## +# +# Website As App +# Run any website as standalone desktop application +# +# @author Marcin Orlowski +# @copyright 2023-2024 Marcin Orlowski +# @license https://www.opensource.org/licenses/mit-license.php MIT +# @link https://github.com/MarcinOrlowski/website-as-app +# +##### +""" diff --git a/websiteapp/about.py b/websiteapp/about.py new file mode 100755 index 0000000..8446c08 --- /dev/null +++ b/websiteapp/about.py @@ -0,0 +1,89 @@ +""" +################################################################################## +# +# Website As App +# Run any website as standalone desktop application +# +# @author Marcin Orlowski +# @copyright 2023-2024 Marcin Orlowski +# @license https://www.opensource.org/licenses/mit-license.php MIT +# @link https://github.com/MarcinOrlowski/website-as-app +# +################################################################################## +""" +import webbrowser +import importlib.resources as pkg_resources + +from PySide6.QtCore import Qt +from PySide6.QtGui import QPixmap +from PySide6.QtWidgets import QApplication, QVBoxLayout, QLabel, QDialog, QPushButton, QHBoxLayout + +from websiteapp.const import Const + + +class About(QDialog): + def __init__(self): + super().__init__() + + self.app = QApplication.instance() + + # Set the layout for the dialog + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 6) + layout.setSpacing(0) + self.setLayout(layout) + self.setWindowTitle(f"About {Const.APP_NAME}") + + # Create a label for the logo image + logo_label = QLabel(self) + with pkg_resources.path('websiteapp.icons', 'logo.png') as icon_path: + icon_file = str(icon_path) + logo_label.setPixmap(QPixmap(icon_file)) + layout.addWidget(logo_label) + + # Create a label for the credits text + label_layout = QVBoxLayout() + + label = QLabel(f'{Const.APP_NAME} v{Const.APP_VERSION}', self) + label.setTextFormat(Qt.RichText) + label.setAlignment(Qt.AlignCenter) + font = label.font() + font.setPointSize(font.pointSize() * 1.7) + label.setFont(font) + label_layout.addWidget(label) + + label = QLabel(f'©2023-{Const.APP_YEAR} Marcin Orlowski', self) + label.setTextFormat(Qt.RichText) + label.setAlignment(Qt.AlignCenter) + font = label.font() + font.setPointSize(font.pointSize() * 1.2) + label.setFont(font) + label_layout.addWidget(label) + + label = QLabel(Const.APP_URL, self) + # label.setTextFormat(Qt.RichText) + label.setAlignment(Qt.AlignCenter) + label_layout.addWidget(label) + + layout.addLayout(label_layout) + + # # Create a horizontal layout for the buttons + bt_website = QPushButton("Open project website", self) + bt_website.clicked.connect(self.on_ok_clicked) + bt_close = QPushButton("Close", self) + bt_close.clicked.connect(self.on_cancel_clicked) + bt_close.setFocus() + + button_layout = QHBoxLayout() + button_layout.setContentsMargins(6, 6, 6, 6) + button_layout.setSpacing(6) + button_layout.addWidget(bt_website) + button_layout.addWidget(bt_close) + layout.addLayout(button_layout) + + def on_ok_clicked(self): + webbrowser.open(Const.APP_URL) + self.reject() + + def on_cancel_clicked(self): + self.reject() diff --git a/websiteapp/const.py b/websiteapp/const.py new file mode 100644 index 0000000..57309a5 --- /dev/null +++ b/websiteapp/const.py @@ -0,0 +1,30 @@ +""" +################################################################################## +# +# Website As App +# Run any website as standalone desktop application +# +# @author Marcin Orlowski +# @copyright 2023-2024 Marcin Orlowski +# @license https://www.opensource.org/licenses/mit-license.php MIT +# @link https://github.com/MarcinOrlowski/website-as-app +# +##### +""" + +from typing import List + + +class Const(object): + APP_NAME: str = 'Website As App' + APP_PROJECT_NAME: str = 'website-as-app' + APP_VERSION: str = '1.0.0' + APP_URL: str = 'https://github.com/MarcinOrlowski/website-as-app/' + APP_DESCRIPTION: str = 'Opens any web site as standalone desktop app.' + APP_YEAR: int = 2024 + + APP_DESCRIPTION: List[str] = [ + f'{APP_NAME} v{APP_VERSION} * Copyright 2023-{APP_YEAR} by Marcin Orlowski.', + APP_DESCRIPTION, + APP_URL, + ] diff --git a/websiteapp/icons/default.png b/websiteapp/icons/default.png new file mode 100644 index 0000000..5f4a450 Binary files /dev/null and b/websiteapp/icons/default.png differ diff --git a/websiteapp/icons/logo.png b/websiteapp/icons/logo.png new file mode 100644 index 0000000..07edf1c Binary files /dev/null and b/websiteapp/icons/logo.png differ diff --git a/websiteapp/utils.py b/websiteapp/utils.py new file mode 100755 index 0000000..11ce3ab --- /dev/null +++ b/websiteapp/utils.py @@ -0,0 +1,107 @@ +""" +################################################################################## +# +# Website As App +# Run any website as standalone desktop application +# +# @author Marcin Orlowski +# @copyright 2023-2024 Marcin Orlowski +# @license https://www.opensource.org/licenses/mit-license.php MIT +# @link https://github.com/MarcinOrlowski/website-as-app +# +################################################################################## +""" +import importlib.resources as pkg_resources +import os +import re +from typing import Optional + +import argparse +from PySide6.QtGui import QIcon + + +class Utils(object): + @staticmethod + def get_icon(icon: Optional[str] = None) -> QIcon: + """ + Attempts to construct QIcon object from given icon file path. If icon file is not given + or does not exist, default icon is used (so it always return valid QIcon object). + + :param icon: Path to icon file (in format supported by QT, i.e. PNG) + :return: QIcon object + """ + icon_file = None + + if icon and os.path.exists(icon): + icon_file = icon + + if icon_file is None: + with pkg_resources.path('websiteapp.icons', 'default.png') as icon_path: + icon_file = str(icon_path) + + return QIcon(icon_file) + + @staticmethod + def parse_geometry(geometry_string: str) -> (int, int, int, int): + """ + Parse the geometry string and return the width, height, x, and y values. + + :param geometry_string: A string representing the geometry in the format WIDTHxHEIGHT+X+Y. + + :return: A tuple containing the x, y, width and height values. + :raises ValueError: If the geometry string format is incorrect. + """ + match = re.search(r'^(\d+)x(\d+)\+(\d+)\+(\d+)$', geometry_string) + if not match: + raise ValueError(f"Invalid geometry. Expected WIDTHxHEIGHT+X+Y, got '{geometry_string}") + + width = int(match.group(1)) + if width < 1: + raise ValueError(f"Invalid geometry. Width must be greater than 0, got '{width}'") + height = int(match.group(2)) + if height < 1: + raise ValueError(f"Invalid geometry. Height must be greater than 0, got '{height}'") + x = int(match.group(3)) + if x < 0: + raise ValueError(f"Invalid geometry. X must be greater than or equal to 0, got '{x}'") + y = int(match.group(4)) + if y < 0: + raise ValueError(f"Invalid geometry. Y must be greater than or equal to 0, got '{y}'") + + return x, y, width, height + + @staticmethod + def handle_args(): + """ + Create an argument parser to handle command line arguments for opening a website in a + standalone window. + + :return: The parsed command line arguments. + """ + parser = argparse.ArgumentParser( + description="Open any website in standalone window (like it's an app)") + parser.add_argument('url', type=str, help='The URL to open') + + parser.add_argument('--profile', '-p', type=str, default='default', + help='Profile name (for cookies isolation etc). Default: "%(default)s"') + + # Can't use "title" as it is swallowed by QT and used for window title which cannot be later + # changed. So we use "name" instead. + parser.add_argument('--name', '-n', type=str, default=None, + help='Application name (shown as window title)') + + parser.add_argument('--icon', '-i', type=str, default=None, + help='Full path to PNG image file to be used as app icon') + parser.add_argument('--geometry', '-g', type=str, default='450x600+0+0', + help='Initial window ("WIDTHxHEIGHT+X+Y"). Default: "%(default)s"') + parser.add_argument('--zoom', '-z', type=float, default="1.0", + help='WebView zoom factor. Default: %(default)s') + parser.add_argument('--no-tray', action='store_true', + help='Disables docking app in system tray') + # parser.add_argument('--minimized', '-m', action='store_true', + # help='Starts app minimized', target='minimized') + + parser.add_argument('--debug', '-d', action='store_true', + help='Makes app print more debug messages during execution') + + return parser.parse_args() diff --git a/websiteapp/webapp.py b/websiteapp/webapp.py new file mode 100755 index 0000000..17d2670 --- /dev/null +++ b/websiteapp/webapp.py @@ -0,0 +1,167 @@ +""" +################################################################################## +# +# Website As App +# Run any website as standalone desktop application +# +# @author Marcin Orlowski +# @copyright 2023-2024 Marcin Orlowski +# @license https://www.opensource.org/licenses/mit-license.php MIT +# @link https://github.com/MarcinOrlowski/website-as-app +# +################################################################################## +""" +import sys +from typing import Optional + +from PySide6.QtCore import QUrl +from PySide6.QtGui import QAction +from PySide6.QtWebEngineCore import QWebEngineProfile, QWebEnginePage, QWebEngineSettings +from PySide6.QtWebEngineWidgets import QWebEngineView +from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QSystemTrayIcon, \ + QMenu + +from websiteapp.about import About +from websiteapp.const import Const +from websiteapp.utils import Utils + + +class WebApp(QMainWindow): + about_dialog: Optional[About] = None + + def __init__(self): + super().__init__() + + self.app = QApplication.instance() + self.args = Utils.handle_args() + + # Set window geometry + x, y, width, height = Utils.parse_geometry(self.args.geometry) + self.setGeometry(x, y, width, height) + self.dbug(f'Geometry: {width}x{height}+{x}+{y}') + + app_icon = Utils.get_icon(self.args.icon) + self.setWindowIcon(app_icon) + if self.args.no_tray: + self.app.setQuitOnLastWindowClosed(True) # Ensure we quit when last window is closed + else: + self.setup_tray_icon(app_icon) + + window_title = self.args.name if self.args.name else self.args.url + window_title += f' ({self.args.profile})' if self.args.debug else '' + self.setWindowTitle(f'{window_title} · {Const.APP_NAME}') + + # Create a persistent profile (cookie jar etc.) + self.dbug(f'Profile: {self.args.profile}') + self.profile = QWebEngineProfile(self.args.profile, self) + self.page = QWebEnginePage(self.profile, self) + + self.browser = QWebEngineView(self) + web_settings = self.browser.settings() + web_settings.setAttribute( + QWebEngineSettings.WebAttribute.JavascriptCanAccessClipboard, True) + + self.browser.setPage(self.page) + self.browser.setZoomFactor(self.args.zoom) + + self.dbug(f'URL: {self.args.url}') + self.browser.setUrl(QUrl(self.args.url)) + + # Window layout + layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + layout.addWidget(self.browser) + + central_widget = QWidget() + central_widget.setLayout(layout) + self.setCentralWidget(central_widget) + + def closeEvent(self, event) -> None: + """ + Override closeEvent to hide the window instead of closing it and hides the window instead. + + :param event: + """ + if not self.args.no_tray: + self.hide() + event.ignore() + else: + super().closeEvent(event) + + def setup_tray_icon(self, icon) -> None: + """ + This method is used to set up the system tray icon for the application. + + :param icon: QPixmap object representing the icon that will be used in the system tray. + """ + tray_menu = QMenu() + + tray_icon = QSystemTrayIcon(icon, self.app) + tray_icon.setContextMenu(tray_menu) + tray_icon.activated.connect(self.on_tray_icon_activated) + + about_label = f'About {Const.APP_NAME}' + about_action = QAction(about_label, self.app) + about_action.triggered.connect(self.open_about_dialog) + tray_menu.addAction(about_action) + + quit_label = f'Quit {self.args.name}' if self.args.name else 'Quit' + quit_action = QAction(quit_label, self.app) + quit_action.triggered.connect(self.quit_app) + tray_menu.addAction(quit_action) + + tray_icon.show() + + def quit_app(self) -> None: + """ + Closes the application. + """ + self.app.quit() + + def open_about_dialog(self) -> None: + """ + Opens the about dialog. + """ + if not self.about_dialog: + self.about_dialog = About() + + if not self.about_dialog.isVisible(): + self.about_dialog.show() + self.about_dialog.activateWindow() + + def on_tray_icon_activated(self, reason) -> None: + """ + Callback invoked when application icon in system tray is clicked. + """ + if reason == QSystemTrayIcon.ActivationReason.Trigger: + # Check for left-click (Trigger) + self.toggle_window() + + def toggle_window(self) -> None: + """ + Toggles the visibility of the window. + """ + self.hide() if self.isVisible() else self.show() + + # ############################################################################################ # + + def dbug(self, msg: str) -> None: + if self.args.debug: + print(msg, file=sys.stderr) + + # ############################################################################################ # + + @staticmethod + def run() -> None: + """ + Application entry point. When renamed, ensure setup.py's reference is updated as well. + """ + app = QApplication(sys.argv) + app.setOrganizationName("MarcinOrlowski") + app.setApplicationName("Website As App") + + window = WebApp() + window.show() + + app.exec()