From 9452627417098d60480554f5580efbd690f7f516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20St=C3=BCrmer?= Date: Thu, 21 Dec 2023 05:26:16 +0100 Subject: [PATCH 1/3] remove old project structure --- .devcontainer/Dockerfile.dev | 19 - .devcontainer/devcontainer.json | 43 - .gitignore | 73 +- .vscode/extensions.json | 7 - .vscode/launch.json | 12 - .vscode/settings.json | 46 - .vscode/tasks.json | 92 -- CHANGELOG.md | 13 + CONTRIBUTING.md | 84 ++ Dockerfile | 12 - LICENSE.md | 851 ++++++++++++++---- README.md | 110 ++- frontend/__init__.py | 1 - frontend/__main__.py | 87 -- frontend/application.py | 49 - frontend/base/__init__.py | 11 - frontend/base/handler.py | 15 - frontend/base/oauth.py | 182 ---- frontend/base/request.py | 151 ---- frontend/base/route.py | 10 - frontend/base/state.py | 16 - frontend/config.py | 67 -- frontend/const.py | 14 - frontend/frontend.py | 192 ---- frontend/i18n.py | 106 --- frontend/locales/base.pot | 24 - frontend/locales/de/LC_MESSAGES/messages.mo | Bin 435 -> 0 bytes frontend/locales/de/LC_MESSAGES/messages.po | 25 - .../locales/en_US/LC_MESSAGES/messages.mo | Bin 425 -> 0 bytes .../locales/en_US/LC_MESSAGES/messages.po | 25 - .../locales/vi_VN/LC_MESSAGES/messages.mo | Bin 410 -> 0 bytes .../locales/vi_VN/LC_MESSAGES/messages.po | 22 - .../locales/zh_TW/LC_MESSAGES/messages.mo | Bin 400 -> 0 bytes .../locales/zh_TW/LC_MESSAGES/messages.po | 22 - frontend/static/browserconfig.xml | 2 - frontend/static/custom-icon.png | Bin 15999 -> 0 bytes frontend/static/favicon.ico | Bin 1150 -> 0 bytes frontend/static/favicon.png | Bin 2427 -> 0 bytes frontend/templates/base.jinja2 | 72 -- frontend/templates/error.jinja2 | 28 - frontend/templates/index.jinja2 | 29 - frontend/templates/index/grid.jinja2 | 22 - frontend/templates/index/shared.jinja2 | 11 - frontend/templates/index/user.jinja2 | 171 ---- frontend/templates/login_required.jinja2 | 96 -- frontend/templates/partials/app.jinja2 | 29 - frontend/templates/partials/footer.jinja2 | 41 - frontend/templates/partials/header.jinja2 | 37 - frontend/views/callback.py | 57 -- frontend/views/index.py | 53 -- frontend/views/login.py | 36 - frontend/views/logout.py | 28 - frontend/views/photo.py | 42 - frontend/views/settings.py | 24 - frontend/views/shared.py | 46 - frontend/views/user.py | 85 -- frontend/webserver.py | 173 ---- requirements.txt | 6 - requirements_test.txt | 12 - server.py | 8 - setup.cfg | 42 - setup.py | 56 -- tests/__init__.py | 1 - tests/conftest.py | 13 - tests/login/test_routes_login.py | 48 - tests/test_internationalization.py | 26 - tests/test_oauth_flow.py | 4 - tests/test_routes_static.py | 12 - 68 files changed, 857 insertions(+), 2834 deletions(-) delete mode 100644 .devcontainer/Dockerfile.dev delete mode 100644 .devcontainer/devcontainer.json delete mode 100644 .vscode/extensions.json delete mode 100644 .vscode/launch.json delete mode 100644 .vscode/settings.json delete mode 100644 .vscode/tasks.json create mode 100644 CONTRIBUTING.md delete mode 100644 Dockerfile delete mode 100644 frontend/__init__.py delete mode 100644 frontend/__main__.py delete mode 100644 frontend/application.py delete mode 100644 frontend/base/__init__.py delete mode 100644 frontend/base/handler.py delete mode 100644 frontend/base/oauth.py delete mode 100644 frontend/base/request.py delete mode 100644 frontend/base/route.py delete mode 100644 frontend/base/state.py delete mode 100644 frontend/config.py delete mode 100644 frontend/const.py delete mode 100644 frontend/frontend.py delete mode 100644 frontend/i18n.py delete mode 100644 frontend/locales/base.pot delete mode 100644 frontend/locales/de/LC_MESSAGES/messages.mo delete mode 100644 frontend/locales/de/LC_MESSAGES/messages.po delete mode 100644 frontend/locales/en_US/LC_MESSAGES/messages.mo delete mode 100644 frontend/locales/en_US/LC_MESSAGES/messages.po delete mode 100644 frontend/locales/vi_VN/LC_MESSAGES/messages.mo delete mode 100644 frontend/locales/vi_VN/LC_MESSAGES/messages.po delete mode 100644 frontend/locales/zh_TW/LC_MESSAGES/messages.mo delete mode 100644 frontend/locales/zh_TW/LC_MESSAGES/messages.po delete mode 100644 frontend/static/browserconfig.xml delete mode 100644 frontend/static/custom-icon.png delete mode 100644 frontend/static/favicon.ico delete mode 100644 frontend/static/favicon.png delete mode 100644 frontend/templates/base.jinja2 delete mode 100644 frontend/templates/error.jinja2 delete mode 100644 frontend/templates/index.jinja2 delete mode 100644 frontend/templates/index/grid.jinja2 delete mode 100644 frontend/templates/index/shared.jinja2 delete mode 100644 frontend/templates/index/user.jinja2 delete mode 100644 frontend/templates/login_required.jinja2 delete mode 100644 frontend/templates/partials/app.jinja2 delete mode 100644 frontend/templates/partials/footer.jinja2 delete mode 100644 frontend/templates/partials/header.jinja2 delete mode 100644 frontend/views/callback.py delete mode 100644 frontend/views/index.py delete mode 100644 frontend/views/login.py delete mode 100644 frontend/views/logout.py delete mode 100644 frontend/views/photo.py delete mode 100644 frontend/views/settings.py delete mode 100644 frontend/views/shared.py delete mode 100644 frontend/views/user.py delete mode 100644 frontend/webserver.py delete mode 100644 requirements.txt delete mode 100644 requirements_test.txt delete mode 100644 server.py delete mode 100644 setup.cfg delete mode 100644 setup.py delete mode 100644 tests/__init__.py delete mode 100644 tests/conftest.py delete mode 100644 tests/login/test_routes_login.py delete mode 100644 tests/test_internationalization.py delete mode 100644 tests/test_oauth_flow.py delete mode 100644 tests/test_routes_static.py diff --git a/.devcontainer/Dockerfile.dev b/.devcontainer/Dockerfile.dev deleted file mode 100644 index 2f7c844..0000000 --- a/.devcontainer/Dockerfile.dev +++ /dev/null @@ -1,19 +0,0 @@ -FROM mcr.microsoft.com/vscode/devcontainers/python:3.10 - -# Install zsh -ARG INSTALL_ZSH="true" - -# setup non-root user -ARG USERNAME=vscode -ARG USER_UID=1000 -ARG USER_GID=$USER_UID - -WORKDIR /workspaces - -COPY . . - -# Install Python dependencies -RUN pip3 --disable-pip-version-check --no-cache-dir install -r requirements_test.txt - -# Install frontend -RUN python3 setup.py install diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index e901607..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "Photos.network frontend", - "build": { - "dockerfile": "Dockerfile.dev", - "context": ".." - }, - "appPort": [ - "7778:7778" - ], - "postCreateCommand": "mkdir -p config && pip3 install -r requirements_test.txt", - "runArgs": ["-e", "GIT_EDITOR=code --wait"], - "extensions": [ - "ms-python.python", - "ms-python.vscode-pylance", - "esbenp.prettier-vscode" - ], - "remoteUser": "vscode", - "settings": { - "editor.formatOnPaste": true, - "editor.formatOnSave": true, - "editor.formatOnType": true, - "python.pythonPath": "/usr/local/bin/python", - "python.languageServer": "Pylance", - "python.linting.lintOnSave": true, - "python.linting.enabled": true, - "python.linting.pylintEnabled": true, - "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint", - "python.linting.flake8Enabled": true, - "python.linting.flake8Path": "/usr/local/bin/flake8", - "python.linting.flake8Args": [ - "--max-line-length=130" - ], - "python.testing.pytestEnabled": true, - "python.testing.pytestPath": "/usr/local/bin/pytest", - "python.formatting.provider": "black", - "python.formatting.blackArgs": [ - "--line-length", - "120" - ], - "python.formatting.blackPath": "/usr/local/py-utils/bin/black", - "files.trimTrailingWhitespace": true, - } -} diff --git a/.gitignore b/.gitignore index a012844..add8cdd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,67 +1,16 @@ -# pytest -.pytest_cache -.cache +# Generated by Cargo +# will have compiled files and executables +/target/ +pkg -# GITHUB Proposed Python stuff: -*.py[cod] +# These are backup files generated by rustfmt +**/*.rs.bk -# C extensions -*.so - -# Packages -*.egg -*.egg-info -dist -build -eggs -.eggs -parts -bin -var -sdist -develop-eggs -.installed.cfg -lib -lib64 -pip-wheel-metadata - -# Logs -*.log* -pip-log.txt - -# Unit test / coverage reports -.coverage -.tox -coverage.xml -nosetests.xml -htmlcov/ -test-reports/ -test-results.xml -test-output.xml - -# venv stuff -pyvenv.cfg -pip-selfcheck.json -venv -.venv -Pipfile* -share/* -/Scripts/ - -# Visual Studio Code -.vscode/* -!.vscode/cSpell.json -!.vscode/extensions.json -!.vscode/launch.json -!.vscode/settings.json -!.vscode/tasks.json -.env +# node e2e test tools and outputs +node_modules/ +test-results/ +end2end/playwright-report/ +playwright/.cache/ # Built docs docs/build - -# data directory -data/ - -# config directory -config/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 42f796f..0000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "recommendations": [ - "esbenp.prettier-vscode", - "ms-python.python", - "esbenp.prettier-vscode" - ] -} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 5288844..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Photos.network", - "type": "python", - "request": "launch", - "program": "server.py", - "console": "integratedTerminal" - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index af989ea..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "editor.formatOnSave": false, - "editor.formatOnPaste": true, - "editor.formatOnType": true, - "editor.fontFamily": "JetBrains Mono", - "editor.quickSuggestionsDelay": 1500, - "editor.detectIndentation": true, - "editor.insertSpaces": true, - "editor.tabSize": 4, - "editor.codeActionsOnSave": { - "source.organizeImports": true - }, - "files.exclude": { - ".gitattributes": true, - "**/__pycache__": true, - "**/.pytest_cache": true, - "**/.mypy_cache": true - }, - "breadcrumbs.enabled": true, - "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python3", - "python.linting.lintOnSave": true, - "python.linting.enabled": true, - "python.linting.pylintEnabled": false, - "python.linting.flake8Enabled": true, - "python.linting.flake8Path": "/usr/local/bin/flake8", - "python.linting.flake8Args": [ - "--max-line-length=130" - ], - "python.testing.pytestEnabled": true, - "python.testing.pytestPath": "/usr/local/bin/pytest", - "python.testing.pytestArgs": [ - "--no-cov" - ], - "python.formatting.provider": "black", - "python.formatting.blackArgs": [ - "--line-length", - "120" - ], - "[python]": { - "editor.tabSize": 4, - "editor.formatOnSave": true - }, - "python.terminal.activateEnvInCurrentTerminal": true, - "python.testing.unittestEnabled": false, - "python.testing.nosetestsEnabled": false -} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 20d5033..0000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "Photos.network Frontend", - "type": "shell", - "command": "python3 server.py", - "group": "build", - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Pytest", - "type": "shell", - "command": "pytest --timeout=10 tests", - "group": { - "kind": "test", - "isDefault": true - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Flake8", - "type": "shell", - "command": "pre-commit run flake8 --all-files", - "dependsOn": [ - "Install all Test Requirements" - ], - "group": { - "kind": "test", - "isDefault": true - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Pylint", - "type": "shell", - "command": "pylint core", - "dependsOn": [ - "Install all Requirements" - ], - "group": { - "kind": "test", - "isDefault": true - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Install all Requirements", - "type": "shell", - "command": "pip3 install -r requirements.txt", - "group": { - "kind": "build", - "isDefault": true - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Install all Test Requirements", - "type": "shell", - "command": "pip3 install -r requirements_test.txt", - "group": { - "kind": "build", - "isDefault": true - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - } - ] -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8851dd7..30d15c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.3.0] - 2023-12-21 +### Changed +- Rewrite in Rust with Leptos Framework + + ## [0.2.2] - 2022-06-15 ### Fixed - persist refreshed token @@ -27,6 +39,7 @@ - draft of profile page +[0.3.0]: https://github.com/photos-network/frontend/compare/Release/v0.2.2...Release/v0.3.0 [0.2.2]: https://github.com/photos-network/frontend/compare/Release/v0.2.1...Release/v0.2.2 [0.2.1]: https://github.com/photos-network/frontend/compare/Release/v0.2.0...Release/v0.2.1 [0.2.0]: https://github.com/photos-network/frontend/compare/Release/v0.1.0...Release/v0.2.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b155351 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,84 @@ +# Welcome to Photos.network + +This is a **FOSS** (free and open-source software) and lives from contributions of the community. + +There are many ways to contribute: + + * 📣 Spread the project or its apps to the world + * ✍️ Writing tutorials and blog posts + * 📝 Create or update the documentation + * 🐛 Submit bug reports + * 💡 Adding ideas and feature requests to Discussions + * 👩‍🎨 Create designs or UX flows + * 🧑‍💻 Contribute code or review PRs + + + +## 📜 Ground Rules + +A community like this should be **open**, **considerate** and **respectful**. + +Behaviours that reinforce these values contribute to a positive environment, and include: + + * **Being open**. Members of the community are open to collaboration. + * **Focusing on what is best for the community**. We're respectful of the processes set forth in the community, and we work within them. + * **Acknowledging time and effort**. We're respectful and thoughtful when addressing the efforts of others, keeping in mind that often times the labor was completed simply for the good of the community. + * **Being respectful of differing viewpoints and experiences**. We're receptive to constructive comments and criticism, as the experiences and skill sets of other members contribute to the whole of our efforts. + * **Showing empathy towards other community members**. We're attentive in our communications, whether in person or online, and we're tactful when approaching differing views. + * **Being considerate**. Members of the community are considerate of their peers. + * **Being respectful**. We're respectful of others, their positions, their skills, their commitments, and their efforts. + * **Gracefully accepting constructive criticism**. When we disagree, we are courteous in raising our issues. + * **Using welcoming and inclusive language**. We're accepting of all who wish to take part in our activities, fostering an environment where anyone can participate and everyone can make a difference. + + + +## 🧑‍💻 Code Contribution + +To contribute code to the repository, you don't need any permissions. +First start by forking the repository, clone and checkout your clone and start coding. +When you're happy with your changes, create Atomic commits on a **new feature branch** and push it to ***your*** fork. + +Atomic commits will make it easier to track down regressions. Also, it enables the ability to cherry-pick or revert a change if needed. + +1. Fork it (https://github.com/photos-network/core/fork) +2. Create a new feature branch (`git checkout -b feature/fooBar`) +3. Commit your changes (`git commit -am 'Add some fooBar'`) +4. Push to the branch (`git push origin feature/fooBar`) +5. Create a new Pull Request + + + +## 🐛 How to report a bug + +> If you find a security vulnerability, do NOT open an issue. Email [security@photos.network](mailto:security@photos.network) instead. See [SECURITY.md](./SECURITY.md) for details. + +1. Open the [issues tab](https://github.com/photos-network/core/issues) on github +2. Click on [New issue](https://github.com/photos-network/core/issues/new/choose) +3. Choose the bug report 🐛 template and fill out all required fields + + + +## 💡 How to suggest a feature or enhancement + +Check [open issues](https://github.com/photos-network/core/issues) for a list of proposed features. + +If your suggestion can not be found already, see if it is already covered by our [Roadmap](https://github.com/photos-network/core/#roadmap). + + + +## 📟 Communication + +To get in touch with the community or write use on Mastodon: [@photos@mastodon.cloud](https://mastodon.cloud/@photos). + + + +## 💾 Technology + +The project is written in [Rust](https://rust-lang.org/) + +Underneath it is using these frameworks: + +* [tokio](https://github.com/tokio-rs/tokio) - an asynchronous runtime +* [tower](https://github.com/tower-rs/tower) - for networking +* [axum](https://github.com/tokio-rs/axum) - as web framework +* [abi_stable](https://github.com/rodrimati1992/abi_stable_crates) - FFI for dynamic library loading diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 3b4b78f..0000000 --- a/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM python:3.10 -LABEL "description"="Photos.network web frontend" -LABEL "version"="0.2.2" -LABEL "maintainer"="github.com/photos-network" - -WORKDIR /app - -ADD . . - -RUN python3 setup.py install - -CMD [ "python3", "/usr/local/bin/frontend" ] diff --git a/LICENSE.md b/LICENSE.md index 901233a..c796e17 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,190 +1,661 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -Copyright 2020 Photos network developers - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + Photos.network · A privacy first, self-hosted photo storage and sharing service for fediverse. + Copyright 2020 Photos network developers + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md index 6b34cd2..5512004 100644 --- a/README.md +++ b/README.md @@ -2,46 +2,112 @@ [![License](https://img.shields.io/github/license/photos-network/frontend)](./LICENSE.md) [![GitHub contributors](https://img.shields.io/github/contributors/photos-network/frontend?color=success)](https://github.com/photos.network/core/graphs/contributors) -[![Discord](https://img.shields.io/discord/793235453871390720)](https://discord.gg/dGFDpmWp46) -[Photos.network](https://photos.network) is an open source project for self hosted photo management. +Photos.network](https://photos.network) is a free and open source, privacy first, self-hosted photo storage and sharing service for fediverse. + Its core features are: - - Share photos with friends, family or public - - Filter / Search photos by attributes like location or date - - Group photos by objects like people of objects +- Share photos with friends, family or public +- Filter / Search photos by attributes like location or date +- Group photos by their content like people or objects +- Upload photos and videos without resolution or quality constraints + ## Frontend -This repository contains the official App-like web frontend. +This repository contains the official App-like web frontend. of the project. + +It is responsible for interacting with the core system via REST calls. +- **Overview** of the users media items in a grid +- **Albums** the users has access to +- **Upload** new media items +- **Details** of items like location, date and time taken. -Its containing -- a grid of the users photos -- the users profile -## Development +## 🧩 Contribution -### Visual Studio Code -The fastest start into development can be archived by using [Visual Studio Code](https://code.visualstudio.com/) and [Docker](https://www.docker.com/get-started). +This is a free and open project and lives from contributions of the community. -1. Install [Docker](https://www.docker.com/get-started) -2. Install [Visual Studio Code](https://code.visualstudio.com/) -3. Install [Visual Studio Code Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) -4. Clone and Open this repository in Visual Studio Code -5. Click the "Reopen in Container" Dialog +See our [Contribution Guide](CONTRIBUTING.md) +## 🧪 Development +The frontend is written in 🦀 [Rust](https://rust-lang.org/) using the [Leptos](https://leptos.dev/) framework. ---- -## Release -Update the version in `frontend/const.py` and `Dockerfile` before creating a new image. +#### 🏃 Running -To support multiple architectures, we need to create and use or own builder. +```shell +cargo leptos watch +``` + + +#### 🔬 Testing + +```shell +cargo leptos end-to-end +``` + +```shell +cargo leptos end-to-end --release +``` + +Cargo-leptos uses Playwright as the end-to-end test tool. +Tests are located in end2end/tests directory. + + +#### 📦 Release + +```shell +cargo leptos build --release +``` + +1. The server binary located in `target/server/release` +2. The `site` directory and all files within located in `target/site` + +Copy these files to your remote server. The directory structure should be: +```text +frontend +site/ +``` +Set the following environment variables (updating for your project as needed): +```text +LEPTOS_OUTPUT_NAME="frontend" +LEPTOS_SITE_ROOT="site" +LEPTOS_SITE_PKG_DIR="pkg" +LEPTOS_SITE_ADDR="127.0.0.1:3000" +LEPTOS_RELOAD_PORT="3001" +``` + + +## 🚀 Release + +To support multiple architectures, an own builder needs to be created. ```shell docker buildx create --name multiarchitecturebuilder docker buildx use multiarchitecturebuilder docker buildx build --platform linux/arm64,linux/amd64 --tag photosnetwork/frontend:latest --push . -``` +``` + + + +## 🏛️ License + +``` +Photos.network · A privacy first photo storage and sharing service for fediverse +Copyright (C) 2020 Photos network developers + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +``` diff --git a/frontend/__init__.py b/frontend/__init__.py deleted file mode 100644 index 543a16c..0000000 --- a/frontend/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""init of Photos.network frontend.""" diff --git a/frontend/__main__.py b/frontend/__main__.py deleted file mode 100644 index f1e1771..0000000 --- a/frontend/__main__.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Entry point of Photos.network frontend.""" -import asyncio -import json -import logging -import os -import socket -import sys -from typing import Any, Dict -from xml.dom.expatbuilder import parseString - -from aiohttp import web - -from frontend.application import create_application -from frontend.config import Config -from frontend.const import REQUIRED_PYTHON_VER -from frontend.frontend import Frontend - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -def validate_python() -> None: - """Validate that the right Python version is running.""" - if sys.version_info[:3] < REQUIRED_PYTHON_VER: - print( - "Photos.network requires at least Python " - f"{REQUIRED_PYTHON_VER[0]}.{REQUIRED_PYTHON_VER[1]}.{REQUIRED_PYTHON_VER[2]}" - ) - sys.exit(1) - - -def main() -> int: - """Start Photos.network application.""" - validate_python() - - config_dir = os.path.abspath(os.path.join(os.getcwd(), "config")) - config_file = os.path.join(config_dir, "frontend_configuration.json") - - if not os.path.exists(config_dir): - _LOGGER.info(f"create directory {config_dir}") - os.mkdir(config_dir) - - if not os.path.exists(config_file): - # create default config file - with open(file=config_file, mode="w+", encoding="utf-8") as file: - hostname = socket.gethostname() - - output = { - "frontend_url": "http://" + str(socket.gethostbyname(hostname)), - "frontend_port": 7778, - "core_url": "http://127.0.0.1", - "core_port": 7777, - "client_id": "", - "client_secret": "", - "redirect_uri": "http://127.0.0.1:7778/callback", - } - json.dump(output, file, indent=2) - file.close() - _LOGGER.info(f"default config_file {config_file} created.") - else: - _LOGGER.info(f"config_file found at {config_file}") - - with open(file=config_file, mode="r", encoding="utf-8") as file: - conf_dict = json.load(file) - file.close() - - if not isinstance(conf_dict, dict): - msg = f"The configuration file {os.path.basename(config_dir)} does not contain a dictionary" - _LOGGER.error(msg) - raise RuntimeError(msg) - - config = Config.fromConfigFile(conf_dict) - - frontend = Frontend(config) - frontend.async_enable_logging(verbose=True) - - try: - exit_code = asyncio.run(frontend.async_run()) - except KeyboardInterrupt: - # TODO: handle running threads - print("### Interrupt application without taking care of running threads ###") - exit_code = 0 - - return exit_code - -if __name__ == "__main__": - sys.exit(main()) diff --git a/frontend/application.py b/frontend/application.py deleted file mode 100644 index 5b2a562..0000000 --- a/frontend/application.py +++ /dev/null @@ -1,49 +0,0 @@ -import asyncio -import os -from typing import Any, Awaitable, Callable, Dict - -import aiohttp_jinja2 -import aiohttp_session -import jinja2 -from aiohttp import web -from aiohttp_session import session_middleware -from aiohttp_session.cookie_storage import EncryptedCookieStorage - -from frontend.config import Config -from frontend.i18n import i18n - - -async def username_ctx_processor(request: web.Request) -> Dict[str, Any]: - """Jinja2 context processor to extract the username from an active session.""" - session = await aiohttp_session.get_session(request) - if "first_name" in session: - username = session.get("first_name") - elif "last_name" in session: - username = session.get("last_name") - else: - username = session.get("username") - - return {"username": username} - - -def create_application( - loop=asyncio.new_event_loop(), - config: Config = None, -) -> web.Application: - app = web.Application(client_max_size=64 * 1024**2) - - # check if secret length is 32 bytes - if len(config.cookie_secret) == 32: - secret_key = bytes(config.cookie_secret, "utf-8") - else: - secret_key = bytes("Thirty two length bytes key!", "utf-8") - - aiohttp_session.setup(app, EncryptedCookieStorage(secret_key)) - - for route in app.routes.routes: - app.router.add_route(*route[0], **route[1]) - - for middleware in app.middlewares.middlewares: - app.middlewares.append(middleware) - - return app diff --git a/frontend/base/__init__.py b/frontend/base/__init__.py deleted file mode 100644 index 9b9e4ef..0000000 --- a/frontend/base/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import Any, Awaitable, Callable, Dict - -from aiohttp import web - - -def require_login( - func: Callable[[web.Request], Awaitable[web.StreamResponse]], -) -> Callable[[web.Request], Awaitable[web.StreamResponse]]: - """Decorator to indicate internal calls checked by the `login_required_middleware`.""" - func.__require_login__ = True # type: ignore - return func diff --git a/frontend/base/handler.py b/frontend/base/handler.py deleted file mode 100644 index 7477c91..0000000 --- a/frontend/base/handler.py +++ /dev/null @@ -1,15 +0,0 @@ -from aiohttp import web -from aiohttp_session import get_session - - -class Handler(web.View): - async def get_current_user(self) -> str | None: - """Current user""" - - session = await get_session(self.request) - email = session.get("email", None) - - if email is None: - return None - - return email diff --git a/frontend/base/oauth.py b/frontend/base/oauth.py deleted file mode 100644 index 29a1bad..0000000 --- a/frontend/base/oauth.py +++ /dev/null @@ -1,182 +0,0 @@ -import asyncio -import logging -import time -from dataclasses import dataclass -from typing import Any, Dict - -import aiohttp -import aiohttp_session -from aiohttp import request, web -from frontend.config import Config - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -class CoreClient: - """Http client for Photos.network core communication. - - * Handles oauth flow https://developers.photos.network/core/authentication_flow/ - """ - - state: str = None - accessToken: str = None - refreshToken: str = None - expiresIn: int = None - scope: str = None - - def __init__(self, config: Config) -> None: - self.config: Config = config - - def get_authorize_url(self, scope: str, state: str = None) -> str: - """generate authorize url based on inputs.""" - self.scope = scope - - return ( - str(self.config.core_url) - + ":" - + str(self.config.core_port) - + "/api/oauth/authorize?client_id=" - + str(self.config.client_id) - + "&response_type=code&redirect_uri=" - + str(self.config.redirect_uri) - + "&response_mode=query&scope=" - + scope - + "&state=" - + state - ) - - async def get_access_token(self, code: str): - """request access token.""" - url = str(self.config.core_url) + ":" + str(self.config.core_port) + "/api/oauth/token" - - raw_data = ( - "grant_type=authorization_code&code=" - + code - + "&client_id=" - + self.config.client_id - + "&redirect_uri=" - + self.config.redirect_uri - ) - - async with aiohttp.ClientSession() as session: - async with session.request( - method="POST", - url=url, - headers={"Content-Type": "application/x-www-form-urlencoded"}, - data=raw_data, - ssl=None, - verify_ssl=False, - ) as resp: - response = await resp.json() - - self.accessToken = response["access_token"] - self.refreshToken = response["refresh_token"] - self.expiresIn = response["expires_in"] - - await session.close() - - return { - "expires_in": response["expires_in"], - "access_token": response["access_token"], - "refresh_token": response["refresh_token"], - } - - async def check_access_token(self) -> int: - """check if current access token is still valid.""" - url = str(self.config.core_url) + ":" + str(self.config.core_port) + "/api/protected" - - async with aiohttp.ClientSession() as session: - async with session.get(url=url, headers={"Authorization": "Bearer " + str(self.accessToken)}) as resp: - return resp.status - - async def user_info(self): - """user info.""" - - url = str(self.config.core_url) + ":" + str(self.config.core_port) + "/api/user/" - - async with aiohttp.ClientSession() as session: - async with session.get(url=url, headers={"Authorization": "Bearer " + str(self.accessToken)}) as resp: - response = await resp.json() - - return UserInfo( - response["id"], - response["email"], - response["firstname"], - response["lastname"], - ) - - async def refresh_access_token_call(self): - """refresh access token""" - - url = str(self.config.core_url) + ":" + str(self.config.core_port) + "/api/oauth/token" - - if self.refreshToken is not None and self.scope is not None: - - raw_data = ( - "grant_type=refresh_token" - + "&refresh_token=" - + str(self.refreshToken) - + "&client_id=" - + self.config.client_id - + "&scope=" - + self.scope - ) - - async with aiohttp.ClientSession() as clientSession: - async with clientSession.request( - method="POST", - url=url, - headers={"Content-Type": "application/x-www-form-urlencoded"}, - data=raw_data, - ssl=None, - verify_ssl=False, - ) as resp: - response = await resp.json() - - timestamp = time.time() - expires_in = int(timestamp) + response["expires_in"] - - self.accessToken = response["access_token"] - self.refreshToken = response["refresh_token"] - self.expiresIn = expires_in - - status = resp.status - await clientSession.close() - - return (status, self.accessToken, self.refreshToken, self.expiresIn) - else: - return (None, None, None, None) - - async def get_photos(self): - async with aiohttp.ClientSession() as session: - async with session.get( - url=str(self.config.core_url) + ":" + str(self.config.core_port) + "/api/photos", - headers={"Authorization": "Bearer " + str(self.accessToken)}, - ) as resp: - return await resp.json() - - async def request(self, method: str, url: str): - """Check if user is authenticated and communicate with core instance.""" - - async with aiohttp.ClientSession() as session: - async with session.request( - method=method, - url=str(self.config.core_url) + ":" + str(self.config.core_port) + url, - headers={"Authorization": "Bearer " + str(self.accessToken)}, - ) as resp: - return await resp.read() - - -@dataclass -class UserInfo: - id: str - username: str - first_name: str - last_name: str - - def __init__(self, id: str, username: str, first_name: str, last_name: str): - self.id = id - self.username = username - self.first_name = first_name - self.last_name = last_name diff --git a/frontend/base/request.py b/frontend/base/request.py deleted file mode 100644 index 9a301a5..0000000 --- a/frontend/base/request.py +++ /dev/null @@ -1,151 +0,0 @@ -"""Base class for webserver requests.""" -import asyncio -import json -import logging -from typing import TYPE_CHECKING, Any, Callable, List, Optional - -from aiohttp import web -from aiohttp.typedefs import LooseHeaders -from aiohttp.web_exceptions import HTTPInternalServerError, HTTPUnauthorized -from frontend.const import KEY_AUTHENTICATED, KEY_USER_ID - -if TYPE_CHECKING: - from frontend.frontend import Frontend - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -def is_callback(func: Callable[..., Any]) -> bool: - """Check if function is safe to be called in the event loop.""" - return getattr(func, "_callback", False) is True - - -class RequestView: - """Base request.""" - - requires_auth = True - url: Optional[str] = None - - extra_urls: List[str] = [] - - @staticmethod - def json( - result: Any, - status_code: int = 200, - headers: Optional[LooseHeaders] = None, - ) -> web.Response: - """Return a JSON response.""" - try: - msg = json.dumps(result, cls=ComplexEncoder, allow_nan=False).encode("UTF-8") - except (ValueError, TypeError) as err: - _LOGGER.error(f"Unable to serialize to JSON: {err}\n{result}") - raise HTTPInternalServerError from err - response = web.Response( - body=msg, - content_type="application/json", - status=status_code, - headers=headers, - ) - response.enable_compression() - return response - - def json_message( - self, - message: str, - status_code: int = 200, - message_code: Optional[str] = None, - headers: Optional[LooseHeaders] = None, - ) -> web.Response: - """Return a JSON message response.""" - data = {"message": message} - if message_code is not None: - data["code"] = message_code - return self.json(data, status_code, headers=headers) - - def register(self, frontend: "Frontend", router: web.UrlDispatcher) -> None: - """Register the view with a router.""" - assert self.url is not None, "No url set for view" - urls = [self.url] + self.extra_urls - routes = [] - - for method in ("get", "post", "delete", "put", "patch", "head", "options"): - handler = getattr(self, method, None) - - if not handler: - continue - - handler = request_handler_factory(self, frontend, handler) - - for url in urls: - routes.append(router.add_route(method, url, handler)) - - -class ComplexEncoder(json.JSONEncoder): - """Encoder for complex classes.""" - - def default(self, o): - """Encode all properties.""" - if isinstance(o, complex): - return [o.real, o.imag] - # Let the base class default method raise the TypeError. - return json.JSONEncoder.default(self, o) - - -def request_handler_factory(view: RequestView, frontend: "Frontend", handler: Callable) -> Callable: - """Wrap the handler classes.""" - assert asyncio.iscoroutinefunction(handler) or is_callback(handler), "Handler should be a coroutine or a callback." - - async def handle(request: web.Request) -> web.StreamResponse: - """Handle incoming request.""" - if frontend.is_stopping: - return web.Response(status=503) - - authenticated = request.get(KEY_AUTHENTICATED, False) - - if view.requires_auth and not authenticated: - raise HTTPUnauthorized() - - _LOGGER.info(f"Serving {request.path} to {request.remote} (auth: {authenticated})") - _LOGGER.info(f"match_info {request.match_info}") - - # try: - result = await handler(frontend, request, **request.match_info) - - if asyncio.iscoroutine(result): - result = await result - # except voluptuous.Invalid as err: - # raise HTTPBadRequest() from err - # except exceptions.ServiceNotFound as err: - # raise HTTPInternalServerError() from err - # except exceptions.Unauthorized as err: - # raise HTTPUnauthorized() from err - - if isinstance(result, web.StreamResponse): - # The method handler returned a ready-made Response, how nice of it - return result - - status_code = 200 - - if isinstance(result, tuple): - result, status_code = result - - bresult = convert_to_bytes(result) - - return web.Response(body=bresult, status=status_code) - - return handle - - -def convert_to_bytes(input: Any) -> bytes: - """Convert given input into bytes.""" - if isinstance(input, bytes): - bresult = input - elif isinstance(input, str): - bresult = input.encode("utf-8") - elif input is None: - bresult = b"" - else: - assert False, f"Result should be None, string, bytes or Response. Got: {input}" - - return bresult diff --git a/frontend/base/route.py b/frontend/base/route.py deleted file mode 100644 index 294fcb0..0000000 --- a/frontend/base/route.py +++ /dev/null @@ -1,10 +0,0 @@ -def __namedict(name): - return dict(name=name) if name is not None else dict() - - -def get(url, handler, name=None): - return (("GET", url, handler), __namedict(name)) - - -def post(url, handler, name=None): - return (("POST", url, handler), __namedict(name)) diff --git a/frontend/base/state.py b/frontend/base/state.py deleted file mode 100644 index ade3adc..0000000 --- a/frontend/base/state.py +++ /dev/null @@ -1,16 +0,0 @@ -import enum - - -class FrontendState(enum.Enum): - """Represent the current state of Photos.network frontend.""" - - not_running = "NOT_RUNNING" - starting = "STARTING" - running = "RUNNING" - stopping = "STOPPING" - final_write = "FINAL_WRITE" - stopped = "STOPPED" - - def __str__(self) -> str: # pylint: disable=invalid-str-returned - """Return the event.""" - return self.value # type: ignore diff --git a/frontend/config.py b/frontend/config.py deleted file mode 100644 index 70a3813..0000000 --- a/frontend/config.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Configurations for frontend instance.""" -import logging -from typing import Optional - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -class Config: - """Representation class of configurations.""" - - def __init__( - self, - frontend_url: str = "http://127.0.0.1", - frontend_port: int = 7778, - core_url: str = "http://127.0.0.1", - core_port: int = 7777, - client_id: str | None = None, - client_secret: str | None = None, - redirect_uri: str | None = None, - cookie_name: str = "Photos.network", - cookie_secret: str = "", - ) -> None: - self.frontend_url: Optional[str] = frontend_url - self.frontend_port: int = frontend_port - self.core_url: Optional[str] = core_url - self.core_port: int = core_port - self.client_id: str = client_id - self.client_secret: str = client_secret - self.redirect_uri: str = redirect_uri - self.cookie_name: str = cookie_name - self.cookie_secret: str = cookie_secret - - @classmethod - def fromConfigFile(cls, conf_dict: dict): - """create config instance from configuration file.""" - - config = cls() - - if "frontend_url" in conf_dict: - config.frontend_url = conf_dict["frontend_url"] - - if "frontend_port" in conf_dict: - config.frontend_port = conf_dict["frontend_port"] - - if "core_url" in conf_dict: - config.core_url = conf_dict["core_url"] - - if "core_port" in conf_dict: - config.core_port = conf_dict["core_port"] - - if "client_id" in conf_dict: - config.client_id = conf_dict["client_id"] - - if "client_secret" in conf_dict: - config.client_secret = conf_dict["client_secret"] - - if "redirect_uri" in conf_dict: - config.redirect_uri = conf_dict["redirect_uri"] - - if "cookie_name" in conf_dict: - config.cookie_name = conf_dict["cookie_name"] - - if "cookie_secret" in conf_dict: - config.cookie_secret = conf_dict["cookie_secret"] - - return config diff --git a/frontend/const.py b/frontend/const.py deleted file mode 100644 index ad06558..0000000 --- a/frontend/const.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Constants used by Photos.network frontend.""" -MAJOR_VERSION = 0 -MINOR_VERSION = 2 -PATCH_VERSION = 2 -__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" -__version__ = f"{__short_version__}.{PATCH_VERSION}" -REQUIRED_PYTHON_VER = (3, 7, 1) - -FRONTEND_VERSION = __version__ - -KEY_AUTHENTICATED = "authenticated" -KEY_USER_ID = "user_id" - -SCOPES = "openid profile email phone library:read library:write" diff --git a/frontend/frontend.py b/frontend/frontend.py deleted file mode 100644 index 3d5bdfe..0000000 --- a/frontend/frontend.py +++ /dev/null @@ -1,192 +0,0 @@ -"""Frontend application""" -import asyncio -import logging -import os -import sys -from logging.handlers import TimedRotatingFileHandler -from time import monotonic -from typing import Any, Dict, List, Optional, Set - -from colorlog import ColoredFormatter - -from frontend.base.oauth import CoreClient -from frontend.base.state import FrontendState -from frontend.config import Config -from frontend.webserver import Webserver - -ERROR_LOG_FILENAME = "frontend.log" - -# How long to wait to log tasks that are blocking -BLOCK_LOG_TIMEOUT = 60 # seconds - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -class Frontend: - """Frontend application class.""" - - http: Webserver = None - core_client: CoreClient = None - - def __init__(self, config: Config) -> None: - self.loop = asyncio.new_event_loop() - self.config: Config = config - self.state: FrontendState = FrontendState.not_running - self.exit_code: int = 0 - - # If not None, use to signal end-of-loop - self._stopped: Optional[asyncio.Event] = None - - self._pending_tasks: List = [] - - @property - def is_running(self) -> bool: - """Return if frontend is running.""" - return self.state in (FrontendState.starting, FrontendState.running) - - @property - def is_stopping(self) -> bool: - """Return if frontend is stopping.""" - return self.state in (FrontendState.stopping, FrontendState.final_write) - - def async_enable_logging(self, verbose: bool = False) -> None: - """Set up logging for frontend.""" - fmt = "%(asctime)s %(levelname)s (%(threadName)s) [%(name)s] %(message)s" - datefmt = "%Y-%m-%d %H:%M:%S" - logging.basicConfig(level=logging.INFO) - - colorfmt = f"%(log_color)s{fmt}%(reset)s" - logging.getLogger().handlers[0].setFormatter( - ColoredFormatter( - colorfmt, - datefmt=datefmt, - reset=True, - log_colors={ - "DEBUG": "cyan", - "INFO": "green", - "WARNING": "yellow", - "ERROR": "red", - "CRITICAL": "red", - }, - ) - ) - - logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO) - - logging.getLogger("requests").setLevel(logging.WARNING) - logging.getLogger("urllib3").setLevel(logging.WARNING) - logging.getLogger("aiohttp.access").setLevel(logging.WARNING) - - sys.excepthook = lambda *args: logging.getLogger("").exception( - "Uncaught exception", exc_info=args # type: ignore - ) - log_rotate_days = 14 - - err_log_path = os.path.join(os.getcwd(), ERROR_LOG_FILENAME) - err_dir = os.path.dirname(err_log_path) - if not err_dir: - os.mkdir(err_dir) - err_handler: logging.FileHandler = TimedRotatingFileHandler( - err_log_path, when="midnight", backupCount=log_rotate_days - ) - err_handler.setLevel(logging.DEBUG if verbose else logging.WARNING) - err_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt)) - - logger = logging.getLogger("") - logger.addHandler(err_handler) - logger.setLevel(logging.DEBUG if verbose else logging.WARNING) - - async def start(self) -> int: - """Start frontend application. - Note: This function is only used for testing. - For regular use, use "await photos.run()". - """ - _LOGGER.debug("start core loop") - await self.async_block_till_done() - - return self.exit_code - - async def async_run(self, *, attach_signals: bool = True) -> int: - """Run the application. - Main entry point of the frontend application. - """ - if self.state != FrontendState.not_running: - raise RuntimeError("Frontend is already running") - - # _async_stop will set this instead of stopping the loop - self._stopped = asyncio.Event() - - await self.async_start() - - await self._stopped.wait() - return self.exit_code - - async def async_start(self) -> None: - """Finalize startup from inside the event loop.""" - self.state = FrontendState.starting - - try: - await self.async_block_till_done() - except asyncio.TimeoutError: - _LOGGER.warning( - "Something is blocking frontend from wrapping up the start up phase. We're going to continue anyway." - ) - - # Wait for all startup triggers before changing state - await asyncio.sleep(0) - - if self.state != FrontendState.starting: - _LOGGER.warning("Frontend startup has been interrupted. Its state may be inconsistent") - return - - self.state = FrontendState.running - - async def async_block_till_done(self) -> None: - """Block until all pending work is done.""" - # To flush out any call_soon_threadsafe - await asyncio.sleep(0) - - # setup core client - self.core_client = CoreClient(self.config) - _LOGGER.info("Core client setup done.") - - # setup webserver - self.http = Webserver(self) - await self.http.start() - _LOGGER.info("Webserver should be up and running...") - - start_time = None - - # iterate through pending tasks - while self._pending_tasks: - pending = [task for task in self._pending_tasks if not task.done()] - self._pending_tasks.clear() - if pending: - await self._await_and_log_pending(pending) - - if start_time is None: - # Avoid calling monotonic() until we know - # we may need to start logging blocked tasks. - start_time = 0 - elif start_time == 0: - # If we have waited twice then we set the start time - start_time = monotonic() - elif monotonic() - start_time > BLOCK_LOG_TIMEOUT: - # We have waited at least three loops and new tasks - # continue to block. At this point we start - # logging all waiting tasks. - for task in pending: - _LOGGER.debug("Waiting for task: %s", task) - else: - await asyncio.sleep(0) - - async def async_stop(self) -> None: - """Stop Photos.network core application.""" - self.state = FrontendState.stopping - self.state = FrontendState.final_write - self.state = FrontendState.not_running - self.state = FrontendState.stopped - - if self._stopped is not None: - self._stopped.set() diff --git a/frontend/i18n.py b/frontend/i18n.py deleted file mode 100644 index 8ec6962..0000000 --- a/frontend/i18n.py +++ /dev/null @@ -1,106 +0,0 @@ -import gettext -import importlib.resources as importlib_resources -import os -import sys -import threading - -from jinja2 import pass_context -from jinja2.ext import InternationalizationExtension -from markupsafe import Markup - -pkg = importlib_resources.files("frontend") -localedir = os.path.join(pkg / "locales") -domain = "messages" -threadLocalData = threading.local() -threadLocalData.locale = "en_US" - -locales = [] -for dirpath, dirnames, filenames in os.walk(localedir): - for dirname in dirnames: - locales.append(dirname) - break - -all_translations = {} -for locale in locales: - all_translations[locale] = gettext.translation(domain, localedir, [locale]) - - -def context_locale(context): - lang = "en_US" - if "locale" in context and context["locale"] in all_translations: - lang = context["locale"] - return lang - - -def parseAcceptLanguage(acceptLanguage): - try: - languages = acceptLanguage.split(",") - locale_q_pairs = [] - - for language in languages: - if language.split(";")[0] == language: - # no q => q = 1 - locale_q_pairs.append((language.strip().replace("-", "_"), "1")) - else: - locale = language.split(";")[0].strip() - q = language.split(";")[1].split("=")[1] - locale_q_pairs.append((locale.replace("-", "_"), q)) - - return locale_q_pairs - except AttributeError: - return "en_US" - - -def setLocale(locale): - languagePairs = parseAcceptLanguage(locale) - for pair in languagePairs: - if pair[0] in locales: - threadLocalData.locale = pair[0] - break - - -class InternationalizationWithContextExtension(InternationalizationExtension): - def _install_callables(self, gettext, ngettext, newstyle=None): - if newstyle is not None: - self.environment.newstyle_gettext = newstyle - if self.environment.newstyle_gettext: - gettext = _make_new_gettext(gettext) - ngettext = _make_new_ngettext(ngettext) - self.environment.globals.update(gettext=gettext, ngettext=ngettext) - - def gettext(msg): - return all_translations[threadLocalData.locale].gettext(msg) - - def ngettext(singular, plural, n): - return all_translations[threadLocalData.locale].ngettext(singular, plural, n) - - -@pass_context -def _gettext_alias(__context, *args, **kwargs): - return __context.call(__context.resolve("gettext"), *args, **kwargs) - - -def _make_new_gettext(func): - @pass_context - def gettext(__context, __string, **variables): - rv = __context.call(func, __context, __string) - if __context.eval_ctx.autoescape: - rv = Markup(rv) - return rv % variables - - return gettext - - -def _make_new_ngettext(func): - @pass_context - def ngettext(__context, __singular, __plural, __num, **variables): - variables.setdefault("num", __num) - rv = __context.call(func, __context, __singular, __plural, __num) - if __context.eval_ctx.autoescape: - rv = Markup(rv) - return rv % variables - - return ngettext - - -i18n = InternationalizationWithContextExtension diff --git a/frontend/locales/base.pot b/frontend/locales/base.pot deleted file mode 100644 index c283042..0000000 --- a/frontend/locales/base.pot +++ /dev/null @@ -1,24 +0,0 @@ -# English translations for Photos.network frontend. -# Copyright (C) 2020 Photos.network -# This file is distributed under the Apache License, Version 2.0 by Photos.network. -# -msgid "" -msgstr "" -"Project-Id-Version: 1.0.0\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-04-19 20:12+0800\n" -"PO-Revision-Date: 2022-04-19 20:12+0800\n" -"Last-Translator: Benjamin Stürmer \n" -"Language-Team: none\n" -"Language: en\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: frontend/views/partials/header.jinja2:21 -msgid "Home" -msgstr "Home" - -#: frontend/views/partials/header.jinja2:21 -msgid "Settings" -msgstr "Settings" diff --git a/frontend/locales/de/LC_MESSAGES/messages.mo b/frontend/locales/de/LC_MESSAGES/messages.mo deleted file mode 100644 index db80067e7304c09c0fe6eb01865438ae8f2fe657..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 435 zcmY+A!Ab)$5Qd{x&;=1Zd&pfpaZTBstjTOq@aiM<l^D9>pLr-Ad=0n*;L_yY8-MKHJW>2D-46a_M~E`(Px^hi)06I zrgMXGOrIYv2PfN;AM4ap36)hZoIT-HOiP`Dib3a*M2qn2MKXb4&>M9B#SP9a5+FbL zTyP?SC)zJEZSY7=AGGs8$2ak%YW>Ys!S2DSE>3O2KM~2+#84o{q#cPf%pprkOhwnA KC&)7H0mU~~ZF2em diff --git a/frontend/locales/de/LC_MESSAGES/messages.po b/frontend/locales/de/LC_MESSAGES/messages.po deleted file mode 100644 index 972a0ec..0000000 --- a/frontend/locales/de/LC_MESSAGES/messages.po +++ /dev/null @@ -1,25 +0,0 @@ -# German translations for Photos.network frontend. -# Copyright (C) 2020 Photos.network -# This file is distributed under the Apache License, Version 2.0 by Photos.network. -# -msgid "" -msgstr "" -"Project-Id-Version: 1.0.0\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-04-19 20:12+0800\n" -"PO-Revision-Date: 2022-04-19 20:12+0800\n" -"Last-Translator: Benjamin Stürmer \n" -"Language-Team: none\n" -"Language: de\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -msgid "Home" -msgstr "Start" - -msgid "Settings" -msgstr "Einstellungen" - -msgid "Login" -msgstr "Anmeldung" diff --git a/frontend/locales/en_US/LC_MESSAGES/messages.mo b/frontend/locales/en_US/LC_MESSAGES/messages.mo deleted file mode 100644 index beee7369448f88660ed12eb978b8d805ecdb540e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 425 zcmZvY!Ab)$5Qd{xki7_=J>)K_*~LSRRis+6&|+ywZCb3D}60s^GW)DR47sM9vOl+?)_Cgx-CG|0X zC5`zTDVe7|Kx~?#m4FS>EBCx)9Qy8P@OvDZNV4PJvt;@Ph22eSVq8AD? z5v_R-8&W1+9Z=+6+c4AUyy7jeQ(YR% zhxqaCOYpoo`nFC3&LLX$%DWS~YKyrpK;)oH#jUgO<0%d{>2-STKkFuE&LUWP@TsDW s5FT;AnrUN?`1oG?0C;?zUx>!R!2kdN diff --git a/frontend/locales/en_US/LC_MESSAGES/messages.po b/frontend/locales/en_US/LC_MESSAGES/messages.po deleted file mode 100644 index 9cb1f5c..0000000 --- a/frontend/locales/en_US/LC_MESSAGES/messages.po +++ /dev/null @@ -1,25 +0,0 @@ -# English translations for Photos.network frontend. -# Copyright (C) 2020 Photos.network -# This file is distributed under the Apache License, Version 2.0 by Photos.network. -# -msgid "" -msgstr "" -"Project-Id-Version: 1.0.0\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-04-19 20:12+0800\n" -"PO-Revision-Date: 2022-04-19 20:12+0800\n" -"Last-Translator: Benjamin Stürmer \n" -"Language-Team: none\n" -"Language: en\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -msgid "Home" -msgstr "Home" - -msgid "Settings" -msgstr "Settings" - -msgid "Login" -msgstr "Login" diff --git a/frontend/locales/vi_VN/LC_MESSAGES/messages.mo b/frontend/locales/vi_VN/LC_MESSAGES/messages.mo deleted file mode 100644 index d429c1f45bab2ac011f95295c38292b142b9d4c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 410 zcmY+A&q@O^5XPfckVO$Zd&pf~CvlR9xGE|qRLV+n6xZT7YAKH*b;|guLhB^2gPEvY8{Wg!yUW4x zv;I%((&G$5t6VsHL8~U4sSHF4Dwo_?1HV1V=Q?UP+l_yAqbq9=3_bWnua_Ulr|adSU|;8! BaKr!r diff --git a/frontend/locales/vi_VN/LC_MESSAGES/messages.po b/frontend/locales/vi_VN/LC_MESSAGES/messages.po deleted file mode 100644 index 249a817..0000000 --- a/frontend/locales/vi_VN/LC_MESSAGES/messages.po +++ /dev/null @@ -1,22 +0,0 @@ -# Vietnamese translations for Photos.network frontend. -# Copyright (C) 2020 Photos.network -# This file is distributed under the Apache License, Version 2.0 by Photos.network. -# -msgid "" -msgstr "" -"Project-Id-Version: 1.0.0\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-04-19 20:12+0800\n" -"PO-Revision-Date: 2022-04-19 20:12+0800\n" -"Last-Translator: Benjamin Stürmer \n" -"Language-Team: none\n" -"Language: vi\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -msgid "Home" -msgstr "Trang chính" - -msgid "Settings" -msgstr "Thiết lập" diff --git a/frontend/locales/zh_TW/LC_MESSAGES/messages.mo b/frontend/locales/zh_TW/LC_MESSAGES/messages.mo deleted file mode 100644 index 231e37df9647ffdde2e87088d6338f10f339f19f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 400 zcmY+Aze+8Eo(;5^Q_)yM_`>YTF_FF7kSi%+*r?=E>(S2ourN> zM$o8rz}j%8!X(?39t4ef95Y^Kr8O)?DPG|f&ZCBOo+ValHU1JgU\n" -"Language-Team: none\n" -"Language: zh_TW\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -msgid "Home" -msgstr "首頁" - -msgid "Settings" -msgstr "設定" diff --git a/frontend/static/browserconfig.xml b/frontend/static/browserconfig.xml deleted file mode 100644 index c554148..0000000 --- a/frontend/static/browserconfig.xml +++ /dev/null @@ -1,2 +0,0 @@ - -#ffffff \ No newline at end of file diff --git a/frontend/static/custom-icon.png b/frontend/static/custom-icon.png deleted file mode 100644 index 6fd7f9750bfae850f4439e686ea3e4b4c7e524d1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15999 zcmZ{L1yogGx9tHzLXk!qq(vH}TN*(LX{1xSOF$4QX%LW75D=Aa>5>K!q`Q$W0jamn z|Lz^*zB|Sn4m`@)XP>?Ix4yOJnscu3r>gQe*yPv<1Oi73n~^`5H{oyfX*&#f zL9>!lkwPF!V{ToTpu_)Z%@ovC5C|V81S0S?0`U)i6}XN-xN{*8e~b|b;S>bou2cGV zHBorurm3>L4B{I3=R-q&0{jNUO;JS_V-<~v5cQ$KSWFWFAu6UQBc%iuAf z1a;B!gr)QTT%Yzm&qj$JNGB^zvqq?tWldJ!EQ&2Fvnwkn#KOT5{KM3!fjP^y-a@Zv%=bPe!*HcL z!QWxlPvPZDjGMveh)O*vU0r^f00Sv+?>nWXA5YIW@z54Nr(KQ0xl!m8jmsx=jK+qC zxO=j$G?fhv@Amdm=oJ19IKE~jazVZEQs|{{LUH4l>}{ zt*w;x-K@o|>of7nN~*p-HZvYwypE>1l_1&=S`WhC-p;Pzi@e^W;drWr(3( z;DqOe?u5`O`}~&1IF`X{^Zzh^V5H}ijUuZ$jb}lZ2U|o zAV3x+=RP8_UD);jA#k|ZA$qbSwv=o%xxP(hWowIHXSn?P#hjiv zD2I`_)br=qJgJ_F%N+HnPB*eyO6%*pbL3x+{C58}GSW6b&&QvJGlp?BM|Tes6$=YL z@tvjXjsc2=g%AUytWUD6L-T{cm$ZrmX=x%yM{kFxr#rW&*pH6tjjcMnt}w2e&iF@c zi30*S$+F092kyUsV=vC(FJw!vd{Rcm5` z+d0wj^AHP!$o$a;oA_l#P`TA1Cgz@0L87k}8uK3ZJh4Lj>j}G+j!5su%c}76lPNOF zbdP)Ay!S9sPyhWy2yB_tF%8p+oAwhY%c39fp9%E*P zQ%6Uq!_-;ct#d|j05>S;;`tjDiSuvq6%{&(N@r>U17`b7ZI^d~XpQ;xNy$=k3kpK} zRm7rS?b-y>NJY^Krj@}j6E)?3{yhQ~f9#gU9pM%Es3t0OF1=EWxMqemL+ z$2UbcSwqYSPEJnl@$$Zc4ZvZ@dQV(@nu6{n`YdBCVOVzin=npksY$w^b2_fLVE@@+2pZa{*Vwh{JaJ%xAdkp)-(a!Jn(D1;hlj&6 zmqEwwV*F_@uZ{M|KWk#c4RsAZSx9)o>UDjY089Mgsn^)Wz`+1^vuUYGQS zqjH^LyLlNGKR<*?q2Hkj{nFAc*dKVZ`%fo1l11WIcz71i4ti||vN##;-J^_>+dg`4 zM8w!P6fEJWD(xN+aKC|B{wVsO_MJ4}n7i^;005)2J}}uQ5zc;J9E49Ex`_% zqGZM%9%NsC-we#kicU{w=;?J8@+=BmTy%r%WBBh*WXF%@W}+AxiTtXnVCEHny0pfcy>Am+Mw5Uj}s>;J7S#}^o%4Z_;;zF~hr>xfL zW7x$7QSy`-R|*+f>cak#|DVk|Yr8E|d;9a1fBA$BQ|J;5S9M?W(!XqPltfGRl3q>G1+l_#rRgKlqm!hc9 z{%~1QV(#Th*x$bb{^jQa!+4JAZc)xm*ByC^+7k15Nna}o2_Ew2Y{UmTMl7+a0GxCN?z zK-xrxbYbC*^<$=F>7-~WT6%8opp#jVZsx4Sgm{7_^)R(~2FMJXn^TZr_qVHY*S9Dw zY;0}>x3o89;4tu!6E7@C5s~EW56R~m)ayKUDv`hr>Dy?rDxc8RpPG$QuiU#li6NuP z=&&qra)FdrYw#u}=1ps>rubD&(EInOrl!*S$G;n&<@Y*CDW#Z0v>C6q`w7vUq3}dc@q^9}|<;_I_*shKOpL zGg^B$D4{g#g+hAU(*JY;|9Ugvva73$?ZE?zx*KydCJIl#K5PH+#`$2G8}_dX#MZrg zR~!vk_WU9b9_T}nn;*(qL!^smNI_(nn7rZ$eWmCvT;r zr)Oqv{toiPm#>EfpSxNH2ZfjgHZnsZgQ#^YOV2O$haz9m*Qiy2QuFn?XjayX?2-(M?I!&1-m>Sm!wfn<^gMm1ay`#0NPE^?=gp|jlk9vh?aP{?X^79{A^RYrI z9kNcOCgVS5K~adO&unP0|Bz#rNFhLtM?>?Y@`VL8d*B>h1YHpAqe89iA3a9b<&<&E zj3_9LpMNXG_%B_vA0B?4{{Fjfd|Y&_I9W~ay(|Dap47e^JAF|l#LeLFt1CsoEqMh6 z$wNcBMTQSLY%8hVhyCwLAyOa9xwtUwHh9M0x*O5mqeM$<96)^?L_5EhzXTOzaj~@~ zpeX}@AQ^S06cJ+#$?FbNGh5s6sp_n~i!-#9l?lHKXQA5KZt4}oWqj0c&nn~_RsP0{vkMcTjBnHR_C|NfK~3`9so>(`s@$wz+E4)068oOKcVn2$;Jc=@g+)>C@OQ(+ zpZb)Jjz&qhYZHQz@G?1B+Q;X1NQey}wAQ80OL$fntD?=kaSY*iIagCNpJz9`OhIPu zTGtSBD`o;}YA*)QRNQ9>K*$aD#(sXUFmQcN|GkhmH?!(oud2@omd2CJQ6nTHJK|BP z1xy}@ph>1qXL&WmT2^9j=GLJ`&&H^!_$iAZL;Ve}Fdo{^4L(h+)u&HmrCnWV0?w3q z`}-RpahdQ@%dozFaWQ+MU+D4gJRg8Wc4K2C0NPJqSYr|sNiQzlLxm4%A`lGSab;W* zO;B|PYa#7+#+*+~PEJHw(M;~TlS!SvxGaKX-FTWE@#hb+Dm?v~fxE33*uhk4Qo&nd zo@eo^dHK7UD-FfxdCmIhx0pCB!a}yS^CNb2$s&{P3)0*|Lfw4)9{a~*QEu*0rBzi? z{T?-M8j7I&iipU4uj?eG$yysTnmR?Iwak8cVPUkwQSY3fOQ(5c>;b6rI6S1;o0k&k z?bRoZVv2bw=-XwEDxC3tWJE{&x<0-)Sy@3TqP0_g>6d;FM^FpvuO{Qgjl{U2XS_a< zuiXWbjO~8+wEiW@aAi_$x9N@?UCbePD#%T>WtbQfd24AYruY7RAyruW>jTacQqmMl ztIn;`wVS?Lyii^1dTVrdW?rn753&{%4Rj7Z@tvnwxpC>1Gt@eqe`C7-EVeTy3z{TU z3W_PO17UYu*b+)gL{Q#Wp|(+qkeusfiZG?Mca?=y+1$u^K%(VgM_Tx+$){~yDq*hpHiwu17mXU-r=zGg%{<*!mH)Ae5c*#B1qWL(o#Tw$l>y) zL8dJ=)naGX-ptxM!uKT8d0X#3>_IB))|pqB&47ye_}aFoo_OtjNFEqa)6%YvWf;6; zvlEv>D&_8uvNm{ET_*)DvZ8lWy`s; zsST0tbQSk^Qzi*LWz!PusMcElgkB3k%6J5e@^*GC-rn7?v(>c6TK4vi8aX%WS~vUl z^z}uK{zmhPHMH*S-3F)ti9DjO&#OZ3KxSv3<9v>Tu`i=KaPxtIhb!wE3jMJ{nUBCVl;`|{4jvo!`;*j z!-XGF7)Xq4G>3MVlLS)CV>`PP{cPEDl0X(EKq9V(t2eHW^54vOHU7?t*`QK5J+)R; zz+4+CxDB8xzk-hq+-mf!)V7)(@QF%8g{4W{3t3JPsqDwE65$dStmS`#$Pn9X7 zgx({mw{I{d&{4>t!Q9&)da52jP-^B3Sfr&;9~U5TSa&yLRW*+MA!hn^@XTTG^T~!97_-V)1Yg#^Qfas08&Q1pi^){0z_t<&0{jhc7r%y8b`<*Kz zBSi6W_)nh3W+`tfsJYlZ4Sm-KkwRuzSz=jYRz|h+Vwj^fuY2HUS=T{y)Y>geY*RjS zS?fJD5#RT0^P688+1cNhlpuI`2|pL_F0S@70kOkH&hFUE<8e*-Qg{}HK3U{;5dgK- zi!*6#>|bHwSk6N6#-++{3w72r1h^~gEFF5^%-Tt)FErgwd%wlzFy&Qhdrw5^k7RqKBBVs$J)P{u`#k3Ut-?ECOtIP2hm z60XD`no`FTL-#s6wS|$_?5iceN_4`rCn?8hEv0NerIsc(oj<1EMQyl0=xqJnj?Stk z;_d%IvnM?A79{)EW}T)`v2#9tOoL`KHAQD{-g@ZDs~2s4uL$eT9oh#E;vympfP2U* z;lhcMTlvL2Art;EIOFbDl7Dym-3C*~r=!+CDuPgbz0sQw7Wf+?o1TL!J+C|t2O#u z$JKIJX|O&#nvA|zgnuhDRrSzws>!BonBU&q?zc4TmuYL9QWPcB3YYW|6x+&pXqtZg zGG$@!Uxnp(pLXuGZ#rv}FehMgYhMc!YB>a(9FUAgermE7790TmyH*C$dXxxK?C4(H zjLQ_Fw;z_v<7d<95LlABV)sxq?J&+U(Hv~f4iD0y>To7+^&?tO9GK~ysf*VIAP7B8 zVPWt^Z_~)HN#Xf}w|@%i_fY|F5fSGS5)(^9cVS|JVqG~f))ViP^1}Z8)={6xuS6>W zd(v-o)IL)`Z5Xh=5%k$l=+HQ0eq>NpHu?5WvXC-&>$lCA@5Ve_o~HI%LPmxRz`dcN zdycZ)jRMaRjg903>D}!Pcd@V!+-+*<6{FpP7glC2E^r>axOv{ow^EH>hvpO6b^1QZ zJtyOptbCP?YT_H(!s(3(KuLrEdZ9gT6xHw^_Fe)`rwa>n&-rZkZ5@`?%psFpGJBuB zF0NQ+!1%aKcoLO}K2uy0mHe!_|JyesPaih2u4S7A&DY%8rp%nna;!%fbU<}7sOG7$ zHZ|_^8Z~tlfR^o38RW^Z_Z9l#eonaImoX3^!jtyUu{7u{>#o9 zX0p8x`%h5>eLkUs5>`7v;^$%ZEDs zwW9`p5XuEK+vH3M9ZUaabI@RWVcq$gU&0jyc^XKY$G~73MhGd#n?==GmWbRbvy6IN ztk12-xRp72PW^^KDE$FRwLRb49j+R4CJDJm3(qGj6C8-=KfCD~Zhz*QfO-jZ^^+%q zk%raSgWa45m?&N5JkSa2>V_LMJ^#@W*}=dhx42vEULaR!==SQ}7wy7iFdykHzXwJ~T z&=Ul{hOZD8M-sN5i#WV&SkHyEr}G59iDvNgRG3?u_D6L8b&&U*4i7X_6e_o|fOH11 zV*}>_^OAczhL9sEZGuplYfg5MqH-joh)5hzyGTl@wl;~r?o&V)YOII7NYH%_)%!0uM)mI)*2Ey#f_ti4D0^hlpCq_HSYs$ z(o*Nr=g(a$kd>&#da@PDE_W}PTL@VOr3ujdg4J(rCBNC$GG#@bfs&g z*Jw3IqjPo1f3T{H>jh&T4B8L+)#T;HLgPR!zP0otN)Aaoy1K4Os`%r|Ab3&(o0_7L z$l$s@B7r$3A%%1!{yc~!LWhmN&+>t70@|lWzR~S<(iYoF0HJxFm)AGxFi}Y& zlZJ+bn3zbsul(Xa-jk234+`R=qeIR3M+wb{iwhAJ*23~K1N0N3X=y&FHw^18j$+2E z5z8(@0Fy70uMfnLY%8m;0)d?)1*k(gKq_SU`$&Tla5ET7Em5>aA@eSBHQ)kxL`7Bf zr?`;xWd&baVj@ev<9_#w3xEJvAFCHHFp`wQ{pRjZIp75XkxH>)74NK{w-sv$C9QlwEKeMbU(Ft*Eyx2c> zCt^4XBQq{G88EG!L~3Va0t3LiNSc`ml$U=^>3>H*cx&HfZ`=+^x`SjF+$b)Ovd!7_ zjfs;pBo;3~^UEAfJfLB@FT9j`te~Tj-X^(;k$-x1E`M?1`hbuy($1dDqhIIbd!F2&{?lsOuUTA;7^g#4(qR~dsfgDj$iKC`J z)eSik8J9e|dw2Xe18C$GKke^(TUu$aZ;&mW9V&kLasWDFa7jt?ja2+;j=(4!G2mE4 z{ig>(R1!KRg;r8- zX6oRO1jwuw#6tjs9v;&g_U*lO;X~G|Af07OWP#lE7%6vwLWJauPsVCjMEDJ_v%%%?!D@x_43Tda=H5L zLjl1yp>La4z!JwoO8WChn2h}W-x`;c=d*TbD3B7M3eC6SK1oQYTzMzb>acf{7g`}H$1B+E6ElRoR z(0PM|0lmt?-)$10M)T`@-$vnFejM{ZIAB_0DOlOvedOUmmzz6b(BKnMUr%&#QQ>gO zR9z(|xsY(Dr_ISJ>S~>rP(aK0_RY?Rv(NXe4k%7YaV)G#YyXa2kgB5K_}yG} zXJe8x3_beC1)6x|IXF-U{v#B6Gmy;Njk)SpT*{LLaHa5m+CKY~-+2I?Sh`qR zC$zd~80QU!t2Oh2HSm!0KX--J)Np~;Ur@L`5A6Go&buXg_MyLtIH?0c)#BhtkGvaM z<97z##C=dMcBfX`S3D>?aATtH+!Gar2d+cbp(ND+E|CZN^ zrvTO!mI+B~=<5tW`+HdkBp^`XtE&S$SQ%NaM&Dvxw{$203IXdApPDKOY&oPw==gn; zWgUs5ODw&-@Fph0H8q!AuKe!F%G#(ti@soowD7Iao{)6COMHCSW)FJt>M2lma!O0n zpi@-M8erhynBMRzR4;%>8UmLH&D;F>E)9|e86Tf^$aN~lo7H4clx8CQx#6dv7@l1( z8X6EFqDh9IRa|z%AC$4g?{no8S|O4Mb4!OvH(_wT!-dg(Q+`vhqR9dLmx% zsYWp4=TAg_9)IV@(CC8$J09L88+)qd&0>o6byBJPsiy0kiS^%`*GO7 z05^xBaEn#ev)FiosHUbnA&V#Mgy>R|ApVe$bOGZEb#G{VJWBeW{MTo7YXx3Hd=+{R zL0@NKQOGUhNe&C6tmw>&JLEZxjsm9)7{zpbALSbu* z6x3TwZ|?-ydNocTQ#QwX6;z_63bh)`Zcu9Ey>D;FMAGmM)k{lDvrCSg7*Fi%NRv6w z>5_Y~Zo6F(X%jKBkwCS%b#bI|OX6ZLY+*rnbgbHQFlxGvK|HXqI z`Nj_Q%Eg%}Xi^LhA5Inw;GxOCuS^yoEYu3o`s%Xb$e;+3|7vpbj{jw4)ae)| zPwzT!8RN?1S9EuylaOeE9-32kjt=?tsor}P8JV9$(^Pwl&LG?$9Ndhd7RxUxN?2We z4&|#9(sDl49!U>>gwJBECWbnOCNJqaA;vET0EH{yuY97 zd!5nVo(U(Y)N+oyku#sV+ngyR+J`Yv8fIlcBpl3oa^k6<x~^fL9Svh6IUaKIK-D#$cIJCJin?XdJ3_o)Mfg3~=w^KdcVrP2N@O3! zU0iY=GTf}IYMg5`FDt32kkX&RoAaMScD4HYZ|IfZiV}b_^ZK`~nY()$O@Q6ULi;cv zxW&&trGsw0wE6X=#rRRR%qeS4?2f#{r+2{K8oJ7j-lo%1J9yqd&5ZgX77NYRoHFLA ztME|hsU)K~>sb76@C<+={{8!p8t-zX#UnLMzs*{7(!482v3XoJo&gj#Fe*&fPso5& zh9qg0VyH|std@Sbqy}c`Nm67pD6Gl*)V$BvkbCp4-!4^VYj$J4G8St-|6Kz4mqqjX zcb|-jrG$l)YUhvWWQnK-y{xU@J2t&k0Qx#7r`*nA$GAFD{*o|83*igv{O=zCn4G@; z!e6uEGS8l&(D*sM`Sb}hpBIgo=M^4}P@^r*LL)Yh)^FfZNcDKkN+M<_nibI`b7JEt zvl%$`dP|)i#`$CODXn3M3t~^HJZ3{G{nHKSZ+CW_-zATE@&<^R<*(wSI?c1hib9HL zZDlnxI|Qw7f5Iv$T{!7gQ&UVr7Akp&q~C{lyo_`f3G{F&B_M9#%l_t*w=KBTjDcck-?ZXBek9Aw zPsYN+8du48Qbja%$TKc^)x2j3KnGIB=PuFPA|0LaGSkwfe(p(G)I^?FTt-3#_gHya zf1AtMV*ctes*8E|jvuM6ra6*xr=9Hhcs&pG{eZZNn6R_3x4+deTTwiS^|R}K*ibA{ zcJ_-=g}M6hZkz`caw1wogfzViv?wkuLlVwXN@a8-L>;zRqupv-hju@Vxyoy8nk_~C zxnJ!r452zPyQE5%sH)1A;Q~GF)I=& zr+IjV|LSMX73{u6H1FqW_n>EAB&82`1BtOPQ4u$wOGv>aVmUR{q(*{BlBShpX9mVj zG5uY;F(Y|M;04uV72s8j%frX*q95i#%hbd7j7jj0J3%ld#mYB@huge&?%Acu%BcC= zm^Cb-PkyD!$jocd5$;75-olE?$D`@8hS5l`KYUXGye~u%0#n8pQFoErdShb(?93tq zAz547+e*2vq1H;ZX1dGy9_dbbA;RzE9w}Q^^=CGCxD#h6$ti`!#|u&^wy@G&5Lpuy zHSB}HjSTn38EJKO2B2M`M)^3kCU_w)bz*%pZ64`m1BeZ!xVIyc5fnqgm+$MV*|0}h z!;Um!39_@TX4_A{py#Ogm-a2~xL-NffkCwl74i zZOG*XxW#^W#25fPN2)MH`od;Uwf7~wfEt6&KE ztf0!oh@65r#|g!J0aPEVV7dA);8;D5Gq zA98i4HQ)A4EYV5~gEe-7W+U!YHc{_iPXewPR8+(`akR`#TYp+7su~R*j&-qw566>3 z+z!Zez73yHxiv+CpN5e^qYx;K$k3Dfl(j^30(s;qO^C9$2-qCJbCG4VvY{bgwb~X6 zJ@Kw_byJ4?x??V)xzxeXXiP(UOcB0kWku*>SR#WS_>EXR<{8%UUQYF=@0K6=q`bG{ zz3H?17)itA$v5y2pgrwHZ+%H@E^i}M6AF6CGV!hR0_lssY%AjxY@_N&Bv2qU- z-Fi18ue-LwHL?hd?fJUFQIl$AJNxaQbB@N|-jPr|!6dfaaH1NYlmr;h^N&E6MRWzq z%uYw_$Wuncawea({&n`hkJ51dqNS33bpGUp_7!VCu8wFlkpU~Me*mnF64w`r(EW3R z@YdS;$KidM6=UHg`wqFQ154cUp8Ltg4SW?(1G1)RiCJ(ObsinZPyA6gbg*86dyt&`yS4Q? zvithcx#l@`mMON^q86o4sP&!E!fXx|HKnDx010ed5F<{=H?4HhxE3hLh#_NYP$^Gc|qT2Xq>$9eUqz9`l zEQZZHe@_MYj^XEA5--cX=!$Wc@kDE>T=)(lXla@H?j1_wc~R<{H_$pirgo;;^{uCTQ>T;*E7+Sh;_(WLg$#hR&oqk#8YIUF2Rf1cS7t@_2;AP)5T)EoT)R(P^; z+x9oy-80-gW7M7{by{U*tU&7xt~rlQ&4z4Tr~2DL?k)oi8w$6yhlI>P0e{8VfRmu& zvm&R<6sX5RFT>2tsn=E@!;bH-r0LSKQL>s@umu3Vs_I_j)psP8938FleBvZV0ruCd zETpYX{NLw9K=rg79N?hbsKVaoZ0enfO75n#aEy!M^p^5Q?Vir}r9I!r2$Zr(euDa_>iG(|?=z{qP5vaoqVH9`lGb^bJI> z>G$`V-w9rZ78+bt{rOq%5)z4IjX&(ZN_s3@;)?XaJdH(aeloJ?uB(r4Ry$Vj^>W%j ze!}T{M8u|eCt)rwjtnTHjX&f_N)LIT?Cduy^;Sk!HJAd6_2lB!6+irlSz56DZW(pd zm!>N)X%r!e*ZhQ$o>%&j$+tJlc*IhAq31z|_BYEXT0u{q3%Cw5^7rnUpFb)(Z+-68 z=^EE-0ZjzhO1VKXo}?1Er)u@LkpsgYFHe&dsjxA$XSKI0C@DpQ*39hgPKE4|_v~n) zki)-9N=y6u`VM*zgI(7mpe^&5{H4Z02)fo%v2)Ecs3jX2yxb+$a*4F;eWfibw>0oK z@@2ld*_TXi)0vsMj*NOBT|3%iq`bV!I)#}uqhPWO3cdv6%+kdf9kMSJ68ckl``7c( zMe0`5DzZSh#o#fPfYwoNJ#kS z=*?qB&vd5Hb)q}f5yr&X3~>b6bV4#KWfO@EieRz&31ld!Z7M1az%bp5$duSafoNr6 zdz=Ff6i9zAt_eUFQQ1=55hHFL)9!^P4=y1#Ik}IsNi;XtVm?)YLNlzaGa09ZT9~A$(+#YU*R63TH44kc`nsXnD3(lVUG=ztTqf<-x4@6PmLZ55l z>e{&^P=g?kiunB5_~1~Z*5{ZgnKO%)Z)A><;<2aer*0?gko2EV1CSdGH0L5Gc`5b@1G4i_&o4wXsXf%oGQwH$5{nq2Laax`5L<-epb~}Lz^lU z)iF+otB-bOe1Cwg6TCYyo+ZfXfwK+Jm{%q%dHqgJ?n9o3af0Zh!fX9#0|X~Uw$n&_ z&wwf&9bFhWjQy|9?t{OP+DK6@o&j71U~e5ACEwVlTI)$l@VoK`W97YKc2hN?)KuLC zqpKBhqzig)kL~N%v^qz>!5zX{1KRudEq?~)`e>?1kw2D#;jd{WrKOPoC|$y+J(V{V)P)^=gp^bx zauz`$?7w|1-~*%%s3E4@l6NYZQxjN9^7@9Dz0CmHlZi((n%2g_Ssbj5C=Pi|E6DG4|Nan?r@CvT8JCgaWz3us zgaCTzqVw7-9OSAR@56;&x6mk@hR#9E%pM9(NP3dF>L1ka-(lR=gYk#o@DSR=2|Ldg zBUxD=xMnmO-Hui%?>09vYf~fQ;QEOHiu#C)Vy=4ZJCa_YUB@6ftUU?%f|TP63UVJQ zdugzk1CEHnRTVtekN%EwE(^@fI}&A8Ri3uCVba^HHpmi>CjIuEkNZsUtpAag7&wGa@Zx%HxHK-vho|EU=UseAoc4&H#B zJ`a8pC@LSu(mV{Qrh|dyV`B@9ysLb=o}&)F?kl<1pN%>wI0MDlNGKo;B9&H1Ei;AE zHQyca;aY*tTS7t!xN+dKV3Ts-JOx;IxJC$#j}kQImzN1($lKSx_IrhNZtyGuNxK4g zQ(ZH0RejxQchU^-HMnAta|Y{M7u%JcW3ou=%&%W|MTR1eocFmwqJsI8twUF_;A%O9 zZ%V%wfJkfGKY765fEk_S?wGTE@OsIWs#3&_HxoH1Zqi^%z^9^WEzn5Se752YDl&u`Fthnh8ZacHRRygSQkI&W^ZFIG;F(Ku>aX@+5n# ziS?!AWWy+#G`Jsr|3(KPB{nH({ujj@n8`~@cSlyvD8IHHZ&T*=E1I zYU$8vtp%r-Ts%UxU7mAd$T<{le*Vzn;%I1xkpK-mUy3?9%|$m}^O~s0*M5Mx99YPU z{g_G!`PRwFcT>B3Frfp~JrsteNX{>x3+^)rj^pP364IBK#~l6f=MM>hZ}@;avit(~ zw7_De*W#iLUA(q`|BN>dS2A=Hp_D4EP?0AEs&4e=ZutfbqADjX|3kI03BZX zF+VpqtGLPGQ=xqdq@X#k^*Wl)d+Bc=GJ1N;kuyEm*y?~Agf8x9hR#n}G{FNMGwRLF^--zrG07Ku#Q7La|DEDo-{*Zh- zV5XQ-f?6Iqw&&;c9>miMy|FuaP>8{*2;+M{ZE@EKzUS``V9&dcN3M;Hc><-ZBHxKw zUfwI8n7!2{8TIz|MIaXk{tnqvYA4c1#U+jNYzYl!# zLQag55Oj}QTMRHNHTzFtz#m4-JVB!XS+D*2BFhL#pMnXU+HQG8l8F!|&^~_u9(cVk zA^!0*%gj>IJs*PE%rmM)CG^gi&i&mhLmW`oc6RQ9f?!tZlQ&gqz|lEpBoCfu;A_Co zy+CzQ+$?gV9<|QUFV%kH=g*EYkYHynLEc*&wd=hs@nkl2Jaaw%V*|n!n1haYZajza zI#s#|0$@u3__>_y@a{}|D+OG)$E96h2T6iC{VoTQ>zNA{c+6oeqJbAf_q^i);%@1b zEF=Pu3%qpAUYMF*(U^NtQ}U!r%E;gm6W4+(2~hlKk!$5)YR+?*`m+bC05qS|bz@-O z6(}sc{8r>ZRf6|vMx}@oyj~!ZH~~gLHk8lG_7Ak{QOB9(azelndv%b^lqtGl<>X{p zt?;34f;TB@6)9q#&hvo^7P^ynQMEuENKrB+`&RB&4E6E;nuGw5-Md9E7yKQ~gg`-h z{pKdjSPpd3Bp=TDGJBLiTXDEg{Eq{~L5fnHZ!jA33!W2FqgL*votnC7$3OLgvFkpO zi{HzKQ1A~r?pndzQc71BOpxXXB)!z%!*1>u?0Jw1Chbp3;9#}!a9SvQ_%oyK&?D;^rKF|-4rpv2K512r0hKUpYsohW&D);2JT9*~1 z37JkrWPgPUopY!2LEw!amgQi|?HkSy2AKO4X3#+V*=gEgH)D{b6BbVF=@A7rMM_ao zxS(KY_EkVL%y}qm&GZ)*?|yS2&M#0c&`S_4N%${B`V9%~&0yNd-%c?)XY%!yotd5(xiJOJ6nTrLy zAb7ZVc-Xo4*m<}#xrKzec!c=`9&m99b8$(M3kCi^AFy{cx3Tp8|9^m6{uOebnHKpC zu8x*&o+d692x~VtCs$!kP8)j@D+~CsF?+$`=wekrNRp1_{F^b5u2n#D%*jQP+As%w`#6)Slf@ALfkMsQ3G1@lfZq~>{ zDE)3s!y#n<;}8`adkZ&jCkw=Xu8Pyi!Ahb`OZ9)9M@18kLU8jx zsg|H7g@!(c6h8>#={>Jq(~FhTb2<0poaedsa@ zRP+487_TXTA5bn7a2|^x6^)|A?_0%3rGjEUkA1HfBRU<1wOZ_XJR(Q6-|${8W6N&G zTz@~d9S%r~RuNIBQ{*Dc%OH*t#as?YfdEb;5lr>gVLZlXM7*X$A{Q{v3l4EfBV*HI!K7Y~n^Y3{hZLTU z%*ci(kPVGjBRnp0`TL~c5SKJE?yj!5M_yc?Ch(N7AR~=d_4JOfpd8hRLw=G*#-ZPj zAx$UZYpZxUS>WFstLbDt<)}uSA&mwHTqEPZda-`qZ(7^)pX)zHT(SOd|5A1SeR)S7 zD*fwi^iSEp3(>z&WB+y~-rMH=6Y~=c;;eptjKiz<4Nw(53jiTV0QmeT z0AO+eK!TCqNHhWsf@B*U7TDTeH=4^cLCr2E-p+E@w1AlC4z&AF`V&wikH?xjxjwIB zvmCO9q&lJ|>ld`-*q^2oRD3RDlN9QplKV@v1mur@;&P5$M#=nvsOzv%B5)1N^P~oC zJ94t$icF}fkt&C36y5P2HD)8I>})hY(e2<7ccckesVEO7DNq!imce%HvWc?t0cA9j?Ed zn3J>F)?pRevjT~krm2tCyEr;HSTUKh9cd8xrs#S)>IA?Sw)n`F30x(7lZIJs13`M{~NrSMDfFqnULbBYMx(udl$(j80cf z;VNJn({V=*czBp9*&lBIwft)R+k8!a;~XXUT7pgg7r80yv4eq7D2={}u(p?NUli_9 zQ9l~HPfZSvhlPX;<(O`zA2K?h_oh+;CM~^Yz^DG^%?wd&=aspo>8^|#+wSnVK2%qhuCZECXoz-xNaKa zZr(d&Y>nS47(9}&Dk-!~En|$riK|~CDykonL(X);zJ5I11-mVs@8?(Y)sz4e+{nzw z1}i%TStOSgu^u_wQx<#l^AsZoYVju(c`H?%5TSTcwIuGBT{)-9?7(>JLm* zI}FZFS)~dWl~W@jFgZMKUw=+ zDFcBIKbn(IA=EjB^ruz(t~WdERM_*wdt>ReE$f$#m?iaTtm>shhGHcH(FA3dz_-?b z(C_OrZry$E;3wug8!R~)nNi@cWHct=v$acG8V)z7(9+;Xo_+j# z$hR{ZF#Wz7p+tyExDw@Vv zXJ?aMb|4(d*;_J@`_n>^q_bqvbA+;iLprxE%MrW=(SwSRj$yGYn2n#Z%zMpj6V!+ z)-lJ-VAp48x~=l?3sI8sIcJguntQL2&#wB)Y3gCW7djFw>Uss_!!cfNK zLLz-IvUkkm@3gm->|7nStWaG1QXH}#pAbE}&p5C%;ro1>!|Urqxm%mEIeCxs`FEWe zrz?h%3kaP{?~fsK!$Ydguvlb5g0Q6V231IS%PNU`RpA4pmgrQ={`;42*kZHOL_y&* z-azyE%qM3jxTO9giL_~&^M!|TXy2b44(H=Y`~v!8D#A)9cv_*%Ue^Bn`6c;)FY&?> z)u~l(dxfs|YiaA4?ne?@ukX0J6YOK$3qhMa^y1Hz73edgul#(pm$x^|fCpWDJS_Nq ziSF+37x@jZR&p7`_f!(0sTPB}WUpPs(~*agp2uj+C#R-TJ1#kGtU^nRi^VO8QYf;@ zlYGP7>|)1L5r;`6q@;+5lr|6*CEw71pJbhB)Xff@JdFpN@M)GmM2BX3tMrpeLc-&b zrZCgIvW(2kwtPd=oA$Dz^FxcJ6`v2y&6RX@qeR6%Ze0ozqR`JWxCjbJx0-k|OO~^)8`Ur7!QA)tS!_k;Jc0LRi=v{La4Wc}kn*GwV@&dw7I0 z+~v3+aDpl_EnS%uPbL-Pb(RVWKpUy8t%=mt)J8g?3^7P;jIO>q5{W?~&7=*Z|Hr{U zkV2=0{eK4(E@pdb%WY=}4x}+dJ9dUt-ho3vMCv!hj_yxohB2tXzpFwp0=!M{I}`p9$Ic0)0F)jYscV4J(bLcD z$?OJ^y}!lzgC0%=kQNqM#U=}&$@w=E$q^6G=zdi2>InY;reB~pg64US?iCmijQH=9 zBMkH?ItGR)eJ`{DjZ8&R4Lr4JXcSV9qGv!gK6Xw - - - - - - - Photos.network | {% block title %}{% endblock %} - - - - - - - - - - - - - - - - - - - - - - - - {% include 'partials/header.jinja2' %} - -
- {% block body %}{% endblock %} -
- - {% include 'partials/footer.jinja2' %} - - - - - - diff --git a/frontend/templates/error.jinja2 b/frontend/templates/error.jinja2 deleted file mode 100644 index e06841f..0000000 --- a/frontend/templates/error.jinja2 +++ /dev/null @@ -1,28 +0,0 @@ -{% extends 'base.jinja2' %} - -{% block title %}Error{% endblock %} - -{% block body %} - -
-
-
-
-
- {{ status_code}} -
-
-
-

{{ error }}

-

Please check the URL in the address bar and try again.

- Go back home - - - - -
-
-
-
- -{% endblock %} diff --git a/frontend/templates/index.jinja2 b/frontend/templates/index.jinja2 deleted file mode 100644 index 4d388f7..0000000 --- a/frontend/templates/index.jinja2 +++ /dev/null @@ -1,29 +0,0 @@ -{% extends 'base.jinja2' %} - -{% block title %}Index{% endblock %} - -{% block body %} - -{% if not username %} - -
-
-
-

Login required

-

There are no public images available, please login or check the link.

-
- -
-
- - {% include 'partials/app.jinja2' %} - -{% else %} - - - - {% include 'index/grid.jinja2' %} - -{% endif %} - -{% endblock %} diff --git a/frontend/templates/index/grid.jinja2 b/frontend/templates/index/grid.jinja2 deleted file mode 100644 index c617a8c..0000000 --- a/frontend/templates/index/grid.jinja2 +++ /dev/null @@ -1,22 +0,0 @@ -
- - -
-
- {% if files is defined and files|length > 0 %} - {% for item in files %} - - {% if item.filetype == "video/mp4" %} - - {% else %} - {{ item.name }} - {% endif %} - - {% endfor %} - - {% else %} - No files to show - {% endif %} -
-
-
diff --git a/frontend/templates/index/shared.jinja2 b/frontend/templates/index/shared.jinja2 deleted file mode 100644 index 59693bd..0000000 --- a/frontend/templates/index/shared.jinja2 +++ /dev/null @@ -1,11 +0,0 @@ -{% extends 'base.jinja2' %} - -{% block title %}Index{% endblock %} - -{% block body %} - - - -{% include 'index/grid.jinja2' %} - -{% endblock %} diff --git a/frontend/templates/index/user.jinja2 b/frontend/templates/index/user.jinja2 deleted file mode 100644 index 88d43f8..0000000 --- a/frontend/templates/index/user.jinja2 +++ /dev/null @@ -1,171 +0,0 @@ -{% extends 'base.jinja2' %} - -{% block title %}Index{% endblock %} - -{% block body %} - - - - - -
-
- -
-
-
-
-

Profile

-

This information will be displayed publicly so be careful what you share.

-
-
-
-
-
-
-
-
- - -
- -
- - -
- -
- -
- -
-

Brief description for your profile. URLs are hyperlinked.

-
- -
- -
-
- -
-
-
-
- - - -
-
-
-
-

Account Information

-

These informations are only used by the system.

-
-
-
-
-
-
-
- -
- - {{ username }} -
- -
- - -
- -
-
-
- -
-
-
-
-
-
- - - -
-
-
-
-

Notifications

-

Decide which communications you'd like to receive and how.

-
-
-
-
-
-
-
- By Email -
-
-
- -
-
- -

Get notified when someones posts a comment on an image of yours.

-
-
-
-
- -
-
- -

Get notified when someone tags you in an image.

-
-
-
-
-
-
- Push Notifications -

These are delivered directly to your app on the mobile phone.

-
-
-
- - -
-
- - -
-
- - -
-
-
-
-
- -
-
-
-
-
-
- -
-
- - -{% endblock %} diff --git a/frontend/templates/login_required.jinja2 b/frontend/templates/login_required.jinja2 deleted file mode 100644 index 6259357..0000000 --- a/frontend/templates/login_required.jinja2 +++ /dev/null @@ -1,96 +0,0 @@ -{% extends 'base.jinja2' %} - -{% block title %}Login{% endblock %} - -{% block body %} - -
-
-
-

Account required

-

To get access to all available system features, an account is needed. Please login to start.

-
-
- -
-
-
- - - - -
-

People

-

Search or filter for images containing a specific person or object.

-
-
- -
-
-
- - - -
-

Location

-

Find a list of images, taken in a specific areaSearch easily for images on a map.

-
-
- -
-
-
- - - -
-

Privacy

-

Hide private images behind a quick filter when showing co-workers or friends fotos.

-
-
- -
-
-
- - - - - - -
-

The Catalyzer

-

Fingerstache flexitarian street art 8-bit waist co, subway tile poke farm.

-
-
- -
-
-
- - - -
-

Bunker

-

Fingerstache flexitarian street art 8-bit waist co, subway tile poke farm.

-
-
- -
-
-
- - - -
-

Shooting Stars

-

Fingerstache flexitarian street art 8-bit waist co, subway tile poke farm.

-
-
- -
- -
-
- -{% endblock %} diff --git a/frontend/templates/partials/app.jinja2 b/frontend/templates/partials/app.jinja2 deleted file mode 100644 index 2001587..0000000 --- a/frontend/templates/partials/app.jinja2 +++ /dev/null @@ -1,29 +0,0 @@ -
-
-
-

NATIVE APP

-

Try our native mobile apps

-
-
- - -
-
-
diff --git a/frontend/templates/partials/footer.jinja2 b/frontend/templates/partials/footer.jinja2 deleted file mode 100644 index 11db3d4..0000000 --- a/frontend/templates/partials/footer.jinja2 +++ /dev/null @@ -1,41 +0,0 @@ - diff --git a/frontend/templates/partials/header.jinja2 b/frontend/templates/partials/header.jinja2 deleted file mode 100644 index b61a7a5..0000000 --- a/frontend/templates/partials/header.jinja2 +++ /dev/null @@ -1,37 +0,0 @@ -
-
- - - - - - - - - - Photos.network - - - - - {% if username %} -
- {% else %} -
- {% endif %} - - - - -
- -
-
diff --git a/frontend/views/callback.py b/frontend/views/callback.py deleted file mode 100644 index 736053a..0000000 --- a/frontend/views/callback.py +++ /dev/null @@ -1,57 +0,0 @@ -import logging -import time -from hashlib import sha1 -from random import SystemRandom -from typing import TYPE_CHECKING - -import aiohttp_session -from aiohttp import web -from frontend.base.request import RequestView -from frontend.const import SCOPES - -if TYPE_CHECKING: - from frontend.frontend import Frontend - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -class OauthCallbackView(RequestView): - """View to handle oauth callback after login.""" - - requires_auth = False - url = "/callback" - - async def get(self, frontend: "Frontend", request: web.Request): - """Initiate the authorization code grant flow and redirect the user .""" - - # its not a valid redirect, if 'code' is not within the query - if request.url.query["state"] != frontend.core_client.state: - _LOGGER.warn("invalid redirect call! ") - - # redirect to oauth provider - raise web.HTTPUnauthorized() - - code = request.url.query["code"] - state = frontend.core_client.state - - # request a token pair from the authorization server - provider_data = await frontend.core_client.get_access_token(code=code) - - # calculate timestamp - timestamp = time.time() - expires_in = int(timestamp) + provider_data["expires_in"] - - # collect informations for current user - user = await frontend.core_client.user_info() - - # save data into session - session = await aiohttp_session.get_session(request) - session["username"] = str(user.username) - session["first_name"] = str(user.first_name) - session["last_name"] = str(user.last_name) - session["expires_in"] = expires_in - session["access_token"] = str(provider_data["access_token"]) - session["refresh_token"] = str(provider_data["refresh_token"]) - - raise web.HTTPSeeOther(location="/") diff --git a/frontend/views/index.py b/frontend/views/index.py deleted file mode 100644 index acf1c28..0000000 --- a/frontend/views/index.py +++ /dev/null @@ -1,53 +0,0 @@ -import logging -from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict - -import aiohttp_jinja2 -import aiohttp_session -from aiohttp import web -from frontend.base.request import RequestView -from frontend.const import KEY_AUTHENTICATED - -if TYPE_CHECKING: - from frontend.frontend import Frontend - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -class IndexView(RequestView): - """View to handle Status requests.""" - - requires_auth = False - url = "/" - - async def get(self, frontend: "Frontend", request: web.Request): - """Retrieve photos grid or landing page if not authenticated.""" - - files = [] - - authenticated = request.get(KEY_AUTHENTICATED, False) - - if authenticated: - files = await self._get_files_for_user(frontend, request) - - response = aiohttp_jinja2.render_template( - template_name="index.jinja2", - request=request, - context={"files": files}, - ) - - return response - - async def head(self, frontend: "Frontend", request: web.Request): - """Retrieve if frontend is running.""" - return self.json_message("Frontend is running.") - - async def _get_files_for_user(self, frontend: "Frontend", request: web.Request) -> Dict[str, Any]: - """Load files where the current user has access""" - - response = await frontend.core_client.get_photos() - - if "results" in response: - return response["results"] - - return [] diff --git a/frontend/views/login.py b/frontend/views/login.py deleted file mode 100644 index d38e849..0000000 --- a/frontend/views/login.py +++ /dev/null @@ -1,36 +0,0 @@ -import logging -from hashlib import sha1 -from random import SystemRandom -from typing import TYPE_CHECKING - -from aiohttp import web -from frontend.base.request import RequestView -from frontend.const import SCOPES - -if TYPE_CHECKING: - from frontend.frontend import Frontend - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -class LoginView(RequestView): - """View to handle login requests.""" - - requires_auth = False - url = "/login" - - async def get(self, frontend: "Frontend", request: web.Request): - """Initiate the authorization code grant flow and redirect the user .""" - # create an opaque value to prevent cross-site requests - state = str(sha1(str(SystemRandom().random()).encode("ascii")).hexdigest()) - - frontend.core_client.state = state - - authorization_url = frontend.core_client.get_authorize_url( - scope=SCOPES, - state=state, - ) - - # redirect the user-agent to the generated authorization endpoint - raise web.HTTPFound(location=authorization_url) diff --git a/frontend/views/logout.py b/frontend/views/logout.py deleted file mode 100644 index 09b6891..0000000 --- a/frontend/views/logout.py +++ /dev/null @@ -1,28 +0,0 @@ -import logging -from hashlib import sha1 -from random import SystemRandom -from typing import TYPE_CHECKING - -import aiohttp_session -from aiohttp import web -from frontend.base.request import RequestView -from frontend.const import SCOPES - -if TYPE_CHECKING: - from frontend.frontend import Frontend - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -class LogoutView(RequestView): - """View to handle logout requests.""" - - requires_auth = False - url = "/logout" - - async def get(self, frontend: "Frontend", request: web.Request): - session = await aiohttp_session.get_session(request) - session["username"] = None - - raise web.HTTPSeeOther(location="/") diff --git a/frontend/views/photo.py b/frontend/views/photo.py deleted file mode 100644 index 0fde185..0000000 --- a/frontend/views/photo.py +++ /dev/null @@ -1,42 +0,0 @@ -import fileinput -import logging -import tempfile -from hashlib import sha1 -from random import SystemRandom -from typing import TYPE_CHECKING - -import aiohttp_session -from aiohttp import web -from frontend.base.request import RequestView -from frontend.const import SCOPES - -if TYPE_CHECKING: - from frontend.frontend import Frontend - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -class PhotoView(RequestView): - """View to handle logout requests.""" - - requires_auth = False - url = "/photo/{entity_id}" - - async def get( - self, - frontend: "Frontend", - request: web.Request, - entity_id: str, - ): - file_response = await frontend.core_client.request( - method="GET", - url="/api/file/" + entity_id, - ) - - resp = web.StreamResponse(status=200) - resp.headers["Content-Type"] = "image/jpeg" - resp.headers["Content-Length"] = str(len(file_response)) - await resp.prepare(request) - await resp.write(file_response) - return resp diff --git a/frontend/views/settings.py b/frontend/views/settings.py deleted file mode 100644 index 4f1f10d..0000000 --- a/frontend/views/settings.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import TYPE_CHECKING - -import aiohttp_jinja2 -from aiohttp import web -from frontend.base.request import RequestView - -if TYPE_CHECKING: - from frontend.frontend import Frontend - - -class SettingsView(RequestView): - """View to handle user settings.""" - - requires_auth = True - url = "/settings" - - async def get(self, frontend: "Frontend", request: web.Request): - response = aiohttp_jinja2.render_template( - template_name="settings.jinja2", - request=request, - context={}, - ) - - return response diff --git a/frontend/views/shared.py b/frontend/views/shared.py deleted file mode 100644 index b5387d7..0000000 --- a/frontend/views/shared.py +++ /dev/null @@ -1,46 +0,0 @@ -import fileinput -import logging -from hashlib import sha1 -from random import SystemRandom -from typing import TYPE_CHECKING - -import aiohttp_jinja2 -import aiohttp_session -from aiohttp import web -from frontend.base.request import RequestView -from frontend.const import SCOPES - -if TYPE_CHECKING: - from frontend.frontend import Frontend - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -class SharedView(RequestView): - """View to handle shared links.""" - - requires_auth = False - url = "/shared/{entity_id}" - - async def get( - self, - frontend: "Frontend", - request: web.Request, - entity_id: str, - ): - _LOGGER.warn("/shared/" + entity_id) - - session = await aiohttp_session.get_session(request) - username = session.get("username") - - # TODO: get files for shared album - files = [] - - response = aiohttp_jinja2.render_template( - template_name="index/shared.jinja2", - request=request, - context={"files": files}, - ) - - return response diff --git a/frontend/views/user.py b/frontend/views/user.py deleted file mode 100644 index c967a86..0000000 --- a/frontend/views/user.py +++ /dev/null @@ -1,85 +0,0 @@ -import logging -from hashlib import sha1 -from random import SystemRandom -from typing import TYPE_CHECKING - -import aiohttp_jinja2 -import aiohttp_session -from aiohttp import web -from frontend.base.request import RequestView -from frontend.const import SCOPES - -if TYPE_CHECKING: - from frontend.frontend import Frontend - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -class UserView(RequestView): - """View to handle logout requests.""" - - requires_auth = True - url = "/user" - - async def get(self, frontend: "Frontend", request: web.Request): - """Display user profile and preferences.""" - session = await aiohttp_session.get_session(request) - username = session.get("username") - - user = await frontend.core_client.user_info() - - _LOGGER.debug(str(user)) - - response = aiohttp_jinja2.render_template( - template_name="index/user.jinja2", - request=request, - context={"username": username, "first_name": user.first_name, "last_name": user.last_name}, - ) - - return response - - async def post(self, frontend: "Frontend", request: web.Request): - """update user profile and preferences.""" - - _LOGGER.warn("apply user changes is not implemented yet") - - data = await request.post() - _LOGGER.error(str(data)) - - if "first-name" in data: - first_name = data["first-name"] - _LOGGER.debug("new first-name: " + str(first_name)) - - if "last-name" in data: - last_name = data["last-name"] - _LOGGER.debug("new last-name: " + str(last_name)) - - if "about" in data: - about = data["about"] - _LOGGER.debug("new about: " + str(about)) - - if "password" in data: - _LOGGER.debug("new password: " + str(data["password"])) - - user = await frontend.core_client.user_info() - - _LOGGER.debug("user: " + str(user)) - - user_id = user.id - update_url = "/api/user/" + str(user_id) - - raw_data = '{"firstname": "' + str(first_name) + '", "lastname": "Administrator"}' - _LOGGER.debug("payload: " + str(raw_data)) - _LOGGER.debug("payload: " + str(type(raw_data))) - - # TODO: patch user - - # await frontend.core_client.patch_user( - # method="PATCH", - # url=update_url, - # headers={"Content-Type": "application/x-www-form-urlencoded"}, - # data=raw_data, - # ) - - raise web.HTTPFound(location="/user") diff --git a/frontend/webserver.py b/frontend/webserver.py deleted file mode 100644 index 4ad8026..0000000 --- a/frontend/webserver.py +++ /dev/null @@ -1,173 +0,0 @@ -import importlib.resources as importlib_resources -import logging -import os -import time -from ipaddress import ip_address -from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict - -import aiohttp_jinja2 -import aiohttp_session -import jinja2 -from aiohttp import web -from aiohttp.web_middlewares import middleware -from aiohttp_session import session_middleware -from aiohttp_session.cookie_storage import EncryptedCookieStorage - -from frontend.const import KEY_AUTHENTICATED -from frontend.i18n import i18n -from frontend.views.callback import OauthCallbackView -from frontend.views.index import IndexView -from frontend.views.login import LoginView -from frontend.views.logout import LogoutView -from frontend.views.photo import PhotoView -from frontend.views.settings import SettingsView -from frontend.views.user import UserView - -if TYPE_CHECKING: - from frontend.frontend import Frontend - -MAX_CLIENT_SIZE: int = 1024**2 * 16 - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -class Webserver: - def __init__(self, frontend: "Frontend"): - self.frontend = frontend - self.app = web.Application(middlewares=[], client_max_size=MAX_CLIENT_SIZE) - self.runner = web.AppRunner(self.app) - - # check if secret length is 32 bytes - if len(self.frontend.config.cookie_secret) == 32: - _LOGGER.debug("use cookie_secret from file") - secret_key = bytes(self.frontend.config.cookie_secret, "utf-8") - else: - _LOGGER.debug("use fallback cookie_secret") - secret_key = bytes("Thirty two length bytes key!", "utf-8") - - # TODO: switch to Redis storage? - aiohttp_session.setup( - self.app, - EncryptedCookieStorage( - secret_key=secret_key, - cookie_name="Photos.network", - ), - ) - - # init jinja2 template engine - pkg = importlib_resources.files("frontend") - env = aiohttp_jinja2.setup( - self.app, - loader=jinja2.FileSystemLoader(pkg / "templates"), - extensions=["jinja2.ext.i18n", "jinja2.ext.debug"], - context_processors=[self.username_ctx_processor], - ) - - # setup i18n - env.install_gettext_translations(i18n, newstyle=True) - - self.register_request(LoginView()) - self.register_request(LogoutView()) - self.register_request(OauthCallbackView()) - self.register_request(IndexView()) - self.register_request(PhotoView()) - self.register_request(UserView()) - self.register_request(SettingsView()) - - # self.app.router.add_static("/static", os.path.join("frontend/static")) - - self.app.middlewares.append(self.auth_middleware) - - async def start(self): - """Start webserver.""" - await self.runner.setup() - - frontend_port = self.frontend.config.frontend_port - - # use host=None to listen on all interfaces. - site = web.TCPSite(runner=self.runner, host=None, port=frontend_port) - await site.start() - _LOGGER.info(f"Webserver is listening on {site._host}:{site._port}") - - async def stop(self): - """Stop webserver.""" - await self.runner.cleanup() - - def register_request(self, view): - """ - Register a request. - The view argument must be a class that inherits from Request. - It is optional to instantiate it before registering; this method will - handle it either way. - """ - # if isinstance(view, type): - # # Instantiate the view, if needed - # view = view() - - if not hasattr(view, "url"): - class_name = view.__class__.__name__ - raise AttributeError(f'{class_name} missing required attribute "url"') - - view.register(self.frontend, self.app.router) - - async def username_ctx_processor(self, request: web.Request) -> Dict[str, Any]: - """Jinja2 context processor to extract the username from an active session.""" - session = await aiohttp_session.get_session(request) - username = session.get("username") - - return {"username": username} - - @middleware - async def auth_middleware( - self, request: web.Request, handler: Callable[[web.Request], Awaitable[web.StreamResponse]] - ) -> web.StreamResponse: - """Check if user is authenticated and authentication is still valid.""" - - authenticated = False - session = await aiohttp_session.get_session(request) - username = session.get("username") - - if not username: - return await handler(request) - - refresh_token = session.get("refresh_token") - expires_in = session.get("expires_in") - - if expires_in is not None and refresh_token is not None: - now = int(time.time()) - - # refresh token if access_token expires in next 3 minutes or has been expired already - if (expires_in - now) <= 180: - _LOGGER.info("access token is expiring in " + str((expires_in - now)) + " seconds. Try to refresh...") - - ( - statusCode, - accessToken, - refreshToken, - expiresIn, - ) = await self.frontend.core_client.refresh_access_token_call() - if statusCode is not None and statusCode == 200: - session["expires_in"] = expiresIn - session["access_token"] = str(accessToken) - session["refresh_token"] = str(refreshToken) - - authenticated = True - else: - authenticated = False - - else: - # still authenticated - authenticated = True - - validate_access_token_status = await self.frontend.core_client.check_access_token() - if validate_access_token_status != 200: - authenticated = False - - if self.frontend.core_client.accessToken == None: - session.clear() - raise web.HTTPFound(location="/") - - request[KEY_AUTHENTICATED] = authenticated - - return await handler(request) diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index d2da1bd..0000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -python-i18n>=0.3.9 -aiohttp-jinja2>=1.4.2 -aiohttp_session>=2.11.0 -aioauth-client>=0.27.0 -cryptography>=1.6 -colorlog>=4.0.0 diff --git a/requirements_test.txt b/requirements_test.txt deleted file mode 100644 index c0ef305..0000000 --- a/requirements_test.txt +++ /dev/null @@ -1,12 +0,0 @@ --r requirements.txt - -pre-commit>=2.17.0 -pylint>=2.12.0 -pytest-asyncio>=0.14.0 -pytest-aiohttp>=0.3.0 -pytest-cov>=2.10.1 -pytest-test-groups>=1.0.3 -pytest-sugar>=0.9.4 -pytest-timeout>=1.4.2 -pytest-xdist>=2.1.0 -pytest>=6.1.2 diff --git a/server.py b/server.py deleted file mode 100644 index fc53cad..0000000 --- a/server.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Entry point of Photos.network frontend.""" - -import sys - -from frontend.__main__ import main - -if __name__ == "__main__": - sys.exit(main()) diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 01445f6..0000000 --- a/setup.cfg +++ /dev/null @@ -1,42 +0,0 @@ -[metadata] -DEBUG=True - -license = Apache License 2.0 -license_file = LICENSE.md -platforms = any -description = Open-source platform for photo management running on Python 3. -long_description = file: README.md -keywords = photos, self-hosted, google-photos, apple-photos, object-detection, face-recognition, face-detection -classifier = - Intended Audience :: End Users/Desktop - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Operating System :: OS Independent - Topic :: Software Development :: Libraries :: Python Modules - Topic :: Scientific/Engineering :: Atmospheric Science - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - Programming Language :: Python :: 3.10 - -[flake8] -exclude = venv,.git,build -doctests = True -# To work with Black -# E501: line too long -# W503: Line break occurred before a binary operator -# W504 line break after binary operator -ignore = - E501, - E722, - W503, - W504 - -[mypy] -python_version = 3.10 -show_error_codes = true -ignore_errors = true -follow_imports = silent -ignore_missing_imports = true -warn_incomplete_stub = true -warn_redundant_casts = true -warn_unused_configs = true diff --git a/setup.py b/setup.py deleted file mode 100644 index f680eb9..0000000 --- a/setup.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Photos.network setup script""" -import sys - -from setuptools import setup - -from frontend import const - -if sys.version_info < (3, 0): - print("{PROJECT_NAME} requires python version >= 3.0") - sys.exit(1) - -setup( - name="frontend", - version=const.FRONTEND_VERSION, - description="The default web frontend for photos.network", - long_description="The default web frontend for photos.network to manage components.", - author="The Photos.network Authors", - author_email="devs@photos.network", - url="https://developers.photos.network/frontend/", - license="Apache License 2.0", - classifiers=[ - "Intended Audience :: End Users/Desktop", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Operating System :: OS Independent", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Scientific/Engineering :: Atmospheric Science", - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "Programming Language :: Python :: 3.10", - ], - keywords=["docker", "photos-network", "api"], - zip_safe=False, - platforms="any", - packages=[ - "frontend", - "frontend.base", - "frontend.static", - "frontend.templates", - "frontend.views", - ], - entry_points={"console_scripts": ["frontend = frontend.__main__:main"]}, - include_package_data=True, - package_data={ - "frontend": ["locales/**/**/*.mo", "static/**", "templates/*.jinja2", "templates/**/*.jinja2"], - }, - install_requires=[ - "aiohttp>=3.7.0,<4.0", - "python-i18n>=0.3.9", - "aiohttp-jinja2==1.5", - "aiohttp_session==2.11.0", - "aioauth-client==0.27.3", - "cryptography==37.0.4", - "colorlog==6.6.0", - ], -) diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index 459a6eb..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for Photos.network""" diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 036e16e..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Add helpers and fixtures for tests in this directory.""" - -import pytest -from aiohttp import web -from aiohttp.test_utils import TestServer - - -@pytest.fixture() -def mock_server(loop): - app = web.Application() - - test_server = TestServer(app) - return test_server diff --git a/tests/login/test_routes_login.py b/tests/login/test_routes_login.py deleted file mode 100644 index 136db43..0000000 --- a/tests/login/test_routes_login.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Test login routes.""" -from typing import Generator - -import aiohttp_session -from aiohttp import web -from aiohttp.test_utils import TestClient -from aiohttp_session import get_session, new_session -from frontend.application import create_application - - -async def test_login_redirect(aiohttp_client: Generator): - """Test login redirect.""" - # given - app = create_application() - client: TestClient = await aiohttp_client(app) - - # when - resp = await client.get("/login", allow_redirects=False) - - # then - assert resp.status == 302 - - -async def test_logout_endpoint(aiohttp_client: Generator): - """Test if logout endpoint is valid.""" - # given - app = create_application() - client: TestClient = await aiohttp_client(app) - - # when - resp = await client.get("/logout") - - # then - assert resp.status == 200 - - -async def test_logout_redirect(aiohttp_client: Generator): - """Test if logout clears session.""" - # given - app = create_application() - client: TestClient = await aiohttp_client(app) - await client.get("/logout") - - # when - resp = await client.get("/settings", allow_redirects=False) - - # then - assert resp.status == 401 diff --git a/tests/test_internationalization.py b/tests/test_internationalization.py deleted file mode 100644 index f9058cd..0000000 --- a/tests/test_internationalization.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Test application Internationalization.""" -from frontend.application import create_application - - -async def test_login_button_in_de(aiohttp_client): - """Test Internationalized login button in header layout.""" - - app = create_application() - - client = await aiohttp_client(app) - resp = await client.get("/", headers={"Accept-Language": "de"}) - assert resp.status == 200 - text = await resp.text() - assert "Anmeldung" in text - - -async def test_login_button_in_en(aiohttp_client): - """Test Internationalized login button in header layout.""" - - app = create_application() - - client = await aiohttp_client(app) - resp = await client.get("/", headers={"Accept-Language": "en"}) - assert resp.status == 200 - text = await resp.text() - assert "Login" in text diff --git a/tests/test_oauth_flow.py b/tests/test_oauth_flow.py deleted file mode 100644 index 3f79947..0000000 --- a/tests/test_oauth_flow.py +++ /dev/null @@ -1,4 +0,0 @@ - - -# TODO: check authorization url against mock server -# TODO: \ No newline at end of file diff --git a/tests/test_routes_static.py b/tests/test_routes_static.py deleted file mode 100644 index d049fe0..0000000 --- a/tests/test_routes_static.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Test static application routes.""" -from frontend.application import create_application - - -async def test_favicon(aiohttp_client): - """Test favicon delivery.""" - - app = create_application() - - client = await aiohttp_client(app) - resp = await client.get("/static/favicon.ico") - assert resp.status == 200 From e838c81b3bac3abb8ebe42bb7f17a921e6540fdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20St=C3=BCrmer?= Date: Thu, 21 Dec 2023 16:44:16 +0100 Subject: [PATCH 2/3] add starter-axum --- frontend/.gitignore | 13 +++ frontend/Cargo.toml | 113 +++++++++++++++++++++++++ frontend/LICENSE | 21 +++++ frontend/README.md | 86 +++++++++++++++++++ frontend/end2end/package-lock.json | 74 ++++++++++++++++ frontend/end2end/package.json | 13 +++ frontend/end2end/playwright.config.ts | 107 +++++++++++++++++++++++ frontend/end2end/tests/example.spec.ts | 9 ++ frontend/public/favicon.ico | Bin 0 -> 15406 bytes frontend/rust-toolchain.toml | 3 + frontend/src/app.rs | 50 +++++++++++ frontend/src/error_template.rs | 74 ++++++++++++++++ frontend/src/fileserv.rs | 40 +++++++++ frontend/src/lib.rs | 19 +++++ frontend/src/main.rs | 43 ++++++++++ frontend/style/main.scss | 4 + 16 files changed, 669 insertions(+) create mode 100644 frontend/.gitignore create mode 100644 frontend/Cargo.toml create mode 100644 frontend/LICENSE create mode 100644 frontend/README.md create mode 100644 frontend/end2end/package-lock.json create mode 100644 frontend/end2end/package.json create mode 100644 frontend/end2end/playwright.config.ts create mode 100644 frontend/end2end/tests/example.spec.ts create mode 100644 frontend/public/favicon.ico create mode 100644 frontend/rust-toolchain.toml create mode 100644 frontend/src/app.rs create mode 100644 frontend/src/error_template.rs create mode 100644 frontend/src/fileserv.rs create mode 100644 frontend/src/lib.rs create mode 100644 frontend/src/main.rs create mode 100644 frontend/style/main.scss diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..8cdaa33 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,13 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +pkg + +# These are backup files generated by rustfmt +**/*.rs.bk + +# node e2e test tools and outputs +node_modules/ +test-results/ +end2end/playwright-report/ +playwright/.cache/ diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml new file mode 100644 index 0000000..b14e422 --- /dev/null +++ b/frontend/Cargo.toml @@ -0,0 +1,113 @@ +[package] +name = "frontend" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +axum = { version = "0.6.4", optional = true } +console_error_panic_hook = "0.1" +console_log = "1" +cfg-if = "1" +leptos = { version = "0.5", features = ["nightly"] } +leptos_axum = { version = "0.5", optional = true } +leptos_meta = { version = "0.5", features = ["nightly"] } +leptos_router = { version = "0.5", features = ["nightly"] } +log = "0.4" +simple_logger = "4" +tokio = { version = "1.25.0", optional = true } +tower = { version = "0.4.13", optional = true } +tower-http = { version = "0.4", features = ["fs"], optional = true } +wasm-bindgen = "=0.2.89" +thiserror = "1.0.38" +tracing = { version = "0.1.37", optional = true } +http = "0.2.8" + +[features] +hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] +ssr = [ + "dep:axum", + "dep:tokio", + "dep:tower", + "dep:tower-http", + "dep:leptos_axum", + "leptos/ssr", + "leptos_meta/ssr", + "leptos_router/ssr", + "dep:tracing", +] + +# Defines a size-optimized profile for the WASM bundle in release mode +[profile.wasm-release] +inherits = "release" +opt-level = 'z' +lto = true +codegen-units = 1 +panic = "abort" + +[package.metadata.leptos] +# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name +output-name = "frontend" + +# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup. +site-root = "target/site" + +# The site-root relative folder where all compiled output (JS, WASM and CSS) is written +# Defaults to pkg +site-pkg-dir = "pkg" + +# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to //app.css +style-file = "style/main.scss" +# Assets source dir. All files found here will be copied and synchronized to site-root. +# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir. +# +# Optional. Env: LEPTOS_ASSETS_DIR. +assets-dir = "public" + +# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup. +site-addr = "127.0.0.1:3000" + +# The port to use for automatic reload monitoring +reload-port = 3001 + +# [Optional] Command to use when running end2end tests. It will run in the end2end dir. +# [Windows] for non-WSL use "npx.cmd playwright test" +# This binary name can be checked in Powershell with Get-Command npx +end2end-cmd = "npx playwright test" +end2end-dir = "end2end" + +# The browserlist query used for optimizing the CSS. +browserquery = "defaults" + +# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head +watch = false + +# The environment Leptos will run in, usually either "DEV" or "PROD" +env = "DEV" + +# The features to use when compiling the bin target +# +# Optional. Can be over-ridden with the command line parameter --bin-features +bin-features = ["ssr"] + +# If the --no-default-features flag should be used when compiling the bin target +# +# Optional. Defaults to false. +bin-default-features = false + +# The features to use when compiling the lib target +# +# Optional. Can be over-ridden with the command line parameter --lib-features +lib-features = ["hydrate"] + +# If the --no-default-features flag should be used when compiling the lib target +# +# Optional. Defaults to false. +lib-default-features = false + +# The profile to use for the lib target when compiling for release +# +# Optional. Defaults to "release". +lib-profile-release = "wasm-release" diff --git a/frontend/LICENSE b/frontend/LICENSE new file mode 100644 index 0000000..e869ce3 --- /dev/null +++ b/frontend/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 henrik + +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/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..70cb3e8 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,86 @@ + + + Leptos Logo + + +# Leptos Axum Starter Template + +This is a template for use with the [Leptos](https://github.com/leptos-rs/leptos) web framework and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool using [Axum](https://github.com/tokio-rs/axum). + +## Creating your template repo + +If you don't have `cargo-leptos` installed you can install it with + +```bash +cargo install cargo-leptos +``` + +Then run +```bash +cargo leptos new --git leptos-rs/start-axum +``` + +to generate a new project template. + +```bash +cd frontend +``` + +to go to your newly created project. +Feel free to explore the project structure, but the best place to start with your application code is in `src/app.rs`. +Addtionally, Cargo.toml may need updating as new versions of the dependencies are released, especially if things are not working after a `cargo update`. + +## Running your project + +```bash +cargo leptos watch +``` + +## Installing Additional Tools + +By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools. + +1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly +2. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly +3. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future) +4. `npm install -g sass` - install `dart-sass` (should be optional in future + +## Compiling for Release +```bash +cargo leptos build --release +``` + +Will generate your server binary in target/server/release and your site package in target/site + +## Testing Your Project +```bash +cargo leptos end-to-end +``` + +```bash +cargo leptos end-to-end --release +``` + +Cargo-leptos uses Playwright as the end-to-end test tool. +Tests are located in end2end/tests directory. + +## Executing a Server on a Remote Machine Without the Toolchain +After running a `cargo leptos build --release` the minimum files needed are: + +1. The server binary located in `target/server/release` +2. The `site` directory and all files within located in `target/site` + +Copy these files to your remote server. The directory structure should be: +```text +frontend +site/ +``` +Set the following environment variables (updating for your project as needed): +```text +LEPTOS_OUTPUT_NAME="frontend" +LEPTOS_SITE_ROOT="site" +LEPTOS_SITE_PKG_DIR="pkg" +LEPTOS_SITE_ADDR="127.0.0.1:3000" +LEPTOS_RELOAD_PORT="3001" +``` +Finally, run the server binary. diff --git a/frontend/end2end/package-lock.json b/frontend/end2end/package-lock.json new file mode 100644 index 0000000..f12af44 --- /dev/null +++ b/frontend/end2end/package-lock.json @@ -0,0 +1,74 @@ +{ + "name": "end2end", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "end2end", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.28.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz", + "integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "playwright-core": "1.28.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/node": { + "version": "18.11.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", + "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "dev": true + }, + "node_modules/playwright-core": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz", + "integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==", + "dev": true, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=14" + } + } + }, + "dependencies": { + "@playwright/test": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz", + "integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==", + "dev": true, + "requires": { + "@types/node": "*", + "playwright-core": "1.28.0" + } + }, + "@types/node": { + "version": "18.11.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", + "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "dev": true + }, + "playwright-core": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz", + "integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==", + "dev": true + } + } +} diff --git a/frontend/end2end/package.json b/frontend/end2end/package.json new file mode 100644 index 0000000..ed78585 --- /dev/null +++ b/frontend/end2end/package.json @@ -0,0 +1,13 @@ +{ + "name": "end2end", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.28.0" + } +} diff --git a/frontend/end2end/playwright.config.ts b/frontend/end2end/playwright.config.ts new file mode 100644 index 0000000..e9891c0 --- /dev/null +++ b/frontend/end2end/playwright.config.ts @@ -0,0 +1,107 @@ +import type { PlaywrightTestConfig } from "@playwright/test"; +import { devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: "./tests", + /* Maximum time one test can run for. */ + timeout: 30 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000, + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { + ...devices["Desktop Chrome"], + }, + }, + + { + name: "firefox", + use: { + ...devices["Desktop Firefox"], + }, + }, + + { + name: "webkit", + use: { + ...devices["Desktop Safari"], + }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { + // ...devices['Pixel 5'], + // }, + // }, + // { + // name: 'Mobile Safari', + // use: { + // ...devices['iPhone 12'], + // }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { + // channel: 'msedge', + // }, + // }, + // { + // name: 'Google Chrome', + // use: { + // channel: 'chrome', + // }, + // }, + ], + + /* Folder for test artifacts such as screenshots, videos, traces, etc. */ + // outputDir: 'test-results/', + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // port: 3000, + // }, +}; + +export default config; diff --git a/frontend/end2end/tests/example.spec.ts b/frontend/end2end/tests/example.spec.ts new file mode 100644 index 0000000..a461f35 --- /dev/null +++ b/frontend/end2end/tests/example.spec.ts @@ -0,0 +1,9 @@ +import { test, expect } from "@playwright/test"; + +test("homepage has title and links to intro page", async ({ page }) => { + await page.goto("http://localhost:3000/"); + + await expect(page).toHaveTitle("Welcome to Leptos"); + + await expect(page.locator("h1")).toHaveText("Welcome to Leptos!"); +}); diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..2ba8527cb12f5f28f331b8d361eef560492d4c77 GIT binary patch literal 15406 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 k<4FtN!5 impl IntoView { + // Provides context that manages stylesheets, titles, meta tags, etc. + provide_meta_context(); + + view! { + + + // injects a stylesheet into the document + // id=leptos means cargo-leptos will hot-reload this stylesheet + + + // sets the document title + + + // content for this welcome page + <Router fallback=|| { + let mut outside_errors = Errors::default(); + outside_errors.insert_with_default_key(AppError::NotFound); + view! { + <ErrorTemplate outside_errors/> + } + .into_view() + }> + <main> + <Routes> + <Route path="" view=HomePage/> + </Routes> + </main> + </Router> + } +} + +/// Renders the home page of your application. +#[component] +fn HomePage() -> impl IntoView { + // Creates a reactive value to update the button + let (count, set_count) = create_signal(0); + let on_click = move |_| set_count.update(|count| *count += 1); + + view! { + <h1>"Welcome to Leptos!"</h1> + <button on:click=on_click>"Click Me: " {count}</button> + } +} diff --git a/frontend/src/error_template.rs b/frontend/src/error_template.rs new file mode 100644 index 0000000..26a5252 --- /dev/null +++ b/frontend/src/error_template.rs @@ -0,0 +1,74 @@ +use cfg_if::cfg_if; +use http::status::StatusCode; +use leptos::*; +use thiserror::Error; + +#[cfg(feature = "ssr")] +use leptos_axum::ResponseOptions; + +#[derive(Clone, Debug, Error)] +pub enum AppError { + #[error("Not Found")] + NotFound, +} + +impl AppError { + pub fn status_code(&self) -> StatusCode { + match self { + AppError::NotFound => StatusCode::NOT_FOUND, + } + } +} + +// A basic function to display errors served by the error boundaries. +// Feel free to do more complicated things here than just displaying the error. +#[component] +pub fn ErrorTemplate( + #[prop(optional)] outside_errors: Option<Errors>, + #[prop(optional)] errors: Option<RwSignal<Errors>>, +) -> impl IntoView { + let errors = match outside_errors { + Some(e) => create_rw_signal(e), + None => match errors { + Some(e) => e, + None => panic!("No Errors found and we expected errors!"), + }, + }; + // Get Errors from Signal + let errors = errors.get_untracked(); + + // Downcast lets us take a type that implements `std::error::Error` + let errors: Vec<AppError> = errors + .into_iter() + .filter_map(|(_k, v)| v.downcast_ref::<AppError>().cloned()) + .collect(); + println!("Errors: {errors:#?}"); + + // Only the response code for the first error is actually sent from the server + // this may be customized by the specific application + cfg_if! { if #[cfg(feature="ssr")] { + let response = use_context::<ResponseOptions>(); + if let Some(response) = response { + response.set_status(errors[0].status_code()); + } + }} + + view! { + <h1>{if errors.len() > 1 {"Errors"} else {"Error"}}</h1> + <For + // a function that returns the items we're iterating over; a signal is fine + each= move || {errors.clone().into_iter().enumerate()} + // a unique key for each item as a reference + key=|(index, _error)| *index + // renders each item to a view + children=move |error| { + let error_string = error.1.to_string(); + let error_code= error.1.status_code(); + view! { + <h2>{error_code.to_string()}</h2> + <p>"Error: " {error_string}</p> + } + } + /> + } +} diff --git a/frontend/src/fileserv.rs b/frontend/src/fileserv.rs new file mode 100644 index 0000000..acc1635 --- /dev/null +++ b/frontend/src/fileserv.rs @@ -0,0 +1,40 @@ +use cfg_if::cfg_if; + +cfg_if! { if #[cfg(feature = "ssr")] { + use axum::{ + body::{boxed, Body, BoxBody}, + extract::State, + response::IntoResponse, + http::{Request, Response, StatusCode, Uri}, + }; + use axum::response::Response as AxumResponse; + use tower::ServiceExt; + use tower_http::services::ServeDir; + use leptos::*; + use crate::app::App; + + pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse { + let root = options.site_root.clone(); + let res = get_static_file(uri.clone(), &root).await.unwrap(); + + if res.status() == StatusCode::OK { + res.into_response() + } else { + let handler = leptos_axum::render_app_to_stream(options.to_owned(), move || view!{<App/>}); + handler(req).await.into_response() + } + } + + async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> { + let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap(); + // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` + // This path is relative to the cargo root + match ServeDir::new(root).oneshot(req).await { + Ok(res) => Ok(res.map(boxed)), + Err(err) => Err(( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {err}"), + )), + } + } +}} diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs new file mode 100644 index 0000000..0f898c8 --- /dev/null +++ b/frontend/src/lib.rs @@ -0,0 +1,19 @@ +use cfg_if::cfg_if; +pub mod app; +pub mod error_template; +pub mod fileserv; + +cfg_if! { if #[cfg(feature = "hydrate")] { + use leptos::*; + use wasm_bindgen::prelude::wasm_bindgen; + use crate::app::*; + + #[wasm_bindgen] + pub fn hydrate() { + // initializes logging using the `log` crate + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + + leptos::mount_to_body(App); + } +}} diff --git a/frontend/src/main.rs b/frontend/src/main.rs new file mode 100644 index 0000000..bb179d3 --- /dev/null +++ b/frontend/src/main.rs @@ -0,0 +1,43 @@ +#[cfg(feature = "ssr")] +#[tokio::main] +async fn main() { + use axum::{routing::post, Router}; + use leptos::*; + use leptos_axum::{generate_route_list, LeptosRoutes}; + use frontend::app::*; + use frontend::fileserv::file_and_error_handler; + + simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging"); + + // Setting get_configuration(None) means we'll be using cargo-leptos's env values + // For deployment these variables are: + // <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain> + // Alternately a file can be specified such as Some("Cargo.toml") + // The file would need to be included with the executable when moved to deployment + let conf = get_configuration(None).await.unwrap(); + let leptos_options = conf.leptos_options; + let addr = leptos_options.site_addr; + let routes = generate_route_list(App); + + // build our application with a route + let app = Router::new() + .route("/api/*fn_name", post(leptos_axum::handle_server_fns)) + .leptos_routes(&leptos_options, routes, App) + .fallback(file_and_error_handler) + .with_state(leptos_options); + + // run our app with hyper + // `axum::Server` is a re-export of `hyper::Server` + log::info!("listening on http://{}", &addr); + axum::Server::bind(&addr) + .serve(app.into_make_service()) + .await + .unwrap(); +} + +#[cfg(not(feature = "ssr"))] +pub fn main() { + // no client-side main function + // unless we want this to work with e.g., Trunk for a purely client-side app + // see lib.rs for hydration function instead +} diff --git a/frontend/style/main.scss b/frontend/style/main.scss new file mode 100644 index 0000000..e4538e1 --- /dev/null +++ b/frontend/style/main.scss @@ -0,0 +1,4 @@ +body { + font-family: sans-serif; + text-align: center; +} \ No newline at end of file From 19a68a092a5ae93e037aa3c036e3dc1355049821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20St=C3=BCrmer?= <benjamin@stuermer.pro> Date: Thu, 4 Jan 2024 22:28:04 +0100 Subject: [PATCH 3/3] base setup --- CONTRIBUTING.md | 71 + Cargo.lock | 2530 +++++++++++++++++ frontend/Cargo.toml => Cargo.toml | 14 +- README.md | 60 - .../end2end => end2end}/package-lock.json | 0 {frontend/end2end => end2end}/package.json | 0 .../end2end => end2end}/playwright.config.ts | 0 .../end2end => end2end}/tests/example.spec.ts | 2 +- frontend/.gitignore | 13 - frontend/LICENSE | 21 - frontend/README.md | 86 - frontend/public/favicon.ico | Bin 15406 -> 0 bytes frontend/src/app.rs | 50 - frontend/style/main.scss | 4 - input.css | 3 + public/favicon.ico | Bin 0 -> 15406 bytes ...rust-toolchain.toml => rust-toolchain.toml | 0 src/app.rs | 95 + {frontend/src => src}/error_template.rs | 6 + {frontend/src => src}/fileserv.rs | 0 src/footer.rs | 57 + src/header.rs | 40 + {frontend/src => src}/lib.rs | 2 + {frontend/src => src}/main.rs | 0 style/output.css | 937 ++++++ tailwind.config.js | 19 + zellij_layout.kdl | 17 + 27 files changed, 3789 insertions(+), 238 deletions(-) create mode 100644 Cargo.lock rename frontend/Cargo.toml => Cargo.toml (89%) rename {frontend/end2end => end2end}/package-lock.json (100%) rename {frontend/end2end => end2end}/package.json (100%) rename {frontend/end2end => end2end}/playwright.config.ts (100%) rename {frontend/end2end => end2end}/tests/example.spec.ts (84%) delete mode 100644 frontend/.gitignore delete mode 100644 frontend/LICENSE delete mode 100644 frontend/README.md delete mode 100644 frontend/public/favicon.ico delete mode 100644 frontend/src/app.rs delete mode 100644 frontend/style/main.scss create mode 100644 input.css create mode 100644 public/favicon.ico rename frontend/rust-toolchain.toml => rust-toolchain.toml (100%) create mode 100644 src/app.rs rename {frontend/src => src}/error_template.rs (93%) rename {frontend/src => src}/fileserv.rs (100%) create mode 100644 src/footer.rs create mode 100644 src/header.rs rename {frontend/src => src}/lib.rs (93%) rename {frontend/src => src}/main.rs (100%) create mode 100644 style/output.css create mode 100644 tailwind.config.js create mode 100644 zellij_layout.kdl diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b155351..3dd36b3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -82,3 +82,74 @@ Underneath it is using these frameworks: * [tower](https://github.com/tower-rs/tower) - for networking * [axum](https://github.com/tokio-rs/axum) - as web framework * [abi_stable](https://github.com/rodrimati1992/abi_stable_crates) - FFI for dynamic library loading + + +## 🧪 Development + +The frontend is written in 🦀 [Rust](https://rust-lang.org/) using the 🚀 [Leptos](https://leptos.dev/) framework. + +It requires to install [cargo-leptos](https://github.com/leptos-rs/cargo-leptos) and the [tailwind css executable](https://github.com/tailwindlabs/tailwindcss/releases) + +#### 🏃 Running + +First the tailwind css needs to be created if modified +```shell +tailwindcss -i ./input.css -o ./style/output.css --watch +``` + +Next leptos needs to compile the application +```shell +cargo leptos watch +``` + +Open any browser at [http://127.0.0.1:7778](http://127.0.0.1:7778) + + + +#### 🔬 Testing + +```shell +cargo leptos end-to-end +``` + +```shell +cargo leptos end-to-end --release +``` +Cargo-leptos uses Playwright as the end-to-end test tool. +Tests are located in end2end/tests directory. + + +#### 📦 Release + +```shell +cargo leptos build --release +``` + +1. The server binary located in `target/server/release` +2. The `site` directory and all files within located in `target/site` + +Copy these files to your remote server. The directory structure should be: +```text +frontend +site/ +``` +Set the following environment variables (updating for your project as needed): +```text +LEPTOS_OUTPUT_NAME="frontend" +LEPTOS_SITE_ROOT="site" +LEPTOS_SITE_PKG_DIR="pkg" +LEPTOS_SITE_ADDR="127.0.0.1:3000" +LEPTOS_RELOAD_PORT="3001" +``` + + +## 🚀 Release + +To support multiple architectures, an own builder needs to be created. +```shell +docker buildx create --name multiarchitecturebuilder +docker buildx use multiarchitecturebuilder +docker buildx build --platform linux/arm64,linux/amd64 --tag photosnetwork/frontend:latest --push . +``` + + diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..8df3d2b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2530 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "anyhow" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" + +[[package]] +name = "async-recursion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "async-trait" +version = "0.1.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "attribute-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c94f43ede6f25dab1dea046bff84d85dea61bd49aba7a9011ad66c0d449077b" +dependencies = [ + "attribute-derive-macro", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "attribute-derive-macro" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b409e2b2d2dc206d2c0ad3575a93f001ae21a1593e2d0c69b69c308e63f3b422" +dependencies = [ + "collection_literals", + "interpolator", + "manyhow", + "proc-macro-utils", + "proc-macro2", + "quote", + "quote-use", + "syn 2.0.48", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "axum-macros", + "bitflags 1.3.2", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-macros" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdca6a10ecad987bda04e95606ef85a5417dcaac1a78455242d72e031e2b6b62" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "bytecheck" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", + "uuid", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cached" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90eb5776f28a149524d1d8623035760b4454ec881e8cf3838fa8d7e1b11254b3" +dependencies = [ + "cached_proc_macro", + "cached_proc_macro_types", + "hashbrown 0.13.2", + "instant", + "once_cell", + "thiserror", +] + +[[package]] +name = "cached_proc_macro" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c878c71c2821aa2058722038a59a67583a4240524687c6028571c9b395ded61f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cached_proc_macro_types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663" + +[[package]] +name = "camino" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "collection_literals" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186dce98367766de751c42c4f03970fc60fc012296e706ccbb9d5df9b6c1e271" + +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys", +] + +[[package]] +name = "config" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23738e11972c7643e4ec947840fc463b6a571afcd3e735bdfce7d03c7a784aca" +dependencies = [ + "async-trait", + "lazy_static", + "nom", + "pathdiff", + "serde", + "toml", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "console_log" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f" +dependencies = [ + "log", + "web-sys", +] + +[[package]] +name = "const_format" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive-where" +version = "1.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "drain_filter_polyfill" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "frontend" +version = "0.3.0" +dependencies = [ + "axum", + "cfg-if", + "console_error_panic_hook", + "console_log", + "http", + "leptos", + "leptos_axum", + "leptos_meta", + "leptos_router", + "log", + "simple_logger", + "thiserror", + "tokio", + "tower", + "tower-http", + "tracing", + "wasm-bindgen", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "gloo-net" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9902a044653b26b99f7e3693a42f171312d9be8b26b5697bd1e43ad1f8a35e10" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "h2" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.7", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash 0.8.7", + "allocator-api2", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "interpolator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" + +[[package]] +name = "inventory" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8573b2b1fb643a372c73b23f4da5f888677feef3305146d68a539250a9bccc7" + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "js-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "leptos" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d02b78d6e38acf8199426058a0d8c4030835d84a4ee16147df25be7fed707e0" +dependencies = [ + "cfg-if", + "leptos_config", + "leptos_dom", + "leptos_macro", + "leptos_reactive", + "leptos_server", + "server_fn", + "tracing", + "typed-builder", + "typed-builder-macro", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_axum" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6283fd19b98f4f7535b80a32064747028fbf4f88ceac4e2a24e9a7d5ed6caf5a" +dependencies = [ + "axum", + "cfg-if", + "futures", + "http", + "hyper", + "leptos", + "leptos_integration_utils", + "leptos_meta", + "leptos_router", + "once_cell", + "parking_lot", + "serde_json", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "leptos_config" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afcaa5db5b22b794b624e14ffe2aefae215b2d21c60a230ae2d06fe21ae5da64" +dependencies = [ + "config", + "regex", + "serde", + "thiserror", + "typed-builder", +] + +[[package]] +name = "leptos_dom" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af459b63567e8e9c921ecbe7863732dc8dcb7874eaad6826b7d3778a53ec0ea6" +dependencies = [ + "async-recursion", + "cfg-if", + "drain_filter_polyfill", + "futures", + "getrandom", + "html-escape", + "indexmap", + "itertools 0.10.5", + "js-sys", + "leptos_reactive", + "once_cell", + "pad-adapter", + "paste", + "rustc-hash", + "serde", + "serde_json", + "server_fn", + "smallvec", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "leptos_hot_reload" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea60376eb80a24b3ab082612d62211e3ea0fc4dee132f7ff34d5fa5a5108cd2" +dependencies = [ + "anyhow", + "camino", + "indexmap", + "parking_lot", + "proc-macro2", + "quote", + "rstml", + "serde", + "syn 2.0.48", + "walkdir", +] + +[[package]] +name = "leptos_integration_utils" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dbbd7d721bcdd9aa7b20987063de41314c836a3ac67e263ca489857e337dec" +dependencies = [ + "futures", + "leptos", + "leptos_config", + "leptos_hot_reload", + "leptos_meta", + "tracing", +] + +[[package]] +name = "leptos_macro" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7e96f4c450f4b5e2ccb135c2b1328890f911ca4ee89da9ed6d582df929e6cb5" +dependencies = [ + "attribute-derive", + "cfg-if", + "convert_case", + "html-escape", + "itertools 0.11.0", + "leptos_hot_reload", + "prettyplease", + "proc-macro-error", + "proc-macro2", + "quote", + "rstml", + "server_fn_macro", + "syn 2.0.48", + "tracing", + "uuid", +] + +[[package]] +name = "leptos_meta" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "983bbf829598d275b01e96bd9fca71e4739dd7b9fdf69cb8898b30ebfb124332" +dependencies = [ + "cfg-if", + "indexmap", + "leptos", + "tracing", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_reactive" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22207568e096ac153ba8da68635e3136c1ec614ea9012736fa861c05bfb2eeff" +dependencies = [ + "base64", + "cfg-if", + "futures", + "indexmap", + "js-sys", + "paste", + "pin-project", + "rkyv", + "rustc-hash", + "self_cell", + "serde", + "serde-wasm-bindgen", + "serde_json", + "slotmap", + "thiserror", + "tokio", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "leptos_router" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1a2ff8b8e8ae8b17efd8be2a407f7f83ed57c5243f70f2d03e6635f9ff61848" +dependencies = [ + "cached", + "cfg-if", + "gloo-net", + "itertools 0.11.0", + "js-sys", + "lazy_static", + "leptos", + "leptos_integration_utils", + "leptos_meta", + "linear-map", + "lru", + "once_cell", + "percent-encoding", + "regex", + "serde", + "serde_json", + "serde_qs", + "thiserror", + "tracing", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "leptos_server" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "272d018a5adf33d10ee57e6f0f83dccc305c68613cd207e8a653aeebd4cd5b4f" +dependencies = [ + "inventory", + "lazy_static", + "leptos_macro", + "leptos_reactive", + "serde", + "server_fn", + "thiserror", + "tracing", +] + +[[package]] +name = "libc" +version = "0.2.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" + +[[package]] +name = "linear-map" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee" +dependencies = [ + "serde", + "serde_test", +] + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "lru" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a83fb7698b3643a0e34f9ae6f2e8f0178c0fd42f8b59d493aa271ff3a5bf21" +dependencies = [ + "hashbrown 0.14.3", +] + +[[package]] +name = "manyhow" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b76546495d933baa165075b95c0a15e8f7ef75e53f56b19b7144d80fd52bd" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "manyhow-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ba072c0eadade3160232e70893311f1f8903974488096e2eb8e48caba2f0cf1" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "pad-adapter" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d80efc4b6721e8be2a10a5df21a30fa0b470f1539e53d8b4e6e75faf938b63" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "prettyplease" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" +dependencies = [ + "proc-macro2", + "syn 2.0.48", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-utils" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f59e109e2f795a5070e69578c4dc101068139f74616778025ae1011d4cd41a8" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907a61bd0f64c2f29cd1cf1dc34d05176426a3f504a78010f08416ddb7b13708" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", + "version_check", + "yansi", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quote-use" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7b5abe3fe82fdeeb93f44d66a7b444dedf2e4827defb0a8e69c437b2de2ef94" +dependencies = [ + "quote", + "quote-use-macros", + "syn 2.0.48", +] + +[[package]] +name = "quote-use-macros" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ea44c7e20f16017a76a245bb42188517e13d16dcb1aa18044bc406cdc3f4af" +dependencies = [ + "derive-where", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rend" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2571463863a6bd50c32f94402933f03457a3fbaf697a707c5be741e459f08fd" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.11.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rkyv" +version = "0.7.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527a97cdfef66f65998b5f3b637c26f5a5ec09cc52a3f9932313ac645f4190f5" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5c462a1328c8e67e4d6dbad1eb0355dd43e8ab432c6e227a43657f16ade5033" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rstml" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe542870b8f59dd45ad11d382e5339c9a1047cde059be136a7016095bbdefa77" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.48", + "syn_derive", + "thiserror", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "self_cell" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58bf37232d3bb9a2c4e641ca2a11d83b5062066f88df7fed36c28772046d65ba" + +[[package]] +name = "serde" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "serde_json" +version = "1.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_qs" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + +[[package]] +name = "serde_test" +version = "1.0.176" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a2f49ace1498612d14f7e0b8245519584db8299541dfe31a06374a828d620ab" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "server_fn" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfed18dfcc8d9004579c40482c3419c07f60ffb9c5b250542edca99f508b0ce9" +dependencies = [ + "ciborium", + "const_format", + "gloo-net", + "inventory", + "js-sys", + "lazy_static", + "once_cell", + "proc-macro2", + "quote", + "reqwest", + "serde", + "serde_json", + "serde_qs", + "server_fn_macro_default", + "syn 2.0.48", + "thiserror", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b70ae8e22546ba85500391b36c08e3fba64871be8a26557a3663a8e08acb56f" +dependencies = [ + "const_format", + "proc-macro-error", + "proc-macro2", + "quote", + "serde", + "syn 2.0.48", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro_default" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7256ba61dfadb220598db418376e7bc2a34b96df36c4dc48f24ffe161810fc0b" +dependencies = [ + "server_fn_macro", + "syn 2.0.48", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "simdutf8" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" + +[[package]] +name = "simple_logger" +version = "4.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e7e46c8c90251d47d08b28b8a419ffb4aede0f87c2eea95e17d1d5bacbf3ef1" +dependencies = [ + "colored", + "log", + "time", + "windows-sys", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "serde", + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "time" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +dependencies = [ + "deranged", + "itoa", + "libc", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.35.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "hashbrown 0.14.3", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" +dependencies = [ + "bitflags 2.4.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typed-builder" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e47c0496149861b7c95198088cbf36645016b1a0734cf350c50e2a38e070f38a" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "982ee4197351b5c9782847ef5ec1fdcaf50503fb19d68f9771adae314e72b492" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + +[[package]] +name = "uuid" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +dependencies = [ + "getrandom", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" + +[[package]] +name = "web-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53be06678ed9e83edb1745eb72efc0bbcd7b5c3c35711a860906aed827a13d61" + +[[package]] +name = "yansi" +version = "1.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377" + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] diff --git a/frontend/Cargo.toml b/Cargo.toml similarity index 89% rename from frontend/Cargo.toml rename to Cargo.toml index b14e422..46b953f 100644 --- a/frontend/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,16 @@ [package] name = "frontend" -version = "0.1.0" +authors = ["Photos network developers <info@photos.network>"] +description = "A privacy first **photo storage and sharing service** in the fediverse." +version = "0.3.0" +homepage = "https://photos.network/" +documentation = "https://developers.photos.network/" +repository = "https://github.com/photos-network/frontend" +readme = "README.md" +license = "AGPL-3.0" edition = "2021" + [lib] crate-type = ["cdylib", "rlib"] @@ -59,7 +67,7 @@ site-root = "target/site" site-pkg-dir = "pkg" # [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css -style-file = "style/main.scss" +style-file = "style/output.css" # Assets source dir. All files found here will be copied and synchronized to site-root. # The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir. # @@ -67,7 +75,7 @@ style-file = "style/main.scss" assets-dir = "public" # The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup. -site-addr = "127.0.0.1:3000" +site-addr = "127.0.0.1:7778" # The port to use for automatic reload monitoring reload-port = 3001 diff --git a/README.md b/README.md index 5512004..08941d6 100644 --- a/README.md +++ b/README.md @@ -31,66 +31,6 @@ This is a free and open project and lives from contributions of the community. See our [Contribution Guide](CONTRIBUTING.md) -## 🧪 Development - -The frontend is written in 🦀 [Rust](https://rust-lang.org/) using the [Leptos](https://leptos.dev/) framework. - - -#### 🏃 Running - -```shell -cargo leptos watch -``` - - -#### 🔬 Testing - -```shell -cargo leptos end-to-end -``` - -```shell -cargo leptos end-to-end --release -``` - -Cargo-leptos uses Playwright as the end-to-end test tool. -Tests are located in end2end/tests directory. - - -#### 📦 Release - -```shell -cargo leptos build --release -``` - -1. The server binary located in `target/server/release` -2. The `site` directory and all files within located in `target/site` - -Copy these files to your remote server. The directory structure should be: -```text -frontend -site/ -``` -Set the following environment variables (updating for your project as needed): -```text -LEPTOS_OUTPUT_NAME="frontend" -LEPTOS_SITE_ROOT="site" -LEPTOS_SITE_PKG_DIR="pkg" -LEPTOS_SITE_ADDR="127.0.0.1:3000" -LEPTOS_RELOAD_PORT="3001" -``` - - -## 🚀 Release - -To support multiple architectures, an own builder needs to be created. -```shell -docker buildx create --name multiarchitecturebuilder -docker buildx use multiarchitecturebuilder -docker buildx build --platform linux/arm64,linux/amd64 --tag photosnetwork/frontend:latest --push . -``` - - ## 🏛️ License diff --git a/frontend/end2end/package-lock.json b/end2end/package-lock.json similarity index 100% rename from frontend/end2end/package-lock.json rename to end2end/package-lock.json diff --git a/frontend/end2end/package.json b/end2end/package.json similarity index 100% rename from frontend/end2end/package.json rename to end2end/package.json diff --git a/frontend/end2end/playwright.config.ts b/end2end/playwright.config.ts similarity index 100% rename from frontend/end2end/playwright.config.ts rename to end2end/playwright.config.ts diff --git a/frontend/end2end/tests/example.spec.ts b/end2end/tests/example.spec.ts similarity index 84% rename from frontend/end2end/tests/example.spec.ts rename to end2end/tests/example.spec.ts index a461f35..628f97d 100644 --- a/frontend/end2end/tests/example.spec.ts +++ b/end2end/tests/example.spec.ts @@ -1,7 +1,7 @@ import { test, expect } from "@playwright/test"; test("homepage has title and links to intro page", async ({ page }) => { - await page.goto("http://localhost:3000/"); + await page.goto("http://localhost:7778/"); await expect(page).toHaveTitle("Welcome to Leptos"); diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index 8cdaa33..0000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -/target/ -pkg - -# These are backup files generated by rustfmt -**/*.rs.bk - -# node e2e test tools and outputs -node_modules/ -test-results/ -end2end/playwright-report/ -playwright/.cache/ diff --git a/frontend/LICENSE b/frontend/LICENSE deleted file mode 100644 index e869ce3..0000000 --- a/frontend/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2022 henrik - -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/frontend/README.md b/frontend/README.md deleted file mode 100644 index 70cb3e8..0000000 --- a/frontend/README.md +++ /dev/null @@ -1,86 +0,0 @@ -<picture> - <source srcset="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_Solid_White.svg" media="(prefers-color-scheme: dark)"> - <img src="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_RGB.svg" alt="Leptos Logo"> -</picture> - -# Leptos Axum Starter Template - -This is a template for use with the [Leptos](https://github.com/leptos-rs/leptos) web framework and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool using [Axum](https://github.com/tokio-rs/axum). - -## Creating your template repo - -If you don't have `cargo-leptos` installed you can install it with - -```bash -cargo install cargo-leptos -``` - -Then run -```bash -cargo leptos new --git leptos-rs/start-axum -``` - -to generate a new project template. - -```bash -cd frontend -``` - -to go to your newly created project. -Feel free to explore the project structure, but the best place to start with your application code is in `src/app.rs`. -Addtionally, Cargo.toml may need updating as new versions of the dependencies are released, especially if things are not working after a `cargo update`. - -## Running your project - -```bash -cargo leptos watch -``` - -## Installing Additional Tools - -By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools. - -1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly -2. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly -3. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future) -4. `npm install -g sass` - install `dart-sass` (should be optional in future - -## Compiling for Release -```bash -cargo leptos build --release -``` - -Will generate your server binary in target/server/release and your site package in target/site - -## Testing Your Project -```bash -cargo leptos end-to-end -``` - -```bash -cargo leptos end-to-end --release -``` - -Cargo-leptos uses Playwright as the end-to-end test tool. -Tests are located in end2end/tests directory. - -## Executing a Server on a Remote Machine Without the Toolchain -After running a `cargo leptos build --release` the minimum files needed are: - -1. The server binary located in `target/server/release` -2. The `site` directory and all files within located in `target/site` - -Copy these files to your remote server. The directory structure should be: -```text -frontend -site/ -``` -Set the following environment variables (updating for your project as needed): -```text -LEPTOS_OUTPUT_NAME="frontend" -LEPTOS_SITE_ROOT="site" -LEPTOS_SITE_PKG_DIR="pkg" -LEPTOS_SITE_ADDR="127.0.0.1:3000" -LEPTOS_RELOAD_PORT="3001" -``` -Finally, run the server binary. diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico deleted file mode 100644 index 2ba8527cb12f5f28f331b8d361eef560492d4c77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15406 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 diff --git a/frontend/src/app.rs b/frontend/src/app.rs deleted file mode 100644 index 03b4b17..0000000 --- a/frontend/src/app.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::error_template::{AppError, ErrorTemplate}; -use leptos::*; -use leptos_meta::*; -use leptos_router::*; - -#[component] -pub fn App() -> impl IntoView { - // Provides context that manages stylesheets, titles, meta tags, etc. - provide_meta_context(); - - view! { - - - // injects a stylesheet into the document <head> - // id=leptos means cargo-leptos will hot-reload this stylesheet - <Stylesheet id="leptos" href="/pkg/frontend.css"/> - - // sets the document title - <Title text="Welcome to Leptos"/> - - // content for this welcome page - <Router fallback=|| { - let mut outside_errors = Errors::default(); - outside_errors.insert_with_default_key(AppError::NotFound); - view! { - <ErrorTemplate outside_errors/> - } - .into_view() - }> - <main> - <Routes> - <Route path="" view=HomePage/> - </Routes> - </main> - </Router> - } -} - -/// Renders the home page of your application. -#[component] -fn HomePage() -> impl IntoView { - // Creates a reactive value to update the button - let (count, set_count) = create_signal(0); - let on_click = move |_| set_count.update(|count| *count += 1); - - view! { - <h1>"Welcome to Leptos!"</h1> - <button on:click=on_click>"Click Me: " {count}</button> - } -} diff --git a/frontend/style/main.scss b/frontend/style/main.scss deleted file mode 100644 index e4538e1..0000000 --- a/frontend/style/main.scss +++ /dev/null @@ -1,4 +0,0 @@ -body { - font-family: sans-serif; - text-align: center; -} \ No newline at end of file diff --git a/input.css b/input.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..81b5bba69bb4a342e453d10244f9cbc16fd4de4b GIT binary patch literal 15406 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 literal 0 HcmV?d00001 diff --git a/frontend/rust-toolchain.toml b/rust-toolchain.toml similarity index 100% rename from frontend/rust-toolchain.toml rename to rust-toolchain.toml diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..f9f2b61 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,95 @@ +use crate::error_template::{AppError, ErrorTemplate}; +use crate::footer::Footer; +use crate::header::Header; +use leptos::*; +use leptos_meta::*; +use leptos_router::*; + +#[component] +pub fn App() -> impl IntoView { + // Provides context that manages stylesheets, titles, meta tags, etc. + provide_meta_context(); + + view! { + // injects a stylesheet into the document <head> + // id=leptos means cargo-leptos will hot-reload this stylesheet + <Stylesheet id="leptos" href="/pkg/frontend.css"/> + + // sets the document title + <Title text="Photos.network"/> + + <Header/> + + + // content for this welcome page + <Router fallback=|| { + let mut outside_errors = Errors::default(); + outside_errors.insert_with_default_key(AppError::NotFound); + view! { + <ErrorTemplate outside_errors/> + } + .into_view() + }> + <main> + <Routes> + <Route path="" view=HomePage/> + </Routes> + </main> + </Router> + } +} + +/// Renders the home page of your application. +#[component] +fn HomePage() -> impl IntoView { + // Creates a reactive value to update the button + let (count, set_count) = create_signal(0); + let on_click = move |_| set_count.update(|count| *count += 1); + + view! { + <main class="ui main container mx-auto"> + <h1>"Photos.network"</h1> + <button + class="bg-accent hover:bg-sky-700 px-5 py-3 text-white rounded-lg" + on:click=on_click + >"Click Me: " {count}</button> + </main> + <MobileApp/> + <Footer/> + } +} + +#[component] +fn MobileApp() -> impl IntoView { + view! { + <section class="text-gray-600 py-8 body-font"> + <div class="container border border-gray-200 rounded-lg px-10 py-10 mx-auto flex items-center md:flex-row flex-col"> + <div class="flex flex-col md:pr-10 md:mb-0 mb-6 pr-0 w-full md:w-auto md:text-left text-center"> + <h2 class="text-xs text-accent tracking-widest font-medium title-font mb-1">NATIVE APP</h2> + <h1 class="md:text-3xl text-2xl font-medium title-font text-gray-900">Try our native mobile apps</h1> + </div> + <div class="flex md:ml-auto md:mr-0 mx-auto items-center flex-shrink-0 space-x-4"> + <button onclick="window.open('https://play.google.com/store/apps/details?id=photos.network')" class="bg-gray-100 inline-flex py-3 px-5 rounded-lg items-center hover:bg-gray-200 focus:outline-none"> + <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="w-6 h-6" viewBox="0 0 512 512"> + <path d="M99.617 8.057a50.191 50.191 0 00-38.815-6.713l230.932 230.933 74.846-74.846L99.617 8.057zM32.139 20.116c-6.441 8.563-10.148 19.077-10.148 30.199v411.358c0 11.123 3.708 21.636 10.148 30.199l235.877-235.877L32.139 20.116zM464.261 212.087l-67.266-37.637-81.544 81.544 81.548 81.548 67.273-37.64c16.117-9.03 25.738-25.442 25.738-43.908s-9.621-34.877-25.749-43.907zM291.733 279.711L60.815 510.629c3.786.891 7.639 1.371 11.492 1.371a50.275 50.275 0 0027.31-8.07l266.965-149.372-74.849-74.847z"></path> + </svg> + <span class="ml-4 flex items-start flex-col leading-none"> + <span class="text-xs text-gray-600 mb-1">GET IT ON</span> + <span class="title-font font-medium">Google Play</span> + </span> + </button> + <button class="bg-gray-50 inline-flex py-3 px-5 rounded-lg items-center focus:outline-none"> + <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="w-6 h-6" viewBox="0 0 305 305"> + <path d="M40.74 112.12c-25.79 44.74-9.4 112.65 19.12 153.82C74.09 286.52 88.5 305 108.24 305c.37 0 .74 0 1.13-.02 9.27-.37 15.97-3.23 22.45-5.99 7.27-3.1 14.8-6.3 26.6-6.3 11.22 0 18.39 3.1 25.31 6.1 6.83 2.95 13.87 6 24.26 5.81 22.23-.41 35.88-20.35 47.92-37.94a168.18 168.18 0 0021-43l.09-.28a2.5 2.5 0 00-1.33-3.06l-.18-.08c-3.92-1.6-38.26-16.84-38.62-58.36-.34-33.74 25.76-51.6 31-54.84l.24-.15a2.5 2.5 0 00.7-3.51c-18-26.37-45.62-30.34-56.73-30.82a50.04 50.04 0 00-4.95-.24c-13.06 0-25.56 4.93-35.61 8.9-6.94 2.73-12.93 5.09-17.06 5.09-4.64 0-10.67-2.4-17.65-5.16-9.33-3.7-19.9-7.9-31.1-7.9l-.79.01c-26.03.38-50.62 15.27-64.18 38.86z"></path> + <path d="M212.1 0c-15.76.64-34.67 10.35-45.97 23.58-9.6 11.13-19 29.68-16.52 48.38a2.5 2.5 0 002.29 2.17c1.06.08 2.15.12 3.23.12 15.41 0 32.04-8.52 43.4-22.25 11.94-14.5 17.99-33.1 16.16-49.77A2.52 2.52 0 00212.1 0z"></path> + </svg> + <span class="ml-4 flex items-start flex-col leading-none"> + <span class="text-xs text-gray-600 mb-1">Download on the</span> + <span class="title-font font-medium">App Store</span> + </span> + </button> + </div> + </div> + </section> + } +} diff --git a/frontend/src/error_template.rs b/src/error_template.rs similarity index 93% rename from frontend/src/error_template.rs rename to src/error_template.rs index 26a5252..11d6e37 100644 --- a/frontend/src/error_template.rs +++ b/src/error_template.rs @@ -2,6 +2,8 @@ use cfg_if::cfg_if; use http::status::StatusCode; use leptos::*; use thiserror::Error; +use crate::footer::Footer; +use crate::header::Header; #[cfg(feature = "ssr")] use leptos_axum::ResponseOptions; @@ -54,6 +56,8 @@ pub fn ErrorTemplate( }} view! { + <Header/> + <main class="ui main container mx-auto"> <h1>{if errors.len() > 1 {"Errors"} else {"Error"}}</h1> <For // a function that returns the items we're iterating over; a signal is fine @@ -70,5 +74,7 @@ pub fn ErrorTemplate( } } /> + </main> + <Footer/> } } diff --git a/frontend/src/fileserv.rs b/src/fileserv.rs similarity index 100% rename from frontend/src/fileserv.rs rename to src/fileserv.rs diff --git a/src/footer.rs b/src/footer.rs new file mode 100644 index 0000000..b2e617b --- /dev/null +++ b/src/footer.rs @@ -0,0 +1,57 @@ +use cfg_if::cfg_if; +use http::status::StatusCode; +use leptos::*; +use thiserror::Error; + +#[component] +pub fn Footer() -> impl IntoView { + view! { + <footer class="bg-gray-100 text-gray-600 body-font"> + <div class="container flex mx-auto px-5 py-8 sm:flex-row flex-col items-center"> + <a class="flex title-font font-medium items-center md:justify-start justify-center text-gray-900"> + <svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 2008 2008" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"> + <g id="außen" transform="matrix(1,0,0,1,-328.484,-623.52)"> + <path d="M1094.51,2405.45L1332.42,2268.1L1570.33,2405.45L1332.42,2542.81L1094.51,2405.45ZM1245.66,2218.01L1007.75,2355.36L864.368,2272.58L864.368,1997.87L1245.66,2218.01ZM1800.48,2272.58L1657.09,2355.36L1419.18,2218.01L1800.48,1997.87L1800.48,2272.58ZM2125.14,2085.13L1887.23,2222.49L1887.23,1947.78L2125.14,1810.42L2125.14,2085.13ZM539.701,1810.42L777.61,1947.78L777.61,2222.49L539.701,2085.13L539.701,1810.42ZM2125.14,1544.67L2125.14,1710.24L1887.23,1847.6L1887.23,1407.32L2125.14,1544.67ZM777.61,1847.6L539.701,1710.24L539.701,1544.67L777.61,1407.32L777.61,1847.6ZM777.61,1032.42L777.61,1307.14L539.701,1444.49L539.701,1169.78L777.61,1032.42ZM2125.14,1169.78L2125.14,1444.49L1887.23,1307.14L1887.23,1032.42L2125.14,1169.78ZM1245.66,1036.9L864.368,1257.05L864.368,982.333L1007.75,899.549L1245.66,1036.9ZM1800.48,982.333L1800.48,1257.05L1419.18,1036.9L1657.09,899.549L1800.48,982.333ZM1570.33,849.459L1332.42,986.816L1094.51,849.459L1332.42,712.102L1570.33,849.459Z" style="fill:rgb(2,0,50);"/> + </g> + <g id="außen1" serif:id="außen" transform="matrix(1,0,0,1,-328.484,-623.52)"> + <path d="M1800.48,1357.23L1800.48,1897.69L1332.42,2167.92L864.368,1897.69L864.368,1357.23L1332.42,1087L1800.48,1357.23Z" style="fill:rgb(113,108,255);"/> + </g> + </svg> + + <span class="ml-3 text-xl">Photos.network</span> + </a> + <p class="text-sm text-gray-500 sm:ml-4 sm:pl-4 sm:border-l-2 sm:border-gray-200 sm:py-2 sm:mt-0 mt-4"> + A free and open source, privacy first, self-hosted photo storage and sharing service. + </p> + <span class="inline-flex sm:ml-auto sm:mt-0 mt-4 justify-center sm:justify-start"> + + <a class="ml-3 text-gray-500" href="https://photos.network" rel="noopener noreferrer" target="_blank"> + <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>Website + + + + + + Github + + + + + + Mastodon + + + + + + Twitter + + + + + + + + } +} + diff --git a/src/header.rs b/src/header.rs new file mode 100644 index 0000000..e0d372f --- /dev/null +++ b/src/header.rs @@ -0,0 +1,40 @@ +use cfg_if::cfg_if; +use http::status::StatusCode; +use leptos::*; +use thiserror::Error; + + +#[component] +pub fn Header() -> impl IntoView { + view! { +
+
+ + + + + + + + + + Photos.network + + + + + +
+ + + + + +
+ +
+
+ } +} diff --git a/frontend/src/lib.rs b/src/lib.rs similarity index 93% rename from frontend/src/lib.rs rename to src/lib.rs index 0f898c8..1bbf13f 100644 --- a/frontend/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,8 @@ use cfg_if::cfg_if; pub mod app; pub mod error_template; +pub mod footer; +pub mod header; pub mod fileserv; cfg_if! { if #[cfg(feature = "hydrate")] { diff --git a/frontend/src/main.rs b/src/main.rs similarity index 100% rename from frontend/src/main.rs rename to src/main.rs diff --git a/style/output.css b/style/output.css new file mode 100644 index 0000000..dace810 --- /dev/null +++ b/style/output.css @@ -0,0 +1,937 @@ +/* +! tailwindcss v3.4.0 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +7. Disable tap highlights on iOS +*/ + +html, +:host { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ + font-variation-settings: normal; + /* 6 */ + -webkit-tap-highlight-color: transparent; + /* 7 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font-family by default. +2. Use the user's configured `mono` font-feature-settings by default. +3. Use the user's configured `mono` font-variation-settings by default. +4. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-feature-settings: normal; + /* 2 */ + font-variation-settings: normal; + /* 3 */ + font-size: 1em; + /* 4 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-feature-settings: inherit; + /* 1 */ + font-variation-settings: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Reset default styling for dialogs. +*/ + +dialog { + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden] { + display: none; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +.container { + width: 100%; +} + +@media (min-width: 640px) { + .container { + max-width: 640px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 768px; + } +} + +@media (min-width: 1024px) { + .container { + max-width: 1024px; + } +} + +@media (min-width: 1280px) { + .container { + max-width: 1280px; + } +} + +@media (min-width: 1536px) { + .container { + max-width: 1536px; + } +} + +.relative { + position: relative; +} + +.mx-auto { + margin-left: auto; + margin-right: auto; +} + +.mb-1 { + margin-bottom: 0.25rem; +} + +.mb-4 { + margin-bottom: 1rem; +} + +.mb-6 { + margin-bottom: 1.5rem; +} + +.ml-3 { + margin-left: 0.75rem; +} + +.ml-4 { + margin-left: 1rem; +} + +.mr-5 { + margin-right: 1.25rem; +} + +.mt-4 { + margin-top: 1rem; +} + +.flex { + display: flex; +} + +.inline-flex { + display: inline-flex; +} + +.h-10 { + height: 2.5rem; +} + +.h-5 { + height: 1.25rem; +} + +.h-6 { + height: 1.5rem; +} + +.w-10 { + width: 2.5rem; +} + +.w-5 { + width: 1.25rem; +} + +.w-6 { + width: 1.5rem; +} + +.w-full { + width: 100%; +} + +.flex-shrink-0 { + flex-shrink: 0; +} + +.transform { + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.flex-col { + flex-direction: column; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.items-start { + align-items: flex-start; +} + +.items-center { + align-items: center; +} + +.justify-center { + justify-content: center; +} + +.space-x-4 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(1rem * var(--tw-space-x-reverse)); + margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); +} + +.rounded-full { + border-radius: 9999px; +} + +.rounded-lg { + border-radius: 0.5rem; +} + +.border { + border-width: 1px; +} + +.border-gray-200 { + --tw-border-opacity: 1; + border-color: rgb(229 231 235 / var(--tw-border-opacity)); +} + +.bg-accent { + --tw-bg-opacity: 1; + background-color: rgb(112 108 246 / var(--tw-bg-opacity)); +} + +.bg-gray-100 { + --tw-bg-opacity: 1; + background-color: rgb(243 244 246 / var(--tw-bg-opacity)); +} + +.bg-gray-50 { + --tw-bg-opacity: 1; + background-color: rgb(249 250 251 / var(--tw-bg-opacity)); +} + +.bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.p-2 { + padding: 0.5rem; +} + +.px-10 { + padding-left: 2.5rem; + padding-right: 2.5rem; +} + +.px-5 { + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +.py-10 { + padding-top: 2.5rem; + padding-bottom: 2.5rem; +} + +.py-3 { + padding-top: 0.75rem; + padding-bottom: 0.75rem; +} + +.py-8 { + padding-top: 2rem; + padding-bottom: 2rem; +} + +.pr-0 { + padding-right: 0px; +} + +.text-center { + text-align: center; +} + +.text-2xl { + font-size: 1.5rem; + line-height: 2rem; +} + +.text-base { + font-size: 1rem; + line-height: 1.5rem; +} + +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.text-xs { + font-size: 0.75rem; + line-height: 1rem; +} + +.font-medium { + font-weight: 500; +} + +.leading-none { + line-height: 1; +} + +.tracking-widest { + letter-spacing: 0.1em; +} + +.text-accent { + --tw-text-opacity: 1; + color: rgb(112 108 246 / var(--tw-text-opacity)); +} + +.text-gray-300 { + --tw-text-opacity: 1; + color: rgb(209 213 219 / var(--tw-text-opacity)); +} + +.text-gray-400 { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); +} + +.text-gray-500 { + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity)); +} + +.text-gray-600 { + --tw-text-opacity: 1; + color: rgb(75 85 99 / var(--tw-text-opacity)); +} + +.text-gray-900 { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + +.text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.shadow-lg { + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.hover\:bg-gray-200:hover { + --tw-bg-opacity: 1; + background-color: rgb(229 231 235 / var(--tw-bg-opacity)); +} + +.hover\:bg-sky-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(3 105 161 / var(--tw-bg-opacity)); +} + +.hover\:text-white:hover { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.focus\:outline-none:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +@media (min-width: 640px) { + .sm\:ml-4 { + margin-left: 1rem; + } + + .sm\:ml-auto { + margin-left: auto; + } + + .sm\:mt-0 { + margin-top: 0px; + } + + .sm\:flex-row { + flex-direction: row; + } + + .sm\:justify-start { + justify-content: flex-start; + } + + .sm\:border-l-2 { + border-left-width: 2px; + } + + .sm\:border-gray-200 { + --tw-border-opacity: 1; + border-color: rgb(229 231 235 / var(--tw-border-opacity)); + } + + .sm\:py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + } + + .sm\:pl-4 { + padding-left: 1rem; + } +} + +@media (min-width: 768px) { + .md\:mb-0 { + margin-bottom: 0px; + } + + .md\:ml-auto { + margin-left: auto; + } + + .md\:mr-0 { + margin-right: 0px; + } + + .md\:w-auto { + width: auto; + } + + .md\:flex-row { + flex-direction: row; + } + + .md\:justify-start { + justify-content: flex-start; + } + + .md\:pr-10 { + padding-right: 2.5rem; + } + + .md\:text-left { + text-align: left; + } + + .md\:text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; + } +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..fb3eaf8 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,19 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: { + files: ["*.html", "./src/**/*.rs"], + }, + theme: { + extend: { + colors: { + clifford: '#da373d', + accent: '#706CF6', + error: '#F2BAB9', + success: '#D2ECAE', + warn: '#F9F4BC', + neutral: '#CEEFF4', + } + }, + }, + plugins: [], +} diff --git a/zellij_layout.kdl b/zellij_layout.kdl new file mode 100644 index 0000000..898971d --- /dev/null +++ b/zellij_layout.kdl @@ -0,0 +1,17 @@ +layout { + pane { + command="hx ." + name="code" + } + pane split_direction="vertical" { + pane { + command="cargo leptos watch" + name="leptos" + } + pane { + command="tailwindcss -i ./input.css -o ./style/output.css --watch" + name="tailwind" + } + } +} +